Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8f26065
chore: migrate encrypted_notes_app_vetkd to icp CLI and @icp-sdk/vetkeys
marc0olo May 29, 2026
4517514
fix(ci): update encrypted_notes_app_vetkd to ic-cdk 0.20.1 and fix IC…
marc0olo May 30, 2026
3bf82f4
fix(encrypted_notes): gen_bindings symlink, npm ci, motoko base dep
marc0olo Jun 1, 2026
102339c
fix(encrypted_notes): use --legacy-peer-deps for npm install
marc0olo Jun 1, 2026
5b29303
fix(encrypted_notes): import from encrypted_notes_rust.did in actor.ts
marc0olo Jun 1, 2026
36a907a
chore(encrypted_notes): rename CI job keys to remove rust-vetkeys- pr…
marc0olo Jun 1, 2026
067ffb6
chore(encrypted_notes): rename CI jobs to language-first format
marc0olo Jun 1, 2026
ec295ac
fix: use key_1 (not test_key_1), npx --yes, console.error in basic_ibe
marc0olo Jun 1, 2026
61471bd
refactor: use generated createActor and Backend type from declarations
marc0olo Jun 1, 2026
e5016f5
fix(encrypted_notes): import createActor from wrapper, not IDL file
marc0olo Jun 1, 2026
f0a721c
docs: add folder structure, fix dev command and cd instructions
marc0olo Jun 1, 2026
288f172
fix: simplify rootKey; use /authorize (not /#authorize) and https://i…
marc0olo Jun 2, 2026
92b2c55
fix: use https://id.ai/authorize for production identityProvider
marc0olo Jun 2, 2026
82dcf95
chore: migrate Motoko backend from base to core 2.5.0
marc0olo Jun 2, 2026
6b9099c
fix: correct base→core API issues in Motoko backends
marc0olo Jun 2, 2026
c84599c
fix: correct pure/Map and VarArray API usage in Motoko backends
marc0olo Jun 2, 2026
7d72998
fix(encrypted_notes): correct pure/List and Runtime API usage
marc0olo Jun 3, 2026
24edb68
fix(encrypted_notes): add explicit type params to List.fromArray calls
marc0olo Jun 3, 2026
1d47100
fix: switch back to test_key_1 (supported again in latest icp-cli-net…
marc0olo Jun 3, 2026
0e70177
ci: switch to icp-dev-env container images, Ubuntu only
marc0olo Jun 3, 2026
9573929
ci: rename workflow files to vetkeys-{example}.yml convention
marc0olo Jun 3, 2026
b790b9a
ci: fix workflow name and simplify job IDs to rust/motoko only
marc0olo Jun 3, 2026
ba85b4a
ci: pass ICP_CLI_GITHUB_TOKEN to avoid network launcher rate limiting
marc0olo Jun 3, 2026
9c7d7b0
fix: use LocalStorage only on localhost as workaround for IDB race co…
marc0olo Jun 3, 2026
a0a1238
fix: format LocalStorage spread for printWidth 80
marc0olo Jun 3, 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
36 changes: 36 additions & 0 deletions .github/workflows/vetkeys-encrypted-notes-app-vetkd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: vetkeys-encrypted-notes-app-vetkd

on:
push:
branches:
- master
pull_request:
paths:
- rust/vetkeys/encrypted_notes_app_vetkd/**
- .github/workflows/vetkeys-encrypted-notes-app-vetkd.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 Encrypted Notes App Vetkd Rust
working-directory: rust/vetkeys/encrypted_notes_app_vetkd/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 Encrypted Notes App Vetkd Motoko
working-directory: rust/vetkeys/encrypted_notes_app_vetkd/motoko
run: icp network start -d && icp deploy
Original file line number Diff line number Diff line change
@@ -1,58 +1,72 @@
# Encrypted notes: vetKD

<!--
ICP Ninja links removed: icp.ninja support requires dfx which is no longer used in this example.
| Motoko backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko)|
| --- | --- |
| Rust backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/encrypted_notes_dapp_vetkd/rust) |
-->

Encrypted notes is an example dapp for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the dapps frontend.
Encrypted notes is an example dapp for authoring and storing confidential information on the Internet Computer (ICP) in the form of short pieces of text. Users can create and access their notes via any number of automatically synchronized devices authenticated via Internet Identity (II). Notes are stored confidentially using vetKeys. The end-to-end encryption is performed by the dapp's frontend.

In particular, the notes are encrypted with an AES key that is derived (directly in the browser) from a note-ID-specific vetKey obtained from the backend canister (in encrypted form, using an ephemeral transport key), which itself obtains it from the vetKD system API. This way, there is no need for any device management in the dapp, plus sharing of notes becomes possible.

The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) to enable the sharing of notes between users. The derived AES keys are stored as non-extractable CryptoKeys in an IndexedDB in the browser for efficiency so that their respective vetKey only has to be fetched from the server once. To improve the security even further, the vetKeys' derivation information could be adapted to include a (numeric) epoch that advances each time the list of users with which the note is shared is changed.
The vetKey used to encrypt and decrypt a note is note-ID-specific (and not, for example, principal-specific) to enable the sharing of notes between users. The derived AES keys are stored as non-extractable CryptoKeys in an IndexedDB in the browser for efficiency so that their respective vetKey only has to be fetched from the server once.

## Prerequisites

This example requires an installation of:

- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx).
- [x] Install the [ICP CLI](https://cli.internetcomputer.org).
- [x] Install [npm](https://www.npmjs.com/package/npm).

### (Optionally) Choose a Different Master Key
## Folder Structure

This example provides both a **Rust** and a **Motoko** backend, sharing a common `frontend/`:

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.
```
encrypted_notes_app_vetkd/
├── 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.
## Running the Frontend in Development Mode

After deploying, run from the `frontend` folder:
```bash
cd frontend
npm run dev:motoko # if you deployed the Motoko backend
# or
npm run dev:rust # if you deployed the Rust backend
```

## Example Components

### Backend

The backend consists of a canister that stores encrypted notes. It is automatically deployed with `dfx deploy`.
The backend consists of a canister that stores encrypted notes. It is automatically deployed with `icp deploy`.

### Frontend

The frontend is a **Svelte** application providing a user-friendly interface for managing encrypted notes.

To run the frontend in development mode with hot reloading (after running `dfx deploy`):

```bash
npm run dev
```

## Limitations

This example dapp does not implement key rotation, which is strongly recommended in a production environment.
Key rotation involves periodically changing encryption keys and re-encrypting data to enhance security.
In a production dapp, key rotation would be useful to limit the impact of potential key compromise if a malicious party gains access to a key, or to limit access when users are added or removed from note sharing.

## Troubleshooting

If you run into issues, clearing all the application-specific IndexedDBs in the browser (which are used to store Internet Identity information and the derived non-extractable AES keys) might help fix the issue. For example in Chrome, go to Inspect → Application → Local Storage → `http://localhost:3000/` → Clear All, and then reload.
If you run into issues, clearing all the application-specific IndexedDBs in the browser might help. For example in Chrome, go to Inspect → Application → Local Storage → Clear All, and then reload.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />

<title>Encrypted Notes</title>

<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="stylesheet" href="/build/bundle.css" />

<script defer src="/build/main.js"></script>
</head>

<body></body>
<body>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
39 changes: 39 additions & 0 deletions rust/vetkeys/encrypted_notes_app_vetkd/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "encrypted-notes-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"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": "npm run build:bindings && vite build",
"build:bindings": "cd scripts && ./gen_bindings.sh",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.4",
"@types/node": "^24.0.10",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.31",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"tailwindcss": "^3.0.17",
"tslib": "^2.8.1",
"typescript": "~5.7.2",
"vite": "^6.4.1"
},
"dependencies": {
"@icp-sdk/auth": "^7.1.0",
"@icp-sdk/core": "^5.4.0",
"@icp-sdk/vetkeys": "^0.5.0-beta.0",
"daisyui": "^1.25.4",
"idb-keyval": "6.2.1",
"isomorphic-dompurify": "^2.25.0",
"svelte-icons": "^2.1.0",
"svelte-router-spa": "^6.0.3",
"typewriter-editor": "^0.6.45"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';

export default {
plugins: [tailwindcss(), autoprefixer()],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
set -eu

# Resolve the script's physical location so we work correctly even when the
# icp CLI has symlinked `frontend/` into a backend subdirectory for the build.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
FRONTEND_DIR="$(dirname "$SCRIPT_DIR")"
EXAMPLE_ROOT="$(dirname "$FRONTEND_DIR")"

# Bindings are always generated from the Rust backend since both backends
# expose the same Candid interface.

rm -rf "$FRONTEND_DIR/src/declarations/encrypted_notes"
mkdir -p "$FRONTEND_DIR/src/declarations/encrypted_notes"
npx --yes @icp-sdk/bindgen \
--did-file "$EXAMPLE_ROOT/rust/backend/src/encrypted_notes_rust.did" \
--out-dir "$FRONTEND_DIR/src/declarations/encrypted_notes" \
--declarations-flat --force
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HttpAgent } from "@icp-sdk/core/agent";
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
import { createActor as createEncryptedNotesActor, type Backend } from "../declarations/encrypted_notes/encrypted_notes_rust";

export type BackendActor = Backend;

const canisterEnv = safeGetCanisterEnv<{
"PUBLIC_CANISTER_ID:encrypted_notes": string;
}>();

export async function createActor(options?: { identity?: any }): Promise<BackendActor> {
const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:encrypted_notes"];
const agent = await HttpAgent.create({
identity: options?.identity,
host: window.location.origin,
rootKey: canisterEnv?.IC_ROOT_KEY,
});
return createEncryptedNotesActor(canisterId, { agent });
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { BackendActor } from './actor';
import { get, set } from 'idb-keyval';

// Usage of the imported bindings only works if the respective .wasm was loaded, which is done in main.ts.
// See also https://github.com/rollup/plugins/tree/master/packages/wasm#using-with-wasm-bindgen-and-wasm-pack
import * as vetkd from "@dfinity/vetkeys";
import * as vetkd from "@icp-sdk/vetkeys";

export class CryptoService {
constructor(private actor: BackendActor) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { EncryptedNote } from '../lib/backend';
import type { EncryptedNote } from '../declarations/encrypted_notes/backend.did';
import type { CryptoService } from './crypto';
import type { Principal } from '@dfinity/principal';
import type { Principal } from '@icp-sdk/core/principal';

export interface NoteModel {
id: bigint;
Expand Down
126 changes: 126 additions & 0 deletions rust/vetkeys/encrypted_notes_app_vetkd/frontend/src/store/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { get, writable } from "svelte/store";
import { type BackendActor, createActor } from "../lib/actor";
import { AuthClient, LocalStorage } from "@icp-sdk/auth/client";
import { CryptoService } from "../lib/crypto";
import { showError } from "./notifications";
import { navigateTo } from "svelte-router-spa";

export type AuthState =
| { state: "initializing-auth" }
| { state: "anonymous"; actor: BackendActor; client: AuthClient }
| { state: "initializing-crypto"; actor: BackendActor; client: AuthClient }
| { state: "synchronizing"; actor: BackendActor; client: AuthClient }
| {
state: "initialized";
actor: BackendActor;
client: AuthClient;
crypto: CryptoService;
}
| { state: "error"; error: string };

export const auth = writable<AuthState>({ state: "initializing-auth" });

async function initAuth() {
const isLocal =
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: isLocal
? "http://id.ai.localhost:8000/authorize"
: "https://id.ai/authorize",
...(isLocal
? { storage: new LocalStorage(), keyType: "Ed25519" as const }
: {}),
});
if (client.isAuthenticated()) {
authenticate(client);
} else {
const actor = await createActor();
auth.update(() => ({
state: "anonymous",
actor,
client,
}));
}
}

initAuth();

export async function login() {
const currentAuth = get(auth);

if (currentAuth.state === "anonymous") {
await currentAuth.client.signIn();
authenticate(currentAuth.client);
}
}

export async function logout() {
const currentAuth = get(auth);

if (currentAuth.state === "initialized") {
await currentAuth.client.signOut();
const actor = await createActor();
auth.update(() => ({
state: "anonymous",
actor,
client: currentAuth.client,
}));
navigateTo("/");
}
}

export async function authenticate(client: AuthClient) {
handleSessionTimeout();

try {
const actor = await createActor({ identity: await client.getIdentity() });

auth.update(() => ({
state: "initializing-crypto",
actor,
client,
}));

const cryptoService = new CryptoService(actor);

auth.update(() => ({
state: "initialized",
actor,
client,
crypto: cryptoService,
}));
} catch (e: any) {
auth.update(() => ({
state: "error",
error: e.message || "An error occurred",
}));
}
}

function handleSessionTimeout() {
setTimeout(() => {
try {
const delegation = JSON.parse(
window.localStorage.getItem("ic-delegation") ?? "null",
) as {
delegations: Array<{ delegation: { expiration: string } }>;
} | null;
if (!delegation) return;

const expirationTimeMs =
Number.parseInt(delegation.delegations[0].delegation.expiration, 16) /
1000000;

setTimeout(() => {
logout();
}, expirationTimeMs - Date.now());
} catch {
console.error("Could not handle delegation expiry.");
}
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

export default {
preprocess: vitePreprocess(),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
content: [
'./index.html',
'./src/**/*.svelte',
'./src/**/*.ts',
],
theme: {
extend: {},
},
plugins: [],
};
Loading
Loading