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