diff --git a/.github/workflows/vetkeys-password-manager.yml b/.github/workflows/vetkeys-password-manager.yml new file mode 100644 index 000000000..791677e07 --- /dev/null +++ b/.github/workflows/vetkeys-password-manager.yml @@ -0,0 +1,36 @@ +name: vetkeys-password-manager + +on: + push: + branches: + - master + pull_request: + paths: + - rust/vetkeys/password_manager/** + - .github/workflows/vetkeys-password-manager.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 Rust + working-directory: rust/vetkeys/password_manager/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 Motoko + working-directory: rust/vetkeys/password_manager/motoko + run: icp network start -d && icp deploy diff --git a/rust/vetkeys/password_manager/README.md b/rust/vetkeys/password_manager/README.md index d44ee8663..0b2478c9f 100644 --- a/rust/vetkeys/password_manager/README.md +++ b/rust/vetkeys/password_manager/README.md @@ -1,8 +1,10 @@ # VetKey Password Manager + 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**. @@ -16,38 +18,61 @@ The **VetKey Password Manager** is an example application demonstrating how to u ### 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/ +├── 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 +cd motoko +icp network start -d && icp deploy +``` + +Or deploy with the **Rust** backend: ```bash -dfx start --background && dfx deploy +cd rust +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. +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 +``` -## Running the Project +## 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/frontend/package.json b/rust/vetkeys/password_manager/frontend/package.json index 2c2bd3812..a88da97ae 100644 --- a/rust/vetkeys/password_manager/frontend/package.json +++ b/rust/vetkeys/password_manager/frontend/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "type": "module", "scripts": { - "dev": "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": "BACKEND=motoko vite", + "dev:rust": "BACKEND=rust vite", "build": "vite build", "lint": "eslint", "prettier": "prettier --write .", @@ -23,16 +25,12 @@ "svelte": "^4.2.19", "tslib": "^2.8.1", "typescript-eslint": "^8.35.1", - "vite": "^5.4.21", - "vite-plugin-environment": "^1.1.3" + "vite": "^5.4.21" }, "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", "@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/components/DisclaimerCopy.svelte b/rust/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte index 336ffde7d..7cf597760 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte +++ b/rust/vetkeys/password_manager/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/frontend/src/components/EditPassword.svelte b/rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte index 73cc65f46..71d0523be 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte +++ b/rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte @@ -15,8 +15,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) => { @@ -64,7 +64,7 @@ v.owner.compareTo(editedPassword.owner) === "eq" && v.name === editedPassword.parentVaultName, ); - const me = $auth.client.getIdentity().getPrincipal(); + const me = $auth.principal; const accessRights = vault && vault.users.find((u) => u[0].compareTo(me) === "eq"); const authorized = accessRights && "Read" in accessRights[1]; @@ -185,8 +185,8 @@ ) { const split = currentRoute.split("/"); vaultOwner = split[split.length - 3]; - const parentVaultName = split[split.length - 2]; - const passwordName = split[split.length - 1]; + const parentVaultName = decodeURIComponent(split[split.length - 2]); + const passwordName = decodeURIComponent(split[split.length - 1]); const searchedForPassword = $vaultsStore.list .find( (v) => @@ -199,7 +199,7 @@ editedPassword = { ...searchedForPassword[1] }; } - const myPrincipal = $auth.client.getIdentity().getPrincipal(); + const myPrincipal = $auth.principal; if (editedPassword.owner.compareTo(myPrincipal) === "eq") { accessRights = { ReadWriteManage: null }; diff --git a/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte b/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte index 5c156fcc8..1b81df2a6 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte +++ b/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte @@ -7,8 +7,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 = value; + }); + onDestroy(unsubscribeCurrentRoute); let editedVault: VaultModel; let updating = false; @@ -21,15 +28,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 = decodeURIComponent(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; if (vault.owner.compareTo(me) === "eq") { canManage = true; } else { diff --git a/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte b/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte index 7270bc0bb..282411f82 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte +++ b/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte @@ -8,12 +8,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 = ""; @@ -30,6 +30,14 @@ return; } + if (vaultName.trim() === "" || passwordName.trim() === "") { + addNotification({ + type: "error", + message: "Vault name and password name must not be empty.", + }); + return; + } + creating = true; await addPassword( @@ -102,7 +110,9 @@ diff --git a/rust/vetkeys/password_manager/frontend/src/components/Password.svelte b/rust/vetkeys/password_manager/frontend/src/components/Password.svelte index 9b30c30a2..a1db1f011 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/Password.svelte +++ b/rust/vetkeys/password_manager/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"; @@ -30,15 +30,15 @@ ) { const split = currentRoute.split("/"); const vaultOwner = Principal.fromText(split[split.length - 3]); - const parentVaultName = split[split.length - 2]; - const passwordName = split[split.length - 1]; + const parentVaultName = decodeURIComponent(split[split.length - 2]); + const passwordName = decodeURIComponent(split[split.length - 1]); const searchedForPassword = $vaultsStore.list .find( (v) => v.owner.compareTo(vaultOwner) === "eq" && v.name === parentVaultName, ) - .passwords.find((p) => p[0] === passwordName); + ?.passwords.find((p) => p[0] === passwordName); if (searchedForPassword) { password = searchedForPassword[1]; diff --git a/rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte b/rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte index 53d6b644f..07038a035 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte +++ b/rust/vetkeys/password_manager/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; diff --git a/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte b/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte index 94959e666..159dc6564 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte +++ b/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte @@ -4,7 +4,7 @@ import GoDatabase from "svelte-icons/go/GoDatabase.svelte"; 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"; @@ -31,7 +31,7 @@
My Principal:
{$auth.state === "initialized" - ? $auth.client.getIdentity().getPrincipal().toText() + ? $auth.principal.toText() : Principal.anonymous().toText()}
diff --git a/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte b/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte index 5c578a5fa..f6ed54213 100644 --- a/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte +++ b/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte @@ -3,14 +3,13 @@ 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 +26,49 @@ }); onDestroy(unsubscribeCurrentRoute); - $: { - if ( - $vaultsStore.state === "loaded" && - $auth.state === "initialized" && - vault.name.length === 0 && - currentRoute.split("/").length > 2 - ) { - const split = currentRoute.split("/"); - const vaultOwner = Principal.fromText(split[split.length - 2]); - const vaultName = split[split.length - 1]; - const searchedForVault = $vaultsStore.list.find( - (v) => - v.owner.compareTo(vaultOwner) === "eq" && - v.name === vaultName, - ); - if (searchedForVault) { - vault = searchedForVault; - vaultSummary += summarize(vault); - const me = $auth.client.getIdentity().getPrincipal(); - if (vault.owner.compareTo(me) === "eq") { - accessRights = { ReadWriteManage: null }; - } else { - const foundAccessRights = vault.users.find( - (user) => user[0].compareTo(me) === "eq", - ); - if (foundAccessRights) { - accessRights = foundAccessRights[1]; - } - } + // 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: decodeURIComponent(split[split.length - 1]), + }; + } + + // 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) { + vault = searchedForVault; + vaultSummary = summarize(vault); + const me = $auth.principal; + if (vault.owner.compareTo(me) === "eq") { + accessRights = { ReadWriteManage: null }; } else { - vaultSummary = - "could not find vault " + - vaultName + - " owned by " + - vaultOwner.toText(); + const foundAccessRights = vault.users.find( + (user) => user[0].compareTo(me) === "eq", + ); + if (foundAccessRights) { + accessRights = foundAccessRights[1]; + } } + } else { + vaultSummary = + "could not find vault " + + targetVaultName + + " owned by " + + targetOwner.toText(); } } 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 2b2c7e071..ce034b20f 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts +++ b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts @@ -1,43 +1,31 @@ import "./init.ts"; -import { HttpAgent, type HttpAgentOptions } from "@dfinity/agent"; +import { HttpAgent, type HttpAgentOptions } 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"; + +const canisterEnv = safeGetCanisterEnv<{ + "PUBLIC_CANISTER_ID:ic_vetkeys_encrypted_maps_canister": string; +}>(); export async function createEncryptedMaps( agentOptions?: HttpAgentOptions, ): Promise { - const host = - process.env.DFX_NETWORK === "ic" - ? `https://${process.env.CANISTER_ID_IC_VETKEYS_ENCRYPTED_MAPS_CANISTER}.ic0.app` - : "http://localhost:4943"; - const hostOptions = { host }; - - if (!agentOptions) { - agentOptions = hostOptions; - } else { - agentOptions.host = hostOptions.host; + const canisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:ic_vetkeys_encrypted_maps_canister"]; + if (!canisterId) { + throw new Error( + "Canister ID for ic_vetkeys_encrypted_maps_canister is not set", + ); } - 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...`); - - agent.fetchRootKey().catch((err) => { - console.warn( - "Unable to fetch root key. Check to ensure that your local replica is running", - ); - console.error(err); - }); - } + const agent = await HttpAgent.create({ + ...agentOptions, + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }); - // Creates an actor with using the candid interface and the HttpAgent - return new EncryptedMaps( - new DefaultEncryptedMapsClient( - agent, - process.env.CANISTER_ID_IC_VETKEYS_ENCRYPTED_MAPS_CANISTER, - ), - ); + return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId)); } diff --git a/rust/vetkeys/password_manager/frontend/src/lib/init.ts b/rust/vetkeys/password_manager/frontend/src/lib/init.ts index 062c8af94..e69de29bb 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/init.ts +++ b/rust/vetkeys/password_manager/frontend/src/lib/init.ts @@ -1 +0,0 @@ -window.global ||= window; diff --git a/rust/vetkeys/password_manager/frontend/src/lib/password.ts b/rust/vetkeys/password_manager/frontend/src/lib/password.ts index e37c46204..1bd7c6ba1 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/password.ts +++ b/rust/vetkeys/password_manager/frontend/src/lib/password.ts @@ -1,4 +1,4 @@ -import type { Principal } from "@dfinity/principal"; +import type { Principal } from "@icp-sdk/core/principal"; export interface PasswordModel { owner: Principal; diff --git a/rust/vetkeys/password_manager/frontend/src/lib/vault.ts b/rust/vetkeys/password_manager/frontend/src/lib/vault.ts index 02dc5b712..c67041207 100644 --- a/rust/vetkeys/password_manager/frontend/src/lib/vault.ts +++ b/rust/vetkeys/password_manager/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/frontend/src/store/auth.ts b/rust/vetkeys/password_manager/frontend/src/store/auth.ts index ec6547c8b..54a75e50a 100644 --- a/rust/vetkeys/password_manager/frontend/src/store/auth.ts +++ b/rust/vetkeys/password_manager/frontend/src/store/auth.ts @@ -1,10 +1,11 @@ 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 { createEncryptedMaps } from "../lib/encrypted_maps.js"; -import { EncryptedMaps } from "@dfinity/vetkeys/encrypted_maps"; +import { EncryptedMaps } from "@icp-sdk/vetkeys/encrypted_maps"; export type AuthState = | { @@ -18,6 +19,7 @@ export type AuthState = state: "initialized"; encryptedMaps: EncryptedMaps; client: AuthClient; + principal: Principal; } | { state: "error"; @@ -29,8 +31,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()) { void authenticate(client); } else { auth.update(() => ({ @@ -46,14 +62,16 @@ export function login() { const currentAuth = get(auth); if (currentAuth.state === "anonymous") { - void 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:4943/#authorize`, - onSuccess: () => authenticate(currentAuth.client), - }); + 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); + } + })(); } } @@ -61,7 +79,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, @@ -71,17 +89,17 @@ export async function logout() { } export async function authenticate(client: AuthClient) { - handleSessionTimeout(); + void handleSessionTimeout(client); try { - const encryptedMaps = await createEncryptedMaps({ - identity: client.getIdentity(), - }); + const identity = await client.getIdentity(); + const encryptedMaps = await createEncryptedMaps({ identity }); auth.update(() => ({ state: "initialized", encryptedMaps, client, + principal: identity.getPrincipal(), })); } catch (e) { auth.update(() => ({ @@ -92,25 +110,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 delegation = JSON.parse( - window.localStorage.getItem("ic-delegation") || "{}", - ) 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 { - console.error("Could not handle delegation expiry."); - } - }); + setTimeout(() => { + void logout(); + }, expirationMs - Date.now()); + } catch { + console.error("Could not handle delegation expiry."); + } } diff --git a/rust/vetkeys/password_manager/frontend/src/store/vaults.ts b/rust/vetkeys/password_manager/frontend/src/store/vaults.ts index 0abfadf65..288d172b7 100644 --- a/rust/vetkeys/password_manager/frontend/src/store/vaults.ts +++ b/rust/vetkeys/password_manager/frontend/src/store/vaults.ts @@ -6,8 +6,8 @@ import { showError } from "./notifications"; import { type AccessRights, EncryptedMaps, -} from "@dfinity/vetkeys/encrypted_maps"; -import type { Principal } from "@dfinity/principal"; +} from "@icp-sdk/vetkeys/encrypted_maps"; +import type { Principal } from "@icp-sdk/core/principal"; export const vaultsStore = writable< | { @@ -148,7 +148,7 @@ auth.subscribe((auth) => { (e: Error) => showError(e, "Could not poll vaults."), ); - }); + })(); }, 3000); } catch { vaultsStore.set({ diff --git a/rust/vetkeys/password_manager/frontend/tailwind.config.cjs b/rust/vetkeys/password_manager/frontend/tailwind.config.mjs similarity index 100% rename from rust/vetkeys/password_manager/frontend/tailwind.config.cjs rename to rust/vetkeys/password_manager/frontend/tailwind.config.mjs diff --git a/rust/vetkeys/password_manager/frontend/vite.config.ts b/rust/vetkeys/password_manager/frontend/vite.config.ts index 811003329..0146433f6 100644 --- a/rust/vetkeys/password_manager/frontend/vite.config.ts +++ b/rust/vetkeys/password_manager/frontend/vite.config.ts @@ -1,44 +1,61 @@ -import { defineConfig } from 'vite' -import { svelte } from '@sveltejs/vite-plugin-svelte' -import tailwindcss from 'tailwindcss' +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import tailwindcss from "tailwindcss"; import autoprefixer from "autoprefixer"; -import css from 'rollup-plugin-css-only'; -import typescript from '@rollup/plugin-typescript'; -import environment from 'vite-plugin-environment'; -import path from 'path'; +import css from "rollup-plugin-css-only"; +import { execSync } from "child_process"; -// https://vite.dev/config/ -export default defineConfig({ - plugins: [ - svelte(), - css({ output: "bundle.css" }), - typescript({ - inlineSources: true, - }), - environment("all", { prefix: "CANISTER_" }), - environment("all", { prefix: "DFX_" }), - ], - css: { - postcss: { - plugins: [autoprefixer(), tailwindcss()], +const environment = process.env.ICP_ENVIRONMENT || "local"; +const CANISTER_NAMES = ["ic_vetkeys_encrypted_maps_canister"]; + +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`.", + ); } - }, - build: { - sourcemap: true, - rollupOptions: { - output: { - inlineDynamicImports: true, - }, + + 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, + }; +} + +export default defineConfig(({ command }) => ({ + plugins: [svelte(), css({ output: "bundle.css" })], + css: { + postcss: { + plugins: [autoprefixer(), tailwindcss()], + }, }, - }, - resolve: { - alias: { - 'ic_vetkeys': path.resolve(__dirname, '../../../frontend/ic_vetkeys/src'), - 'ic_vetkeys/encrypted_maps': path.resolve(__dirname, '../../../frontend/ic_vetkeys/src/encrypted_maps'), - } - }, - root: "./", - server: { - hmr: false - } -}) \ No newline at end of file + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + root: "./", + ...(command === "serve" ? { server: getDevServerConfig() } : {}), +})); diff --git a/rust/vetkeys/password_manager/motoko/backend/icp.yaml b/rust/vetkeys/password_manager/motoko/backend/icp.yaml deleted file mode 100644 index b4101fe9c..000000000 --- a/rust/vetkeys/password_manager/motoko/backend/icp.yaml +++ /dev/null @@ -1,7 +0,0 @@ -canisters: - - name: ic_vetkeys_encrypted_maps_canister - recipe: - type: "@dfinity/motoko@v4.1.0" - configuration: - main: src/Main.mo - args: --enhanced-orthogonal-persistence diff --git a/rust/vetkeys/password_manager/motoko/backend/mops.toml b/rust/vetkeys/password_manager/motoko/backend/mops.toml deleted file mode 100644 index 8cc38c25d..000000000 --- a/rust/vetkeys/password_manager/motoko/backend/mops.toml +++ /dev/null @@ -1,16 +0,0 @@ -[toolchain] -moc = "1.5.0" - -[package] -name = "ic-vetkeys-encrypted-maps-canister" -version = "0.1.0" -repository = "https://github.com/dfinity/vetkeys/backend/mo/canisters/ic_vetkeys_encrypted_maps_canister" -keywords = [ - "vetkeys,vetkd,encryption,privacy,signature,BLS,key ", - "derivation,IBE" -] -license = "Apache-2.0" - -[dependencies] -base = "0.14.6" -ic-vetkeys = "0.4.0" diff --git a/rust/vetkeys/password_manager/motoko/backend/src/Main.mo b/rust/vetkeys/password_manager/motoko/backend/src/Main.mo index ad0c468bf..8a2fe463f 100644 --- a/rust/vetkeys/password_manager/motoko/backend/src/Main.mo +++ b/rust/vetkeys/password_manager/motoko/backend/src/Main.mo @@ -1,10 +1,10 @@ import IcVetkeys "mo:ic-vetkeys"; import Types "mo:ic-vetkeys/Types"; -import Principal "mo:base/Principal"; -import Text "mo:base/Text"; -import Blob "mo:base/Blob"; -import Result "mo:base/Result"; -import Array "mo:base/Array"; +import Principal "mo:core/Principal"; +import Text "mo:core/Text"; +import Blob "mo:core/Blob"; +import Result "mo:core/Result"; +import Array "mo:core/Array"; persistent actor class (keyName : Text) { let encryptedMapsState = IcVetkeys.EncryptedMaps.newEncryptedMapsState({ curve = #bls12_381_g2; name = keyName }, "password_manager_example_dapp"); diff --git a/rust/vetkeys/password_manager/motoko/dfx.json b/rust/vetkeys/password_manager/motoko/dfx.json deleted file mode 100644 index 3118676e7..000000000 --- a/rust/vetkeys/password_manager/motoko/dfx.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "canisters": { - "ic_vetkeys_encrypted_maps_canister": { - "main": "backend/src/Main.mo", - "type": "motoko", - "args": "--enhanced-orthogonal-persistence", - "init_arg": "(\"test_key_1\")" - }, - "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": ["ic_vetkeys_encrypted_maps_canister", "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": "" - } - } -} diff --git a/rust/vetkeys/password_manager/motoko/icp.yaml b/rust/vetkeys/password_manager/motoko/icp.yaml new file mode 100644 index 000000000..6630f604d --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/icp.yaml @@ -0,0 +1,22 @@ +canisters: + - name: ic_vetkeys_encrypted_maps_canister + 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/motoko/mops.toml b/rust/vetkeys/password_manager/motoko/mops.toml index 593456675..0d53ffde3 100644 --- a/rust/vetkeys/password_manager/motoko/mops.toml +++ b/rust/vetkeys/password_manager/motoko/mops.toml @@ -1,13 +1,6 @@ -[package] -name = "ic-vetkeys-encrypted-maps-canister" -version = "0.1.0" -repository = "https://github.com/dfinity/vetkeys/examples/password_manager/motoko" -keywords = [ - "vetkeys,vetkd,encryption,privacy,signature,BLS,key ", - "derivation,IBE" -] -license = "Apache-2.0" +[toolchain] +moc = "1.9.0" [dependencies] -base = "0.14.6" -ic-vetkeys = "0.4.0" +core = "2.5.0" +ic-vetkeys = "0.5.0" diff --git a/rust/vetkeys/password_manager/rust/Cargo.toml b/rust/vetkeys/password_manager/rust/Cargo.toml index 0169acf9f..8450c49e2 100644 --- a/rust/vetkeys/password_manager/rust/Cargo.toml +++ b/rust/vetkeys/password_manager/rust/Cargo.toml @@ -3,9 +3,10 @@ members = ["backend"] resolver = "2" [workspace.dependencies] -ic-cdk = "0.19.0" +ic-cdk = "0.20.1" +ic-cdk-management-canister = "0.1.1" ic-stable-structures = "0.7.0" -ic-vetkeys = "0.6.0" +ic-vetkeys = "0.7.0" [profile.release] lto = true diff --git a/rust/vetkeys/password_manager/rust/backend/Cargo.toml b/rust/vetkeys/password_manager/rust/backend/Cargo.toml index cb662f05f..7e3c821a2 100644 --- a/rust/vetkeys/password_manager/rust/backend/Cargo.toml +++ b/rust/vetkeys/password_manager/rust/backend/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib"] [dependencies] candid = "0.10.2" ic-cdk = { workspace = true } +ic-cdk-management-canister = { workspace = true } ic-dummy-getrandom-for-wasm = "0.1.0" ic-stable-structures = { workspace = true } ic-vetkeys = { workspace = true } diff --git a/rust/vetkeys/password_manager/rust/backend/src/lib.rs b/rust/vetkeys/password_manager/rust/backend/src/lib.rs index ee4034817..ca58ed57e 100644 --- a/rust/vetkeys/password_manager/rust/backend/src/lib.rs +++ b/rust/vetkeys/password_manager/rust/backend/src/lib.rs @@ -1,11 +1,11 @@ use std::cell::RefCell; use candid::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::DefaultMemoryImpl; +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}; @@ -17,17 +17,30 @@ thread_local! { 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_dapp", + "encrypted_maps_app", key_id, id_to_memory(0), id_to_memory(1), diff --git a/rust/vetkeys/password_manager/rust/dfx.json b/rust/vetkeys/password_manager/rust/dfx.json deleted file mode 100644 index 356bdb3f5..000000000 --- a/rust/vetkeys/password_manager/rust/dfx.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "canisters": { - "ic_vetkeys_encrypted_maps_canister": { - "candid": "backend/ic_vetkeys_encrypted_maps_canister.did", - "package": "ic-vetkeys-encrypted-maps-canister", - "type": "rust", - "init_arg": "(\"test_key_1\")" - }, - "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": ["ic_vetkeys_encrypted_maps_canister", "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" - } - } -} diff --git a/rust/vetkeys/password_manager/rust/icp.yaml b/rust/vetkeys/password_manager/rust/icp.yaml new file mode 100644 index 000000000..b9c298ad1 --- /dev/null +++ b/rust/vetkeys/password_manager/rust/icp.yaml @@ -0,0 +1,23 @@ +canisters: + - name: ic_vetkeys_encrypted_maps_canister + recipe: + type: "@dfinity/rust@v3.2.0" + configuration: + package: ic-vetkeys-encrypted-maps-canister + candid: backend/ic_vetkeys_encrypted_maps_canister.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/rust/rust-toolchain.toml b/rust/vetkeys/password_manager/rust/rust-toolchain.toml index 2a2058b04..990104f05 100644 --- a/rust/vetkeys/password_manager/rust/rust-toolchain.toml +++ b/rust/vetkeys/password_manager/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