diff --git a/.github/workflows/vetkeys-password-manager-with-metadata.yml b/.github/workflows/vetkeys-password-manager-with-metadata.yml new file mode 100644 index 000000000..95dfa5679 --- /dev/null +++ b/.github/workflows/vetkeys-password-manager-with-metadata.yml @@ -0,0 +1,36 @@ +name: vetkeys-password-manager-with-metadata + +on: + push: + branches: + - master + pull_request: + paths: + - rust/vetkeys/password_manager_with_metadata/** + - .github/workflows/vetkeys-password-manager-with-metadata.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust:0.1.0 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy Password Manager With Metadata Rust + working-directory: rust/vetkeys/password_manager_with_metadata/rust + run: icp network start -d && icp deploy + motoko: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-motoko:0.1.0 + env: + ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy Password Manager With Metadata Motoko + working-directory: rust/vetkeys/password_manager_with_metadata/motoko + run: icp network start -d && icp deploy diff --git a/rust/vetkeys/password_manager_with_metadata/README.md b/rust/vetkeys/password_manager_with_metadata/README.md index 0f8d2e80f..719685c3a 100644 --- a/rust/vetkeys/password_manager_with_metadata/README.md +++ b/rust/vetkeys/password_manager_with_metadata/README.md @@ -1,8 +1,10 @@ # VetKey Password Manager with Metadata + The **VetKey Password Manager** is an example application demonstrating how to use **VetKeys** and **Encrypted Maps** to build a secure, decentralized password manager on the **Internet Computer (IC)**. This application allows users to create password vaults, store encrypted passwords, and share vaults with other users via their **Internet Identity Principal**. @@ -19,39 +21,61 @@ This version of the application extends the basic password manager by supporting ### Prerequisites -- [Local Internet Computer dev environment](https://internetcomputer.org/docs/building-apps/getting-started/install) +- [ICP CLI](https://cli.internetcomputer.org) - [npm](https://www.npmjs.com/package/npm) ### (Optionally) Choose a Different Master Key -This example uses `test_key_1` by default. To use a different [available master key](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/api#available-master-keys), change the `"init_arg": "(\"test_key_1\")"` line in `dfx.json` to the desired key before running `dfx deploy` in the next step. +This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` to the desired key before running `icp deploy` in the next step. + +### Folder Structure + +This example provides both a **Rust** and a **Motoko** backend, sharing a common `frontend/`: + +``` +password_manager_with_metadata/ +├── frontend/ ← shared frontend (symlinked into rust/ and motoko/) +├── motoko/ ← Motoko backend + icp.yaml +└── rust/ ← Rust backend + icp.yaml +``` ### Deploy the Canisters Locally -If you want to deploy this project locally with a Motoko backend, then run: +Deploy with the **Motoko** backend: ```bash -dfx start --background && dfx deploy +cd motoko +icp network start -d && icp deploy ``` -from the `motoko` folder. -To use the Rust backend instead of Motoko, run the same command in the `rust` folder. +Or deploy with the **Rust** backend: +```bash +cd rust +icp network start -d && icp deploy +``` -## Running the Project +To run the frontend in development mode with hot reloading (after running `icp deploy`): +```bash +cd frontend +npm run dev:motoko # if you deployed the Motoko backend +# or +npm run dev:rust # if you deployed the Rust backend +``` + +When you are done testing, stop the local network to free up resources and unblock the default port for other projects: +```bash +icp network stop +``` + +## Example Components ### Backend -The backend consists of an **Encrypted Maps**-enabled canister that securely stores passwords. It is automatically deployed with `dfx deploy`. +The backend consists of an **Encrypted Maps**-enabled canister that securely stores passwords. ### Frontend The frontend is a **Svelte** application providing a user-friendly interface for managing vaults and passwords. -To run the frontend in development mode with hot reloading: - -```bash -npm run dev -``` - ## Limitations This example dapp does not implement key rotation, which is strongly recommended in a production environment. diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/package.json b/rust/vetkeys/password_manager_with_metadata/frontend/package.json index 49dd1e0e1..a50afea07 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/package.json +++ b/rust/vetkeys/password_manager_with_metadata/frontend/package.json @@ -5,7 +5,9 @@ "type": "module", "scripts": { "build": "npm run build:bindings && vite build", - "dev": "npm run build:bindings && vite", + "dev": "printf '\\nNo backend specified. Use one of:\\n\\n npm run dev:motoko\\n npm run dev:rust\\n\\n' && exit 1", + "dev:motoko": "npm run build:bindings && BACKEND=motoko vite", + "dev:rust": "npm run build:bindings && BACKEND=rust vite", "build:bindings": "cd scripts && ./gen_bindings.sh", "lint": "eslint", "prettier": "prettier --write .", @@ -32,16 +34,12 @@ "typescript-eslint": "^8.26.1", "vite": "^5.4.21", "vite-plugin-compression": "^0.5.1", - "vite-plugin-environment": "^1.1.3", "vite-plugin-eslint": "^1.8.1" }, "dependencies": { - "@dfinity/agent": "^2.3.0", - "@dfinity/auth-client": "^2.3.0", - "@dfinity/candid": "^2.3.0", - "@dfinity/identity": "^2.3.0", - "@dfinity/principal": "^2.3.0", - "@dfinity/vetkeys": "^0.3.0", + "@icp-sdk/auth": "^7.1.0", + "@icp-sdk/core": "^5.4.0", + "@icp-sdk/vetkeys": "^0.5.0-beta.0", "@sveltejs/vite-plugin-svelte": "^3.0.2", "daisyui": "^4.12.23", "save": "^2.9.0", diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh b/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh index e8f13011b..624b67835 100755 --- a/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh +++ b/rust/vetkeys/password_manager_with_metadata/frontend/scripts/gen_bindings.sh @@ -1,15 +1,17 @@ #!/bin/bash +# Bindings are always generated from the Rust backend since both backends +# expose the same Candid interface. -cd ../../backend && make extract-candid - -cd .. && dfx generate password_manager_with_metadata || exit 1 - -rm -r frontend/src/declarations/password_manager_with_metadata > /dev/null 2>&1 || true +# Resolve the physical path of this script so that navigating up works +# correctly even when frontend/ is reached via a symlink (e.g. motoko/frontend). +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd -P) +if command -v candid-extractor >/dev/null 2>&1; then + cd "$SCRIPT_DIR/../../rust/backend" && make extract-candid +fi +cd "$SCRIPT_DIR/../.." +rm -rf frontend/src/declarations/password_manager_with_metadata mkdir -p frontend/src/declarations/password_manager_with_metadata -mv src/declarations/password_manager_with_metadata frontend/src/declarations -rmdir -p src/declarations > /dev/null 2>&1 || true - -# dfx 0.31+ generates @icp-sdk/core imports; rewrite to @dfinity/* to match deps -find frontend/src/declarations -type f \( -name '*.ts' -o -name '*.js' \) -exec \ - perl -i -pe 's|\@icp-sdk/core/agent|\@dfinity/agent|g; s|\@icp-sdk/core/principal|\@dfinity/principal|g; s|\@icp-sdk/core/candid|\@dfinity/candid|g' {} + \ No newline at end of file +npx --yes @icp-sdk/bindgen --did-file rust/backend/backend.did \ + --out-dir frontend/src/declarations/password_manager_with_metadata \ + --declarations-flat --force diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte index 336ffde7d..7cf597760 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/DisclaimerCopy.svelte @@ -1,5 +1,5 @@ -Disclaimer: This sample dapp is intended exclusively for experimental -purpose. You are advised not to use this dapp for storing your critical data such +Disclaimer: This sample app is intended exclusively for experimental +purpose. You are advised not to use this app for storing your critical data such as keys or passwords. diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte index b99631a3d..2f250d92e 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditPassword.svelte @@ -11,8 +11,8 @@ import { auth } from "../store/auth"; import Spinner from "./Spinner.svelte"; import { onDestroy } from "svelte"; - import { Principal } from "@dfinity/principal"; - import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps"; + import { Principal } from "@icp-sdk/core/principal"; + import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps"; export let currentRoute = ""; const unsubscribe = location.subscribe((value) => { @@ -71,7 +71,7 @@ v.owner.compareTo(parentVaultOwnerPrincipal) === "eq" && v.name === parentVaultName, ); - const me = $auth.client.getIdentity().getPrincipal(); + const me = $auth.principal; if ( parentVaultOwnerPrincipal.compareTo(me) !== "eq" && (!vault || @@ -151,10 +151,9 @@ message: "Password saved successfully", }); - await refreshVaults( - $auth.client.getIdentity().getPrincipal(), - $auth.passwordManager, - ).catch((e) => showError(e as Error, "Could not refresh passwords.")); + await refreshVaults($auth.principal, $auth.passwordManager).catch((e) => + showError(e as Error, "Could not refresh passwords."), + ); if (move) { void replace( @@ -184,10 +183,7 @@ showError(e as Error, "Could not delete password."); }); - await refreshVaults( - $auth.client.getIdentity().getPrincipal(), - $auth.passwordManager, - ) + await refreshVaults($auth.principal, $auth.passwordManager) .catch((e) => showError(e as Error, "Could not refresh passwords.")) .finally(() => { addNotification({ @@ -228,12 +224,11 @@ tagsInput = tags.join(", "); } - const myPrincipal = $auth.client.getIdentity().getPrincipal(); - + const myPrincipal = $auth.principal; if (parentVaultOwnerPrincipal.compareTo(myPrincipal) === "eq") { accessRights = { ReadWriteManage: null }; } else { - let foundAccessRights = targetVault.users.find( + const foundAccessRights = targetVault.users.find( (u) => u[0].compareTo(myPrincipal) === "eq", ); if (foundAccessRights) { diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte index 7a3695666..c671bb0fe 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/EditVault.svelte @@ -6,8 +6,15 @@ import Trash from "svelte-icons/fa/FaTrash.svelte"; import { auth } from "../store/auth"; import Spinner from "./Spinner.svelte"; + import { location } from "svelte-spa-router"; + import { Principal } from "@icp-sdk/core/principal"; + import { onDestroy } from "svelte"; - export let currentRoute = ""; + let currentRoute = ""; + const unsubscribeCurrentRoute = location.subscribe((value) => { + currentRoute = decodeURI(value); + }); + onDestroy(unsubscribeCurrentRoute); let editedVault: VaultModel; let updating = false; @@ -20,15 +27,21 @@ if ( $auth.state === "initialized" && $vaultsStore.state === "loaded" && - !editedVault + !editedVault && + currentRoute.split("/").length > 2 ) { + const split = currentRoute.split("/"); + const vaultOwner = Principal.fromText(split[split.length - 2]); + const vaultName = split[split.length - 1]; const vault = $vaultsStore.list.find( - (vault) => vault.name === currentRoute, + (v) => + v.owner.compareTo(vaultOwner) === "eq" && + v.name === vaultName, ); if (vault) { editedVault = { ...vault }; - const me = $auth.client.getIdentity().getPrincipal(); + const me = $auth.principal; canManage = vault.owner.compareTo(me) === "eq" || "ReadWriteManage" in diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte index 998df9cf7..4dc1a099e 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/NewPassword.svelte @@ -7,12 +7,12 @@ import { addNotification, showError } from "../store/notifications"; import Header from "./Header.svelte"; import PasswordEditor from "./PasswordEditor.svelte"; - import { Principal } from "@dfinity/principal"; + import { Principal } from "@icp-sdk/core/principal"; let creating = false; let vaultOwner = $auth.state === "initialized" - ? $auth.client.getIdentity().getPrincipal().toText() + ? $auth.principal.toText() : Principal.anonymous().toText(); let vaultName = ""; let passwordName = ""; @@ -45,6 +45,14 @@ return; } + if (vaultName.trim() === "" || passwordName.trim() === "") { + addNotification({ + type: "error", + message: "Vault name and password name must not be empty.", + }); + return; + } + creating = true; await setPassword( @@ -72,10 +80,9 @@ }); // refresh passwords in the background - refreshVaults( - $auth.client.getIdentity().getPrincipal(), - $auth.passwordManager, - ).catch((e: Error) => showError(e, "Could not refresh passwords.")); + refreshVaults($auth.principal, $auth.passwordManager).catch( + (e: Error) => showError(e, "Could not refresh passwords."), + ); } function saveDraft() { @@ -131,7 +138,9 @@ diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte index 1631811d5..0bfe57331 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Password.svelte @@ -2,7 +2,7 @@ import { type PasswordModel, summarize } from "../lib/password"; import { link, location } from "svelte-spa-router"; import { vaultsStore } from "../store/vaults"; - import { Principal } from "@dfinity/principal"; + import { Principal } from "@icp-sdk/core/principal"; import { onDestroy } from "svelte"; import Spinner from "./Spinner.svelte"; import Header from "./Header.svelte"; diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte index 6e7f0b2a8..f3fe7b565 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SharingEditor.svelte @@ -8,8 +8,8 @@ vaultsStore, } from "../store/vaults"; import { addNotification, showError } from "../store/notifications"; - import { Principal } from "@dfinity/principal"; - import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps"; + import { Principal } from "@icp-sdk/core/principal"; + import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps"; export let editedVault: VaultModel; export let canManage = false; @@ -62,10 +62,9 @@ } finally { adding = false; } - await refreshVaults( - $auth.client.getIdentity().getPrincipal(), - $auth.passwordManager, - ).catch((e: Error) => showError(e, "Could not refresh vaults.")); + await refreshVaults($auth.principal, $auth.passwordManager).catch( + (e: Error) => showError(e, "Could not refresh vaults."), + ); } async function remove(sharing: Principal) { @@ -92,10 +91,9 @@ } finally { removing = false; } - await refreshVaults( - $auth.client.getIdentity().getPrincipal(), - $auth.passwordManager, - ).catch((e: Error) => showError(e, "Could not refresh vaults.")); + await refreshVaults($auth.principal, $auth.passwordManager).catch( + (e: Error) => showError(e, "Could not refresh vaults."), + ); } function onKeyPress(e: KeyboardEvent) { diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte index 439db77a7..94a1e1fe5 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/SidebarLayout.svelte @@ -7,7 +7,7 @@ // @ts-expect-error: svelte-icons have some problems with ts declarations import FaDoorOpen from "svelte-icons/fa/FaDoorOpen.svelte"; import Disclaimer from "./Disclaimer.svelte"; - import { Principal } from "@dfinity/principal"; + import { Principal } from "@icp-sdk/core/principal"; import { link } from "svelte-spa-router"; @@ -34,7 +34,7 @@
My Principal:
{$auth.state === "initialized" - ? $auth.client.getIdentity().getPrincipal().toText() + ? $auth.principal.toText() : Principal.anonymous().toText()}
diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte index 4ce52a0d4..378351761 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/components/Vault.svelte @@ -3,14 +3,14 @@ import { link, location } from "svelte-spa-router"; import { onDestroy } from "svelte"; import { vaultsStore } from "../store/vaults"; - import { Principal } from "@dfinity/principal"; + import { Principal } from "@icp-sdk/core/principal"; import Header from "./Header.svelte"; import Spinner from "./Spinner.svelte"; // @ts-expect-error: svelte-icons have some problems with ts declarations import GiOpenTreasureChest from "svelte-icons/gi/GiOpenTreasureChest.svelte"; import { auth } from "../store/auth"; import SharingEditor from "./SharingEditor.svelte"; - import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps"; + import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps"; export let vault: VaultModel = { name: "", @@ -27,42 +27,46 @@ }); onDestroy(unsubscribeCurrentRoute); - $: { - if ( - $vaultsStore.state === "loaded" && - $auth.state === "initialized" && - vault.name.length === 0 && - currentRoute.split("/").length > 2 - ) { - const split = currentRoute.split("/"); - const vaultName = split[split.length - 1]; - const vaultOwner = Principal.fromText(split[split.length - 2]); - const searchedForVault = $vaultsStore.list.find( - (v) => - v.owner.compareTo(vaultOwner) === "eq" && - v.name === vaultName, - ); - if (!searchedForVault) { - vaultSummary = - "could not find vault " + - vaultName + - " owned by " + - vaultOwner.toText(); - } else { - vault = searchedForVault; - vaultSummary += summarize(vault); - const me = $auth.client.getIdentity().getPrincipal(); + // Parse owner and vault name from the URL once; stored separately so the + // vault lookup below stays reactive to store updates (e.g. from the poller). + let parsedRoute: { owner: Principal; vaultName: string } | null = null; + $: if (currentRoute.split("/").length > 2 && parsedRoute === null) { + const split = currentRoute.split("/"); + parsedRoute = { + owner: Principal.fromText(split[split.length - 2]), + vaultName: split[split.length - 1], // already decoded via decodeURI on subscribe + }; + } - if (vault.owner.compareTo(me) === "eq") { - accessRights = { ReadWriteManage: null }; - } else { - const foundRights = vault.users.find( - (user) => user[0].compareTo(me) === "eq", - ); - accessRights = foundRights - ? foundRights[1] - : { Read: null }; - } + // Re-runs whenever the store updates so new passwords from the poller appear. + $: if ( + $vaultsStore.state === "loaded" && + $auth.state === "initialized" && + parsedRoute !== null + ) { + const { owner: targetOwner, vaultName: targetVaultName } = parsedRoute; + const searchedForVault = $vaultsStore.list.find( + (v) => + v.owner.compareTo(targetOwner) === "eq" && + v.name === targetVaultName, + ); + if (!searchedForVault) { + vaultSummary = + "could not find vault " + + targetVaultName + + " owned by " + + targetOwner.toText(); + } else { + vault = searchedForVault; + vaultSummary = summarize(vault); + const me = $auth.principal; + if (vault.owner.compareTo(me) === "eq") { + accessRights = { ReadWriteManage: null }; + } else { + const foundRights = vault.users.find( + (user) => user[0].compareTo(me) === "eq", + ); + accessRights = foundRights ? foundRights[1] : { Read: null }; } } } 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 543f05f10..e464c683c 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,31 +1,22 @@ -import "./init.ts"; -import { HttpAgent, type HttpAgentOptions } from "@dfinity/agent"; +import { HttpAgent } from "@icp-sdk/core/agent"; import { DefaultEncryptedMapsClient, EncryptedMaps, -} from "@dfinity/vetkeys/encrypted_maps"; +} from "@icp-sdk/vetkeys/encrypted_maps"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -export async function createEncryptedMaps( - agentOptions: HttpAgentOptions, -): Promise { - const agent = await HttpAgent.create({ ...agentOptions }); - // Fetch root key for certificate validation during development - if (process.env.NODE_ENV !== "production") { - console.log(`Dev environment - fetching root key...`); +const canisterEnv = safeGetCanisterEnv<{ + "PUBLIC_CANISTER_ID:password_manager_with_metadata": string; +}>(); - agent.fetchRootKey().catch((err) => { - console.warn( - "Unable to fetch root key. Check to ensure that your local replica is running", - ); - console.error(err); - }); +export function createEncryptedMaps(agent: HttpAgent): EncryptedMaps { + const canisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:password_manager_with_metadata"]; + if (!canisterId) { + throw new Error( + "Canister ID for password_manager_with_metadata is not set", + ); } - // Creates an actor with using the candid interface and the HttpAgent - return new EncryptedMaps( - new DefaultEncryptedMapsClient( - agent, - process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA as string, - ), - ); + return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId)); } diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts index 062c8af94..e69de29bb 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/init.ts @@ -1 +0,0 @@ -window.global ||= window; diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts index 88620bd13..ff7d78287 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/password.ts @@ -1,5 +1,5 @@ -import type { Principal } from "@dfinity/principal"; -import type { PasswordMetadata } from "../declarations/password_manager_with_metadata/password_manager_with_metadata.did"; +import type { Principal } from "@icp-sdk/core/principal"; +import type { PasswordMetadata } from "../declarations/password_manager_with_metadata/backend.did"; export interface PasswordModel { owner: Principal; 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 232f1873a..fe216a638 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,13 +1,19 @@ -import "./init.ts"; -import { type ActorSubclass, type HttpAgentOptions } from "@dfinity/agent"; -import { EncryptedMaps } from "@dfinity/vetkeys/encrypted_maps"; +import { Actor, HttpAgent, type ActorSubclass } 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"; +import { + idlFactory, + type _SERVICE, +} from "../declarations/password_manager_with_metadata/backend.did"; import { createEncryptedMaps } from "./encrypted_maps"; -import type { Principal } from "@dfinity/principal"; -import { createActor } from "../declarations/password_manager_with_metadata"; -import type { _SERVICE } from "../declarations/password_manager_with_metadata/password_manager_with_metadata.did"; import { passwordFromContent, type PasswordModel } from "../lib/password"; import { vaultFromContent, type VaultModel } from "../lib/vault"; +const canisterEnv = safeGetCanisterEnv<{ + "PUBLIC_CANISTER_ID:password_manager_with_metadata": string; +}>(); + export class PasswordManager { /// The actor class representing the full interface of the canister. private readonly canisterClient: ActorSubclass<_SERVICE>; @@ -145,35 +151,28 @@ export class PasswordManager { } } -export async function createPasswordManager( - agentOptions?: HttpAgentOptions, -): Promise { - if (!process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA) { - console.error( - "CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA is not defined", - ); +export async function createPasswordManager(agentOptions?: { + identity?: HttpAgent["config"]["identity"]; +}): Promise { + const canisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:password_manager_with_metadata"]; + if (!canisterId) { throw new Error( - "CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA is not defined", + "Canister ID for password_manager_with_metadata is not defined", ); } - const host = - process.env.DFX_NETWORK === "ic" - ? `https://${process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA}.ic0.app` - : "http://localhost:8000"; - const hostOptions = { host }; - - if (!agentOptions) { - agentOptions = hostOptions; - } else { - agentOptions.host = hostOptions.host; - } - - const encryptedMaps = await createEncryptedMaps({ ...agentOptions }); - const canisterClient = createActor( - process.env.CANISTER_ID_PASSWORD_MANAGER_WITH_METADATA, - { agentOptions }, - ); + const agent = await HttpAgent.create({ + ...agentOptions, + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }); + + const encryptedMaps = createEncryptedMaps(agent); + const canisterClient = Actor.createActor<_SERVICE>(idlFactory, { + agent, + canisterId, + }); return new PasswordManager(canisterClient, encryptedMaps); } diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts index 717fc4767..49d302a8d 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/lib/vault.ts @@ -1,6 +1,6 @@ -import type { Principal } from "@dfinity/principal"; +import type { Principal } from "@icp-sdk/core/principal"; import type { PasswordModel } from "./password"; -import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps"; +import type { AccessRights } from "@icp-sdk/vetkeys/encrypted_maps"; export interface VaultModel { owner: Principal; 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 aa05921c4..1f54fb574 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 @@ -1,7 +1,7 @@ -import "../lib/init.ts"; import { get, writable } from "svelte/store"; -import { AuthClient } from "@dfinity/auth-client"; -import type { JsonnableDelegationChain } from "@dfinity/identity/lib/cjs/identity/delegation"; +import { AuthClient, LocalStorage } from "@icp-sdk/auth/client"; +import { DelegationIdentity } from "@icp-sdk/core/identity"; +import type { Principal } from "@icp-sdk/core/principal"; import { replace } from "svelte-spa-router"; import { PasswordManager, @@ -20,6 +20,7 @@ export type AuthState = state: "initialized"; passwordManager: PasswordManager; client: AuthClient; + principal: Principal; } | { state: "error"; @@ -31,8 +32,22 @@ export const auth = writable({ }); async function initAuth() { - const client = await AuthClient.create(); - if (await client.isAuthenticated()) { + const isLocalEnv = + window.location.hostname === "localhost" || + window.location.hostname.endsWith(".localhost"); + // Workaround for https://github.com/dfinity/icp-js-auth/issues/120 + // IdbStorage has a race condition on localhost dev servers. LocalStorage + // avoids IDB on local but uses plain string storage (less secure), so + // production deployments keep the default secure IdbStorage + ECDSA key. + const client = new AuthClient({ + identityProvider: isLocalEnv + ? "http://id.ai.localhost:8000/authorize" + : "https://id.ai/authorize", + ...(isLocalEnv + ? { storage: new LocalStorage(), keyType: "Ed25519" as const } + : {}), + }); + if (client.isAuthenticated()) { await authenticate(client); } else { auth.update(() => ({ @@ -44,24 +59,20 @@ async function initAuth() { void initAuth(); -export async function login() { +export function login() { const currentAuth = get(auth); if (currentAuth.state === "anonymous") { - await currentAuth.client.login({ - maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000), - identityProvider: - process.env.DFX_NETWORK === "ic" - ? "https://identity.ic0.app/#authorize" - : `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000/#authorize`, - onSuccess: async () => { - await authenticate(currentAuth.client); - }, - onError: (e) => - console.error( - "Failed to authenticate with internet identity: " + e, - ), - }); + void (async () => { + try { + await currentAuth.client.signIn({ + maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000), + }); + void authenticate(currentAuth.client); + } catch (error: unknown) { + console.error("Login failed:", error); + } + })(); } } @@ -69,7 +80,7 @@ export async function logout() { const currentAuth = get(auth); if (currentAuth.state === "initialized") { - await currentAuth.client.logout(); + await currentAuth.client.signOut(); auth.update(() => ({ state: "anonymous", client: currentAuth.client, @@ -79,17 +90,17 @@ export async function logout() { } export async function authenticate(client: AuthClient) { - handleSessionTimeout(); + void handleSessionTimeout(client); try { - const passwordManager = await createPasswordManager({ - identity: client.getIdentity(), - }); + const identity = await client.getIdentity(); + const passwordManager = await createPasswordManager({ identity }); auth.update(() => ({ state: "initialized", passwordManager, client, + principal: identity.getPrincipal(), })); } catch (e) { auth.update(() => ({ @@ -100,31 +111,20 @@ export async function authenticate(client: AuthClient) { } // set a timer when the II session will expire and log the user out -function handleSessionTimeout() { - // upon login the localstorage items may not be set, wait for next tick - setTimeout(() => { - try { - const rawDelegation = window.localStorage.getItem("ic-delegation"); - if (!rawDelegation) { - throw new Error("No delegation found"); - } - const delegation = JSON.parse( - rawDelegation, - ) as JsonnableDelegationChain; +async function handleSessionTimeout(client: AuthClient) { + try { + const identity = await client.getIdentity(); + if (!(identity instanceof DelegationIdentity)) return; - const expirationTimeMs = - Number.parseInt( - delegation.delegations[0].delegation.expiration, - 16, - ) / 1000000; + const chain = identity.getDelegation(); + // expiration is a BigInt of nanoseconds since epoch + const expirationMs = + Number(chain.delegations[0].delegation.expiration) / 1_000_000; - setTimeout(() => { - void logout(); - }, expirationTimeMs - Date.now()); - } catch (e) { - console.error( - "Could not handle delegation expiry: " + (e as Error).message, - ); - } - }); + setTimeout(() => { + void logout(); + }, expirationMs - Date.now()); + } catch { + console.error("Could not handle delegation expiry."); + } } diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts index a236a9559..f56f5dcb8 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts +++ b/rust/vetkeys/password_manager_with_metadata/frontend/src/store/vaults.ts @@ -3,8 +3,8 @@ import { type PasswordModel } from "../lib/password"; import { type VaultModel } from "../lib/vault"; import { auth } from "./auth"; import { showError } from "./notifications"; -import { type AccessRights } from "@dfinity/vetkeys/encrypted_maps"; -import type { Principal } from "@dfinity/principal"; +import { type AccessRights } from "@icp-sdk/vetkeys/encrypted_maps"; +import type { Principal } from "@icp-sdk/core/principal"; import type { PasswordManager } from "../lib/password_manager"; export const vaultsStore = writable< @@ -119,14 +119,13 @@ auth.subscribe((auth) => { void (async () => { try { - await refreshVaults( - auth.client.getIdentity().getPrincipal(), - auth.passwordManager, - ).catch((e) => showError(e as Error, "Could not poll vaults.")); + await refreshVaults(auth.principal, auth.passwordManager).catch( + (e) => showError(e as Error, "Could not poll vaults."), + ); vaultPollerHandle = setInterval(() => { void refreshVaults( - auth.client.getIdentity().getPrincipal(), + auth.principal, auth.passwordManager, ).catch((e) => showError(e as Error, "Could not poll vaults."), diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.cjs b/rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.mjs similarity index 100% rename from rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.cjs rename to rust/vetkeys/password_manager_with_metadata/frontend/tailwind.config.mjs diff --git a/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js b/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js index d8ee7a41a..ca53dc054 100644 --- a/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js +++ b/rust/vetkeys/password_manager_with_metadata/frontend/vite.config.js @@ -4,23 +4,53 @@ import eslint from "vite-plugin-eslint"; import tailwindcss from "tailwindcss"; import autoprefixer from "autoprefixer"; import css from "rollup-plugin-css-only"; -import typescript from "@rollup/plugin-typescript"; import viteCompression from "vite-plugin-compression"; -import environment from "vite-plugin-environment"; -import path from "path"; +import { execSync } from "child_process"; + +const environment = process.env.ICP_ENVIRONMENT || "local"; +const CANISTER_NAMES = ["password_manager_with_metadata"]; + +function getDevServerConfig() { + const backend = process.env.BACKEND; + if (!backend) { + throw new Error( + "BACKEND env var is required. Use `npm run dev:motoko` or `npm run dev:rust`.", + ); + } + + const networkStatus = JSON.parse( + execSync( + `icp network status -e ${environment} --json --project-root-override ../${backend}`, + { encoding: "utf-8" }, + ), + ); + const canisterParams = CANISTER_NAMES.map((name) => { + const id = execSync( + `icp canister status ${name} -e ${environment} --id-only --project-root-override ../${backend}`, + { encoding: "utf-8", stdio: "pipe" }, + ).trim(); + return `PUBLIC_CANISTER_ID:${name}=${id}`; + }).join("&"); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `${canisterParams}&ic_root_key=${networkStatus.root_key}`, + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: networkStatus.api_url, changeOrigin: true }, + }, + hmr: false, + }; +} // https://vite.dev/config/ -export default defineConfig({ +export default defineConfig(({ command }) => ({ plugins: [ svelte(), css({ output: "bundle.css" }), eslint(), - typescript({ - inlineSources: true, - }), viteCompression(), - environment("all", { prefix: "CANISTER_" }), - environment("all", { prefix: "DFX_" }), ], css: { postcss: { @@ -32,31 +62,14 @@ export default defineConfig({ output: { inlineDynamicImports: true, }, - sourcemap: true, - }, - build: { - rollupOptions: { - output: { - inlineDynamicImports: true, - }, - sourcemap: true, - }, - }, - root: "./", - server: { - hmr: false, }, + sourcemap: true, }, + root: "./", resolve: { alias: { - ic_vetkeys: path.resolve( - __dirname, - "../../../frontend/ic_vetkeys/src", - ), - "ic_vetkeys/encrypted_maps": path.resolve( - __dirname, - "../../../frontend/ic_vetkeys/src/encrypted_maps", - ), + "@dfinity/vetkeys": "@icp-sdk/vetkeys", }, }, -}); + ...(command === "serve" ? { server: getDevServerConfig() } : {}), +})); diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo b/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo index 6cfa11b37..b13b3f6a0 100644 --- a/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo +++ b/rust/vetkeys/password_manager_with_metadata/motoko/backend/src/Main.mo @@ -1,14 +1,15 @@ -import Principal "mo:base/Principal"; -import Blob "mo:base/Blob"; -import Buffer "mo:base/Buffer"; -import Array "mo:base/Array"; -import OrderedMap "mo:base/OrderedMap"; -import MotokoResult "mo:base/Result"; -import Text "mo:base/Text"; -import Time "mo:base/Time"; -import Nat64 "mo:base/Nat64"; -import Int "mo:base/Int"; -import Debug "mo:base/Debug"; +import Principal "mo:core/Principal"; +import Blob "mo:core/Blob"; +import List "mo:core/List"; +import Array "mo:core/Array"; +import OrderedMap "mo:core/pure/Map"; +import MotokoResult "mo:core/Result"; +import Text "mo:core/Text"; +import Time "mo:core/Time"; +import Nat64 "mo:core/Nat64"; +import Int "mo:core/Int"; +import Debug "mo:core/Debug"; +import Runtime "mo:core/Runtime"; import VetKeys "mo:ic-vetkeys"; persistent actor class (keyName : Text) { @@ -34,8 +35,7 @@ persistent actor class (keyName : Text) { ownerCompare; }; }; - transient let metadataMapOps = OrderedMap.Make(compareMetadataKeys); - var metadata : OrderedMap.Map = metadataMapOps.empty(); + var metadata : OrderedMap.Map = OrderedMap.empty(); // Types public type PasswordMetadata = { @@ -116,21 +116,21 @@ persistent actor class (keyName : Text) { switch (encryptedMaps.getEncryptedValuesForMap(caller, mapId)) { case (#err(msg)) { #Err(msg) }; case (#ok(mapValues)) { - let results = Buffer.Buffer<(ByteBuf, ByteBuf, PasswordMetadata)>(0); + let results = List.empty<(ByteBuf, ByteBuf, PasswordMetadata)>(); - for ((key, encryptedValue) in mapValues.vals()) { + for ((key, encryptedValue) in mapValues.values()) { let metadataKey = (map_owner, map_name.inner, key); - switch (metadataMapOps.get(metadata, metadataKey)) { + switch (OrderedMap.get(metadata, compareMetadataKeys,metadataKey)) { case (null) { - Debug.trap("bug: inconsistent state: no metadata for key"); + Runtime.trap("bug: inconsistent state: no metadata for key"); }; case (?metadataValue) { - results.add(({ inner = key }, { inner = encryptedValue }, metadataValue)); + List.add(results, ({ inner = key }, { inner = encryptedValue }, metadataValue)); }; }; }; - #Ok(Buffer.toArray(results)); + #Ok(List.toArray(results)); }; }; }; @@ -156,7 +156,7 @@ persistent actor class (keyName : Text) { case (#err(msg)) { #Err(msg) }; case (#ok(optPrevValue)) { let metadataKey = (map_owner, map_name.inner, map_key.inner); - let prevMetadata = metadataMapOps.get(metadata, metadataKey); + let prevMetadata = OrderedMap.get(metadata, compareMetadataKeys,metadataKey); let metadataValue = switch (prevMetadata) { case (null) { @@ -167,15 +167,15 @@ persistent actor class (keyName : Text) { }; }; - metadata := metadataMapOps.put(metadata, metadataKey, metadataValue); + metadata := OrderedMap.add(metadata, compareMetadataKeys,metadataKey, metadataValue); switch (optPrevValue, prevMetadata) { case (null, null) { #Ok(null) }; case (null, ?_) { - Debug.trap("bug: inconsistent state: no previous value but some metadata"); + Runtime.trap("bug: inconsistent state: no previous value but some metadata"); }; case (?_, null) { - Debug.trap("bug: inconsistent state: some previous value but no metadata"); + Runtime.trap("bug: inconsistent state: some previous value but no metadata"); }; case (?prevValue, ?m) { #Ok(?({ inner = prevValue }, m)) }; }; @@ -194,17 +194,17 @@ persistent actor class (keyName : Text) { case (#err(msg)) { #Err(msg) }; case (#ok(optPrevValue)) { let metadataKey = (map_owner, map_name.inner, map_key.inner); - let prevMetadata = metadataMapOps.get(metadata, metadataKey); + let prevMetadata = OrderedMap.get(metadata, compareMetadataKeys,metadataKey); - metadata := metadataMapOps.delete(metadata, metadataKey); + metadata := OrderedMap.remove(metadata, compareMetadataKeys,metadataKey); switch (optPrevValue, prevMetadata) { case (null, null) { #Ok(null) }; case (null, ?_) { - Debug.trap("bug: inconsistent state: no previous value but some metadata"); + Runtime.trap("bug: inconsistent state: no previous value but some metadata"); }; case (?_, null) { - Debug.trap("bug: inconsistent state: some previous value but no metadata"); + Runtime.trap("bug: inconsistent state: some previous value but no metadata"); }; case (?prevValue, ?m) { #Ok(?({ inner = prevValue }, m)) }; }; diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/dfx.json b/rust/vetkeys/password_manager_with_metadata/motoko/dfx.json deleted file mode 100644 index 1cf398f72..000000000 --- a/rust/vetkeys/password_manager_with_metadata/motoko/dfx.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "canisters": { - "password_manager_with_metadata": { - "main": "backend/src/Main.mo", - "args": "--enhanced-orthogonal-persistence", - "type": "motoko", - "init_arg": "(\"test_key_1\")", - "metadata": [ - { - "name": "candid:service", - "visibility": "public" - } - ] - }, - "internet-identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did", - "type": "custom", - "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz" - }, - "www": { - "dependencies": ["password_manager_with_metadata", "internet-identity"], - "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"], - "frontend": { - "entrypoint": "dist/index.html" - }, - "source": ["dist/"], - "type": "assets", - "output_env_file": "frontend/.env" - } - }, - "defaults": { - "build": { - "packtool": "npx ic-mops sources", - "args": "" - } - }, - "networks": { - "local": { - "bind": "127.0.0.1:8000", - "type": "ephemeral" - } - } - } - \ No newline at end of file diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/icp.yaml b/rust/vetkeys/password_manager_with_metadata/motoko/icp.yaml new file mode 100644 index 000000000..c19cfbed6 --- /dev/null +++ b/rust/vetkeys/password_manager_with_metadata/motoko/icp.yaml @@ -0,0 +1,22 @@ +canisters: + - name: password_manager_with_metadata + recipe: + type: "@dfinity/motoko@v4.1.0" + configuration: + main: backend/src/Main.mo + init_args: + type: text + value: "(\"test_key_1\")" + + - name: www + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: dist + build: + - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./ + +networks: + - name: local + mode: managed + ii: true diff --git a/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml b/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml index f8b191454..670c65a28 100644 --- a/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml +++ b/rust/vetkeys/password_manager_with_metadata/motoko/mops.toml @@ -1,3 +1,6 @@ +[toolchain] +moc = "1.9.0" + [dependencies] -base = "0.14.9" -ic-vetkeys = "0.4.0" \ No newline at end of file +core = "2.5.0" +ic-vetkeys = "0.5.0" \ No newline at end of file diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml b/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml index 097e3b72d..964961938 100644 --- a/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml +++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/Cargo.toml @@ -14,9 +14,10 @@ crate-type = ["cdylib"] [dependencies] candid = "0.10.2" -ic-cdk = "0.18.3" +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.2.0" +ic-stable-structures = "0.7.2" +ic-vetkeys = "0.7.0" serde = "1.0.217" serde_cbor = "0.11.2" diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile b/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile index c6860e235..fadd94355 100644 --- a/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile +++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/Makefile @@ -12,4 +12,4 @@ extract-candid: compile-wasm .SILENT: clean clean: cargo clean - rm -rf ../.dfx \ No newline at end of file + rm -rf ../.icp \ No newline at end of file diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did b/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did index 459b6c585..1b4e5f994 100644 --- a/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did +++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/backend.did @@ -1,4 +1,15 @@ -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 }; type PasswordMetadata = record { url : text; diff --git a/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs b/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs index eec198ea0..334d0509b 100644 --- a/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs +++ b/rust/vetkeys/password_manager_with_metadata/rust/backend/src/lib.rs @@ -1,10 +1,10 @@ use candid::{CandidType, Principal}; -use ic_cdk::management_canister::{VetKDCurve, VetKDKeyId}; -use ic_cdk::{init, query, update}; +use ic_cdk_management_canister::{VetKDCurve, VetKDKeyId}; +use ic_cdk::{init, post_upgrade, query, update}; use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; use ic_stable_structures::storable::Blob; use ic_stable_structures::{storable::Bound, Storable}; -use ic_stable_structures::{BTreeMap as StableBTreeMap, DefaultMemoryImpl}; +use ic_stable_structures::{BTreeMap as StableBTreeMap, Cell as StableCell, DefaultMemoryImpl}; use ic_vetkeys::encrypted_maps::{EncryptedMaps, VetKey, VetKeyVerificationKey}; use ic_vetkeys::types::{AccessRights, ByteBuf, EncryptedMapValue, TransportKey}; use serde::{Deserialize, Serialize}; @@ -48,6 +48,10 @@ impl PasswordMetadata { } impl Storable for PasswordMetadata { + fn into_bytes(self) -> Vec { + self.to_bytes().into_owned() + } + fn to_bytes(&self) -> Cow<[u8]> { Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) } @@ -75,17 +79,33 @@ thread_local! { static METADATA: RefCell = RefCell::new(StableBTreeMap::new( MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(4))), )); + static KEY_NAME: RefCell> = + RefCell::new(StableCell::init( + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(5))), + 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( - "password_manager_dapp", + "password_manager_app", key_id, MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))), @@ -143,8 +163,11 @@ fn get_encrypted_values_for_map_with_metadata( METADATA.with_borrow(|metadata| { let iter_metadata = metadata .range((map_owner, map_name, Blob::default())..) - .take_while(|((owner, name, _), _)| owner == &map_owner && name == &map_name) - .map(|((_, _, key), metadata)| (key, metadata)); + .take_while(|entry| { + let (owner, name, _) = entry.key(); + owner == &map_owner && name == &map_name + }) + .map(|entry| (entry.key().2, entry.value())); iter_metadata .zip(map_values) diff --git a/rust/vetkeys/password_manager_with_metadata/rust/dfx.json b/rust/vetkeys/password_manager_with_metadata/rust/dfx.json deleted file mode 100644 index b361f8c27..000000000 --- a/rust/vetkeys/password_manager_with_metadata/rust/dfx.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "canisters": { - "password_manager_with_metadata": { - "candid": "backend/backend.did", - "package": "ic-vetkd-example-password-manager-with-metadata-backend", - "type": "rust", - "init_arg": "(\"test_key_1\")", - "metadata": [ - { - "name": "candid:service", - "visibility": "public" - } - ] - }, - "internet-identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did", - "type": "custom", - "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz" - }, - "www": { - "dependencies": ["password_manager_with_metadata", "internet-identity"], - "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"], - "frontend": { - "entrypoint": "dist/index.html" - }, - "source": ["dist/"], - "type": "assets", - "output_env_file": "frontend/.env" - } - }, - "networks": { - "local": { - "bind": "127.0.0.1:8000", - "type": "ephemeral" - } - } -} diff --git a/rust/vetkeys/password_manager_with_metadata/rust/icp.yaml b/rust/vetkeys/password_manager_with_metadata/rust/icp.yaml new file mode 100644 index 000000000..9d753e1a4 --- /dev/null +++ b/rust/vetkeys/password_manager_with_metadata/rust/icp.yaml @@ -0,0 +1,23 @@ +canisters: + - name: password_manager_with_metadata + recipe: + type: "@dfinity/rust@v3.2.0" + configuration: + package: ic-vetkd-example-password-manager-with-metadata-backend + candid: backend/backend.did + init_args: + type: text + value: "(\"test_key_1\")" + + - name: www + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: dist + build: + - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./ + +networks: + - name: local + mode: managed + ii: true diff --git a/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml b/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml index 2a2058b04..6e5beca4f 100644 --- a/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml +++ b/rust/vetkeys/password_manager_with_metadata/rust/rust-toolchain.toml @@ -1,4 +1,2 @@ [toolchain] -channel = "1.88.0" -targets = ["wasm32-unknown-unknown"] -profile = "default" \ No newline at end of file +targets = ["wasm32-unknown-unknown"] \ No newline at end of file