Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
249a509
chore(vetkeys): migrate password_manager_with_metadata to icp-cli and…
marc0olo May 29, 2026
48ef4ef
fix(ci): update password_manager_with_metadata to ic-cdk 0.20.1 and @…
marc0olo May 30, 2026
caf7272
fix(password_manager_with_metadata): eslint/prettier and ic-stable-st…
marc0olo Jun 1, 2026
b71a5ce
fix(password_manager_with_metadata): await getIdentity() in vaults.ts
marc0olo Jun 1, 2026
37ac318
fix(password_manager_with_metadata): await getIdentity in Svelte comp…
marc0olo Jun 1, 2026
da5c194
fix(password_manager_with_metadata): await getIdentity in all components
marc0olo Jun 1, 2026
14692e3
fix(password_manager_with_metadata): add principal to auth state; ren…
marc0olo Jun 1, 2026
5ef5027
chore(password_manager_with_metadata): rename CI jobs to language-fir…
marc0olo Jun 1, 2026
d1bf7a1
fix(password_manager_with_metadata): format vaults.ts for prettier pr…
marc0olo Jun 1, 2026
f470fb9
fix(password_manager_with_metadata): format refreshVaults calls for p…
marc0olo Jun 1, 2026
0f72e16
fix(password_manager_with_metadata): format .catch arrow for printWid…
marc0olo Jun 1, 2026
7741036
fix(password_manager_with_metadata): use let for vaultOwner in NewPas…
marc0olo Jun 1, 2026
ea60754
fix: use key_1 (not test_key_1), npx --yes, console.error in basic_ibe
marc0olo Jun 1, 2026
8d0f864
docs: update vetkeys doc links to docs.internetcomputer.org/concepts/…
marc0olo Jun 1, 2026
f0871b3
docs: add folder structure, fix dev command and cd instructions
marc0olo Jun 1, 2026
c46a8ef
fix: simplify rootKey; use /authorize (not /#authorize) and https://i…
marc0olo Jun 2, 2026
cd5baa8
fix: use https://id.ai/authorize for production identityProvider
marc0olo Jun 2, 2026
b220d6f
chore: migrate Motoko backend from base to core 2.5.0
marc0olo Jun 2, 2026
53ffb06
fix: correct base→core API issues in Motoko backends
marc0olo Jun 2, 2026
0410023
fix: correct pure/Map and VarArray API usage in Motoko backends
marc0olo Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: rust-vetkeys-password-manager-with-metadata
on:
push:
branches:
- master
pull_request:
paths:
- rust/vetkeys/password_manager_with_metadata/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/rust-vetkeys-password-manager-with-metadata-example.yml
- .ic-commit
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
rust-password-manager-with-metadata-darwin:
runs-on: macos-15
steps:
- uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0
- name: Provision Darwin
run: bash .github/workflows/provision-darwin.sh
- name: Pre-download network launcher
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash .github/workflows/pre-download-launcher.sh
- name: Password Manager with Metadata Rust Darwin
run: |
pushd rust/vetkeys/password_manager_with_metadata/rust
icp network start -d
icp deploy
popd
rust-password-manager-with-metadata-linux:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0
- name: Provision Linux
run: bash .github/workflows/provision-linux.sh
- name: Pre-download network launcher
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash .github/workflows/pre-download-launcher.sh
- name: Password Manager with Metadata Rust Linux
run: |
pushd rust/vetkeys/password_manager_with_metadata/rust
icp network start -d
icp deploy
popd
motoko-password-manager-with-metadata-darwin:
runs-on: macos-15
steps:
- uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0
- name: Provision Darwin
run: bash .github/workflows/provision-darwin.sh
- name: Pre-download network launcher
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash .github/workflows/pre-download-launcher.sh
- name: Password Manager with Metadata Motoko Darwin
run: |
pushd rust/vetkeys/password_manager_with_metadata/motoko
icp network start -d
icp deploy
popd
motoko-password-manager-with-metadata-linux:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@50fbc622fc4ef5163becd7fab6573eac35f8462e # v1.2.0
- name: Provision Linux
run: bash .github/workflows/provision-linux.sh
- name: Pre-download network launcher
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bash .github/workflows/pre-download-launcher.sh
- name: Password Manager with Metadata Motoko Linux
run: |
pushd rust/vetkeys/password_manager_with_metadata/motoko
icp network start -d
icp deploy
popd
52 changes: 38 additions & 14 deletions rust/vetkeys/password_manager_with_metadata/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# VetKey Password Manager with Metadata

<!-- TODO: re-enable once icp.ninja supports icp-cli (currently requires dfx)
| Motoko backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager_with_metadata/motoko)|
| --- | --- |
| Rust backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager_with_metadata/rust) |
-->

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**.

Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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' {} +
npx --yes @icp-sdk/bindgen --did-file rust/backend/backend.did \
--out-dir frontend/src/declarations/password_manager_with_metadata \
--declarations-flat --force
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -131,7 +138,9 @@
<PasswordEditor {editor} class="mb-3" disabled={creating} />
<button
class="btn btn-primary mt-6 {creating ? 'loading' : ''}"
disabled={creating}
disabled={creating ||
vaultName.trim() === "" ||
passwordName.trim() === ""}
on:click={add}>{creating ? "Adding..." : "Add password"}</button
>
</main>
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading
Loading