diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/README.md b/rust/vetkeys/encrypted_notes_dapp_vetkd/README.md
new file mode 100644
index 000000000..01d63e3ea
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/README.md
@@ -0,0 +1,58 @@
+# Encrypted notes: vetKD
+
+| Motoko backend | [](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko)|
+| --- | --- |
+| Rust backend | [](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 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.
+
+## 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 [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.
+
+## Deploy the Canisters Locally
+
+If you want to deploy this project locally with a Motoko backend, then run:
+```bash
+dfx start --background && dfx deploy
+```
+from the `motoko` folder.
+
+To use the Rust backend instead of Motoko, run the same command in the rust folder.
+
+## Example Components
+
+### Backend
+
+The backend consists of a canister that stores encrypted notes. It is automatically deployed with `dfx 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.
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/package.json b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/package.json
new file mode 100644
index 000000000..2a36dba9e
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "encrypted-notes-dapp",
+ "version": "0.2.0",
+ "keywords": [
+ "Internet Computer",
+ "Motoko",
+ "Svelte",
+ "Canister",
+ "Rust"
+ ],
+ "scripts": {
+ "build": "npm run build:bindings && rollup -c --bundleConfigAsCjs",
+ "build:bindings": "cd scripts && ./gen_bindings.sh",
+ "dev": "npm run build:bindings && rollup -c --bundleConfigAsCjs -w",
+ "start": "sirv public --single",
+ "test": "jest src",
+ "test:watch": "npm run test -- --watch",
+ "check": "svelte-check --tsconfig ./tsconfig.json"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.16.8",
+ "@rollup/plugin-commonjs": "^25.0.0",
+ "@rollup/plugin-json": "^6.0.0",
+ "@rollup/plugin-node-resolve": "^15.0.2",
+ "@rollup/plugin-terser": "^1.0.0",
+ "@rollup/plugin-typescript": "^12.1.2",
+ "@tailwindcss/line-clamp": "^0.3.1",
+ "@testing-library/jest-dom": "^5.16.1",
+ "@testing-library/svelte": "^3.0.3",
+ "@tsconfig/svelte": "^2.0.0",
+ "autoprefixer": "^10.4.2",
+ "babel-jest": "^27.4.6",
+ "daisyui": "^1.25.4",
+ "idb-keyval": "6.2.1",
+ "jest": "^30.2.0",
+ "postcss": "^8.4.31",
+ "rollup": "^3.30.0",
+ "rollup-plugin-css-only": "^4.3.0",
+ "rollup-plugin-dotenv": "^0.5.1",
+ "rollup-plugin-inject": "^3.0.2",
+ "rollup-plugin-inject-process-env": "^1.3.1",
+ "rollup-plugin-livereload": "^2.0.0",
+ "rollup-plugin-polyfill-node": "^0.12.0",
+ "rollup-plugin-svelte": "^7.2.2",
+ "svelte": "^3.59.1",
+ "svelte-check": "^3.3.2",
+ "svelte-jester": "^2.3.2",
+ "svelte-preprocess": "^5.0.3",
+ "tailwindcss": "^3.0.17",
+ "tslib": "^2.0.0",
+ "typescript": "^4.0.0"
+ },
+ "dependencies": {
+ "@dfinity/agent": "^2.1.3",
+ "@dfinity/auth-client": "^2.1.3",
+ "@dfinity/candid": "^2.1.3",
+ "@dfinity/identity": "^2.1.3",
+ "@dfinity/principal": "^2.1.3",
+ "@dfinity/vetkeys": "^0.3.0",
+ "isomorphic-dompurify": "^2.25.0",
+ "sirv-cli": "^1.0.0",
+ "svelte-icons": "^2.1.0",
+ "svelte-router-spa": "^6.0.3",
+ "typewriter-editor": "^0.6.45"
+ },
+ "jest": {
+ "transform": {
+ "^.+\\.js$": "babel-jest",
+ "^.+\\.svelte$": "svelte-jester"
+ },
+ "moduleFileExtensions": [
+ "js",
+ "svelte"
+ ],
+ "setupFilesAfterEnv": [
+ "@testing-library/jest-dom/extend-expect",
+ "./jest-env.js"
+ ],
+ "testEnvironment": "jsdom"
+ }
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/.ic-assets.json5 b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/.ic-assets.json5
new file mode 100644
index 000000000..6d07243f3
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/.ic-assets.json5
@@ -0,0 +1,10 @@
+[
+ {
+ match: "**/*",
+ security_policy: "hardened",
+ headers: {
+ "Content-Security-Policy": "default-src 'self';script-src 'self';connect-src 'self' http://localhost:* https://icp0.io https://*.icp0.io https://icp-api.io;img-src 'self';style-src * 'unsafe-inline';object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;",
+ },
+ allow_raw_access: false
+ },
+]
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/favicon.png b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/favicon.png
new file mode 100644
index 000000000..7e6f5eb5a
Binary files /dev/null and b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/favicon.png differ
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png
new file mode 100644
index 000000000..1a227a2b0
Binary files /dev/null and b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png differ
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png
new file mode 100644
index 000000000..e1da19855
Binary files /dev/null and b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png differ
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png
new file mode 100644
index 000000000..90029c464
Binary files /dev/null and b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png differ
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png
new file mode 100644
index 000000000..f1a2d8081
Binary files /dev/null and b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png differ
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/index.html b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/index.html
new file mode 100644
index 000000000..abc12ca3b
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Encrypted Notes
+
+
+
+
+
+
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/rollup.config.js b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/rollup.config.js
new file mode 100644
index 000000000..535cf2306
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/rollup.config.js
@@ -0,0 +1,121 @@
+import svelte from "rollup-plugin-svelte";
+import commonjs from "@rollup/plugin-commonjs";
+import resolve from "@rollup/plugin-node-resolve";
+import livereload from "rollup-plugin-livereload";
+import terser from "@rollup/plugin-terser";
+import sveltePreprocess from "svelte-preprocess";
+import typescript from "@rollup/plugin-typescript";
+import css from "rollup-plugin-css-only";
+import json from "@rollup/plugin-json";
+import injectProcessEnv from "rollup-plugin-inject-process-env";
+
+const production = !process.env.ROLLUP_WATCH;
+
+function serve(exposeHost) {
+ let server;
+
+ function toExit() {
+ if (server) server.kill(0);
+ }
+
+ return {
+ writeBundle() {
+ if (server) return;
+ server = require("child_process").spawn(
+ "npm",
+ exposeHost
+ ? ["run", "start-expose", "--", "--dev"]
+ : ["run", "start", "--", "--dev"],
+ {
+ stdio: ["ignore", "inherit", "inherit"],
+ shell: true,
+ }
+ );
+
+ process.on("SIGTERM", toExit);
+ process.on("exit", toExit);
+ },
+ };
+}
+
+export default (config) => {
+ const exposeHost = !!config.configExpose;
+
+ return {
+ input: "src/main.ts",
+ output: {
+ sourcemap: true,
+ name: "app",
+ format: "iife",
+
+ file: "public/build/main.js",
+ inlineDynamicImports: true,
+ },
+ plugins: [
+ svelte({
+ preprocess: sveltePreprocess({
+ sourceMap: !production,
+ postcss: {
+ plugins: [require("tailwindcss")(), require("autoprefixer")()],
+ },
+ }),
+ compilerOptions: {
+ // enable run-time checks when not in production
+ dev: !production,
+ },
+ }),
+ // we'll extract any component CSS out into
+ // a separate file - better for performance
+ css({ output: "bundle.css" }),
+
+ // If you have external dependencies installed from
+ // npm, you'll most likely need these plugins. In
+ // some cases you'll need additional configuration -
+ // consult the documentation for details:
+ // https://github.com/rollup/plugins/tree/master/packages/commonjs
+ resolve({
+ preferBuiltins: false,
+ browser: true,
+ dedupe: ["svelte"],
+ }),
+ commonjs(),
+ typescript({
+ sourceMap: !production,
+ inlineSources: !production,
+ }),
+ json(),
+ injectProcessEnv({
+ DFX_NETWORK: process.env.DFX_NETWORK,
+ CANISTER_ID_ENCRYPTED_NOTES: process.env.CANISTER_ID_ENCRYPTED_NOTES,
+ }),
+
+ // In dev mode, call `npm run start` once
+ // the bundle has been generated
+ !production && serve(exposeHost),
+
+ // Watch the `public` directory and refresh the
+ // browser on changes when not in production
+ !production && livereload("public"),
+
+ // If we're building for production (npm run build
+ // instead of npm run dev), minify
+ production && terser(),
+ ],
+ watch: {
+ clearScreen: false,
+ },
+ onwarn: function (warning) {
+ if (
+ [
+ "CIRCULAR_DEPENDENCY",
+ "THIS_IS_UNDEFINED",
+ "EVAL",
+ "NAMESPACE_CONFLIC",
+ ].includes(warning.code)
+ ) {
+ return;
+ }
+ console.warn(warning.message);
+ },
+ };
+};
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/scripts/gen_bindings.sh b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/scripts/gen_bindings.sh
new file mode 100755
index 000000000..dbc07b9d7
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/scripts/gen_bindings.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+cd ../.. && dfx generate encrypted_notes || exit 1
+
+rm -r frontend/src/declarations/encrypted_notes > /dev/null 2>&1 || true
+
+mkdir -p frontend/src/declarations/encrypted_notes
+mv src/declarations/encrypted_notes frontend/src/declarations
+rmdir -p src/declarations > /dev/null 2>&1 || true
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/App.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/App.svelte
new file mode 100644
index 000000000..3c24748b1
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/App.svelte
@@ -0,0 +1,19 @@
+
+
+{#if $auth.state === 'initialized'}
+
+{:else}
+
+{/if}
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Disclaimer.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Disclaimer.svelte
new file mode 100644
index 000000000..16ce216fc
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Disclaimer.svelte
@@ -0,0 +1,26 @@
+
+
+{#if !isDismissed}
+
+{/if}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/DisclaimerCopy.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/DisclaimerCopy.svelte
new file mode 100644
index 000000000..dcbfd5c9d
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/DisclaimerCopy.svelte
@@ -0,0 +1,3 @@
+Disclaimer: This sample dapp is intended exclusively for experimental
+purpose. You are advised not to use this dapp for storing your critical data such
+as keys or passwords.
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/EditNote.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/EditNote.svelte
new file mode 100644
index 000000000..a5ef18c09
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/EditNote.svelte
@@ -0,0 +1,162 @@
+
+
+{#if editedNote}
+
+ Edit note
+
+ {#if !deleting}
+
+ {/if}
+
+ {deleting ? 'Deleting...' : ''}
+
+
+
+ {#if $notesStore.state === 'loaded'}
+
+ addTag(e.detail)}
+ on:remove={(e) => removeTag(e.detail)}
+ disabled={updating || deleting}
+ />
+ {updating ? 'Saving...' : 'Save'}
+
+
+ {:else if $notesStore.state === 'loading'}
+ Loading notes...
+ {/if}
+
+{:else}
+
+
+ {#if $notesStore.state === 'loading'}
+
+ Loading note...
+ {:else if $notesStore.state === 'loaded'}
+ Could not find note.
+ {/if}
+
+{/if}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Header.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Header.svelte
new file mode 100644
index 000000000..922f3f8c0
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Header.svelte
@@ -0,0 +1,25 @@
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Hero.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Hero.svelte
new file mode 100644
index 000000000..c88799230
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Hero.svelte
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+ Encrypted Notes
+
+
+ Your private notes on the Internet Computer.
+
+
+ A safe place to store your personal lists, thoughts, ideas or
+ passphrases and much more...
+
+
+ {#if auth.state === 'initializing-auth' || auth.state === 'initializing-crypto'}
+
+
+ Initializing...
+
+ {:else if auth.state === 'synchronizing'}
+
+
+ Synchronizing... Please keep the app open on a device that's already added.
+
+ {:else if auth.state === 'anonymous'}
+
login()}
+ >Please login to start writing notes
+ {:else if auth.state === 'error'}
+
An error occurred.
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/LayoutAuthenticated.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/LayoutAuthenticated.svelte
new file mode 100644
index 000000000..cb2624f25
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/LayoutAuthenticated.svelte
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NewNote.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NewNote.svelte
new file mode 100644
index 000000000..5901ed075
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NewNote.svelte
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+ addTag(e.detail)}
+ on:remove={(e) => removeTag(e.detail)}
+ disabled={creating}
+ />
+ {creating ? 'Adding...' : 'Add note'}
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Note.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Note.svelte
new file mode 100644
index 000000000..9b1f9ee78
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Note.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+
+ {#if note.title}
+ {note.title}
+ {:else}
+ Unnamed note
+ {/if}
+
+ {contentSummary}
+ {#if note.tags.length > 0}
+
+ {#each note.tags as tag}
+ {
+ dispatch('tagclick', tag);
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ >
+ {tag}
+
+ {/each}
+
+ {/if}
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NoteEditor.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NoteEditor.svelte
new file mode 100644
index 000000000..120bbe78f
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NoteEditor.svelte
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notes.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notes.svelte
new file mode 100644
index 000000000..fd1fdaf37
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notes.svelte
@@ -0,0 +1,75 @@
+
+
+
+ Your notes
+
+ {#if $notesStore.state === 'loaded' && $notesStore.list.length > 0}
+ New Note
+ {/if}
+
+
+
+ {#if $notesStore.state === 'loading'}
+
+ Loading notes...
+ {:else if $notesStore.state === 'loaded'}
+ {#if $notesStore.list.length > 0}
+
+
+
+
+
+ {#each filteredNotes as note (note.id)}
+ (filter = e.detail)} />
+ {/each}
+
+ {:else}
+ You don't have any notes.
+
+ {/if}
+ {:else if $notesStore.state === 'error'}
+ Could not load notes.
+ {/if}
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notifications.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notifications.svelte
new file mode 100644
index 000000000..8ea937899
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notifications.svelte
@@ -0,0 +1,22 @@
+
+
+
+ {#each $notifications as n (n.id)}
+
+ {/each}
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SharingEditor.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SharingEditor.svelte
new file mode 100644
index 000000000..15fa4ed2f
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SharingEditor.svelte
@@ -0,0 +1,128 @@
+
+
+
+
Users
+ {#if ownedByMe}
+
+ Add users by their principal to allow them editing the note.
+
+ {:else}
+
+ This note is shared with you. It is owned
+ by {editedNote.owner} .
+
+
Users with whom the owner shared the note:
+ {/if}
+
+ {#each editedNote.users as sharing}
+
{
+ remove(sharing);
+ }}
+ disabled={adding || removing || !ownedByMe}
+ >
+ {sharing}
+
+
+
+
+ {/each}
+
+
{adding ? 'Adding...' : removing ? 'Removing... ' : 'Add'}
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SidebarLayout.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SidebarLayout.svelte
new file mode 100644
index 000000000..1dcbd41f7
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SidebarLayout.svelte
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+ Encrypted Notes
+
+
+
My Principal:
+
{$auth.client.getIdentity().getPrincipal()}
+
+
+
+
+
+
+
+
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Spinner.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Spinner.svelte
new file mode 100644
index 000000000..fd7812cce
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Spinner.svelte
@@ -0,0 +1,3 @@
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/TagEditor.svelte b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/TagEditor.svelte
new file mode 100644
index 000000000..0b4df389e
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/TagEditor.svelte
@@ -0,0 +1,74 @@
+
+
+
+ {#each tags as tag}
+
remove(tag)}
+ >
+ {tag}
+
+
+
+
+ {/each}
+
+
Add
+
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/global.d.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/global.d.ts
new file mode 100644
index 000000000..0e7296906
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/global.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/actor.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/actor.ts
new file mode 100644
index 000000000..6c275f9e5
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/actor.ts
@@ -0,0 +1,54 @@
+import {
+ Actor,
+ ActorConfig,
+ ActorSubclass,
+ HttpAgent,
+ HttpAgentOptions,
+} from "@dfinity/agent";
+import {
+ idlFactory,
+ _SERVICE,
+} from "../declarations/encrypted_notes/encrypted_notes.did.js";
+
+export type BackendActor = ActorSubclass<_SERVICE>;
+
+export function createActor(options?: {
+ agentOptions?: HttpAgentOptions;
+ actorOptions?: ActorConfig;
+}): BackendActor {
+ const hostOptions = {
+ host:
+ process.env.DFX_NETWORK === "ic"
+ ? `https://${process.env.CANISTER_ID_ENCRYPTED_NOTES}.ic0.app`
+ : "http://localhost:8000",
+ };
+ if (!options) {
+ options = {
+ agentOptions: hostOptions,
+ };
+ } else if (!options.agentOptions) {
+ options.agentOptions = hostOptions;
+ } else {
+ options.agentOptions.host = hostOptions.host;
+ }
+
+ const agent = new HttpAgent({ ...options.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);
+ });
+ }
+
+ // Creates an actor with using the candid interface and the HttpAgent
+ return Actor.createActor(idlFactory, {
+ agent,
+ canisterId: process.env.CANISTER_ID_ENCRYPTED_NOTES,
+ ...options?.actorOptions,
+ });
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/crypto.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/crypto.ts
new file mode 100644
index 000000000..cb96c4b83
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/crypto.ts
@@ -0,0 +1,109 @@
+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";
+
+export class CryptoService {
+ constructor(private actor: BackendActor) {
+ }
+
+ // The function encrypts data with the note-id-specific secretKey.
+ public async encryptWithNoteKey(note_id: bigint, owner: string, data: string): Promise {
+ await this.fetch_note_key_if_needed(note_id, owner);
+ const note_key: CryptoKey = await get([note_id.toString(), owner]);
+
+ const data_encoded = Uint8Array.from([...data].map(ch => ch.charCodeAt(0))).buffer
+ // The iv must never be reused with a given key.
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
+ const ciphertext = await window.crypto.subtle.encrypt(
+ {
+ name: "AES-GCM",
+ iv: iv
+ },
+ note_key,
+ data_encoded
+ );
+
+ const iv_decoded = String.fromCharCode(...new Uint8Array(iv));
+ const cipher_decoded = String.fromCharCode(...new Uint8Array(ciphertext));
+ return iv_decoded + cipher_decoded;
+ }
+
+ // The function decrypts the given input data with the note-id-specific secretKey.
+ public async decryptWithNoteKey(note_id: bigint, owner: string, data: string) {
+ await this.fetch_note_key_if_needed(note_id, owner);
+ const note_key: CryptoKey = await get([note_id.toString(), owner]);
+
+ if (data.length < 13) {
+ throw new Error('wrong encoding, too short to contain iv');
+ }
+ const iv_decoded = data.slice(0, 12);
+ const cipher_decoded = data.slice(12);
+ const iv_encoded = Uint8Array.from([...iv_decoded].map(ch => ch.charCodeAt(0))).buffer;
+ const ciphertext_encoded = Uint8Array.from([...cipher_decoded].map(ch => ch.charCodeAt(0))).buffer;
+
+ let decrypted_data_encoded = await window.crypto.subtle.decrypt(
+ {
+ name: "AES-GCM",
+ iv: iv_encoded
+ },
+ note_key,
+ ciphertext_encoded
+ );
+ const decrypted_data_decoded = String.fromCharCode(...new Uint8Array(decrypted_data_encoded));
+ return decrypted_data_decoded;
+ }
+
+ private async fetch_note_key_if_needed(note_id: bigint, owner: string): Promise {
+ if (!await get([note_id.toString(), owner])) {
+ const tsk = vetkd.TransportSecretKey.random();
+
+ const ek_bytes_hex = await this.actor.encrypted_symmetric_key_for_note(note_id, tsk.publicKeyBytes());
+ const encryptedVetKey = vetkd.EncryptedVetKey.deserialize(hex_decode(ek_bytes_hex));
+
+ const pk_bytes_hex = await this.actor.symmetric_key_verification_key_for_note();
+ const dpk = vetkd.DerivedPublicKey.deserialize(hex_decode(pk_bytes_hex));
+
+ const note_id_bytes: Uint8Array = bigintTo128BitBigEndianUint8Array(note_id);
+ const owner_utf8: Uint8Array = new TextEncoder().encode(owner);
+ let input = new Uint8Array(note_id_bytes.length + owner_utf8.length);
+ input.set(note_id_bytes);
+ input.set(owner_utf8, note_id_bytes.length);
+
+ const vetKey = encryptedVetKey.decryptAndVerify(tsk, dpk, input);
+
+ const note_key = await (await vetKey.asDerivedKeyMaterial()).deriveAesGcmCryptoKey("note-key");
+ await set([note_id.toString(), owner], note_key)
+ }
+ }
+}
+
+const hex_decode = (hexString) =>
+ Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
+const hex_encode = (bytes) =>
+ bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
+
+// Inspired by https://coolaj86.com/articles/convert-js-bigints-to-typedarrays/
+function bigintTo128BitBigEndianUint8Array(bn: bigint): Uint8Array {
+ var hex = BigInt(bn).toString(16);
+
+ // extend hex to length 32 = 16 bytes = 128 bits
+ while (hex.length < 32) {
+ hex = '0' + hex;
+ }
+
+ var len = hex.length / 2;
+ var u8 = new Uint8Array(len);
+
+ var i = 0;
+ var j = 0;
+ while (i < len) {
+ u8[i] = parseInt(hex.slice(j, j + 2), 16);
+ i += 1;
+ j += 2;
+ }
+
+ return u8;
+}
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/enums.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/enums.ts
new file mode 100644
index 000000000..daf2b6690
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/enums.ts
@@ -0,0 +1,8 @@
+export type KeysOfUnion = T extends T ? keyof T : never;
+
+export function enumIs(
+ p: EnumType,
+ key: KeysOfUnion
+): p is T {
+ return (key as string) in p;
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/note.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/note.ts
new file mode 100644
index 000000000..50a4d5959
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/note.ts
@@ -0,0 +1,106 @@
+import type { EncryptedNote } from '../lib/backend';
+import type { CryptoService } from './crypto';
+import type { Principal } from '@dfinity/principal';
+
+export interface NoteModel {
+ id: bigint;
+ title: string;
+ content: string;
+ createdAt: number;
+ updatedAt: number;
+ tags: Array;
+ owner: string;
+ users: Array;
+}
+
+type SerializableNoteModel = Omit;
+
+export function noteFromContent(content: string, tags: string[], self_principal: Principal): NoteModel {
+ const title = extractTitle(content);
+ const creationTime = Date.now();
+
+ return {
+ id: BigInt(0),
+ title,
+ content,
+ createdAt: creationTime,
+ updatedAt: creationTime,
+ tags,
+ owner: self_principal.toString(),
+ users: [""],
+ };
+}
+
+export async function serialize(
+ note: NoteModel,
+ cryptoService: CryptoService
+): Promise {
+ const serializableNote: SerializableNoteModel = {
+ title: note.title,
+ content: note.content,
+ createdAt: note.createdAt,
+ updatedAt: note.updatedAt,
+ tags: note.tags,
+ };
+ const encryptedNote = await cryptoService.encryptWithNoteKey(
+ note.id,
+ note.owner,
+ JSON.stringify(serializableNote)
+ );
+ return {
+ id: note.id,
+ encrypted_text: encryptedNote,
+ owner: note.owner,
+ users: note.users,
+ };
+}
+
+export async function deserialize(
+ enote: EncryptedNote,
+ cryptoService: CryptoService
+): Promise {
+ const serializedNote = await cryptoService.decryptWithNoteKey(enote.id, enote.owner, enote.encrypted_text);
+ const deserializedNote: SerializableNoteModel = JSON.parse(serializedNote);
+ return {
+ id: enote.id,
+ owner: enote.owner,
+ users: enote.users,
+ ...deserializedNote,
+ };
+}
+
+export function summarize(note: NoteModel, maxLength = 50) {
+ const div = document.createElement('div');
+ div.innerHTML = note.content;
+
+ let text = div.innerText;
+ const title = extractTitleFromDomEl(div);
+ if (title) {
+ text = text.replace(title, '');
+ }
+
+ return text.slice(0, maxLength) + (text.length > maxLength ? '...' : '');
+}
+
+function extractTitleFromDomEl(el: HTMLElement) {
+ const title = el.querySelector('h1');
+ if (title) {
+ return title.innerText;
+ }
+
+ const blockElements = el.querySelectorAll(
+ 'h1,h2,p,li'
+ ) as NodeListOf;
+ for (const el of blockElements) {
+ if (el.innerText?.trim().length > 0) {
+ return el.innerText.trim();
+ }
+ }
+ return '';
+}
+
+export function extractTitle(html: string) {
+ const div = document.createElement('div');
+ div.innerHTML = html;
+ return extractTitleFromDomEl(div);
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/sleep.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/sleep.ts
new file mode 100644
index 000000000..0d7f188e1
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/sleep.ts
@@ -0,0 +1,3 @@
+export function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/main.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/main.ts
new file mode 100644
index 000000000..3108e9002
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/main.ts
@@ -0,0 +1,9 @@
+import App from "./App.svelte";
+
+const init = async () => {
+ const app = new App({
+ target: document.body,
+ });
+};
+
+init();
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/auth.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/auth.ts
new file mode 100644
index 000000000..04c7540db
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/auth.ts
@@ -0,0 +1,140 @@
+import { get, writable } from "svelte/store";
+import { BackendActor, createActor } from "../lib/actor";
+import { AuthClient } from "@dfinity/auth-client";
+import { CryptoService } from "../lib/crypto";
+import { addNotification, showError } from "./notifications";
+import { sleep } from "../lib/sleep";
+import type { JsonnableDelegationChain } from "@dfinity/identity/lib/cjs/identity/delegation";
+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({
+ state: "initializing-auth",
+});
+
+async function initAuth() {
+ const client = await AuthClient.create();
+ if (await client.isAuthenticated()) {
+ authenticate(client);
+ } else {
+ auth.update(() => ({
+ state: "anonymous",
+ actor: createActor(),
+ client,
+ }));
+ }
+}
+
+initAuth();
+
+export function login() {
+ const currentAuth = get(auth);
+
+ if (currentAuth.state === "anonymous") {
+ currentAuth.client.login({
+ maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000),
+ identityProvider:
+ process.env.DFX_NETWORK === "ic"
+ ? "https://identity.ic0.app/#authorize"
+ : `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:8000/#authorize`,
+ onSuccess: () => authenticate(currentAuth.client),
+ });
+ }
+}
+
+export async function logout() {
+ const currentAuth = get(auth);
+
+ if (currentAuth.state === "initialized") {
+ await currentAuth.client.logout();
+ auth.update(() => ({
+ state: "anonymous",
+ actor: createActor(),
+ client: currentAuth.client,
+ }));
+ navigateTo("/");
+ }
+}
+
+export async function authenticate(client: AuthClient) {
+ handleSessionTimeout();
+
+ try {
+ const actor = createActor({
+ agentOptions: {
+ identity: client.getIdentity(),
+ },
+ });
+
+ auth.update(() => ({
+ state: "initializing-crypto",
+ actor,
+ client,
+ }));
+
+ const cryptoService = new CryptoService(actor);
+
+ auth.update(() => ({
+ state: "initialized",
+ actor,
+ client,
+ crypto: cryptoService,
+ }));
+ } catch (e) {
+ auth.update(() => ({
+ state: "error",
+ error: e.message || "An error occurred",
+ }));
+ }
+}
+
+// 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;
+
+ const expirationTimeMs =
+ Number.parseInt(delegation.delegations[0].delegation.expiration, 16) /
+ 1000000;
+
+ setTimeout(() => {
+ logout();
+ }, expirationTimeMs - Date.now());
+ } catch {
+ console.error("Could not handle delegation expiry.");
+ }
+ });
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/draft.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/draft.ts
new file mode 100644
index 000000000..7e89684d6
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/draft.ts
@@ -0,0 +1,31 @@
+import { writable } from 'svelte/store';
+import { auth } from './auth';
+
+interface DraftModel {
+ content: string;
+ tags: string[];
+}
+
+let initialDraft: DraftModel = {
+ content: '',
+ tags: [],
+};
+
+try {
+ const savedDraft: DraftModel = JSON.parse(localStorage.getItem('draft'));
+ if ('content' in savedDraft && 'tags' in savedDraft) {
+ initialDraft = savedDraft;
+ }
+} catch {}
+
+export const draft = writable(initialDraft);
+
+draft.subscribe((draft) => {
+ localStorage.setItem('draft', JSON.stringify(draft));
+});
+
+auth.subscribe(($auth) => {
+ if ($auth.state === 'anonymous') {
+ draft.set(initialDraft);
+ }
+});
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notes.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notes.ts
new file mode 100644
index 000000000..9c2995342
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notes.ts
@@ -0,0 +1,132 @@
+import { writable } from 'svelte/store';
+import type { BackendActor } from '../lib/actor';
+import type { EncryptedNote } from '../lib/backend';
+import type { CryptoService } from '../lib/crypto';
+import { deserialize, NoteModel, serialize } from '../lib/note';
+import { auth } from './auth';
+import { showError } from './notifications';
+
+export const notesStore = writable<
+ | {
+ state: 'uninitialized';
+ }
+ | {
+ state: 'loading';
+ }
+ | {
+ state: 'loaded';
+ list: NoteModel[];
+ }
+ | {
+ state: 'error';
+ }
+>({ state: 'uninitialized' });
+
+let notePollerHandle: ReturnType | null;
+
+async function decryptNotes(
+ notes: EncryptedNote[],
+ cryptoService: CryptoService
+): Promise {
+ // When notes are initially created, they do not have (and cannot have) any
+ // (encrypted) content yet because the note ID, which is needed to retrieve
+ // the note-specific encryption key, is not known yet before the note is
+ // created because the note ID is a return value of the call to create a note.
+ // The (encrypted) note content is stored in the backend only by a second call
+ // to the backend that updates the note's conent directly after the note is
+ // created. This means that there is a short period of time where the note
+ // already exists but doesn't have any (encrypted) content yet.
+ // To avoid decryption errors for these notes, we skip deserializing (and thus
+ // decrypting) these notes here.
+ const notes_with_content = notes.filter((note) => note.encrypted_text != "");
+
+ return await Promise.all(
+ notes_with_content.map((encryptedNote) => deserialize(encryptedNote, cryptoService))
+ );
+}
+
+function updateNotes(notes: NoteModel[]) {
+ notesStore.set({
+ state: 'loaded',
+ list: notes,
+ });
+}
+
+export async function refreshNotes(
+ actor: BackendActor,
+ cryptoService: CryptoService
+) {
+ const encryptedNotes = await actor.get_notes();
+
+ const notes = await decryptNotes(encryptedNotes, cryptoService);
+ updateNotes(notes);
+}
+
+export async function addNote(
+ note: NoteModel,
+ actor: BackendActor,
+ crypto: CryptoService
+) {
+ const new_id: bigint = await actor.create_note();
+ note.id = new_id;
+ const encryptedNote = (await serialize(note, crypto)).encrypted_text;
+ await actor.update_note(new_id, encryptedNote);
+}
+export async function updateNote(
+ note: NoteModel,
+ actor: BackendActor,
+ crypto: CryptoService
+) {
+ const encryptedNote = await serialize(note, crypto);
+ await actor.update_note(note.id, encryptedNote.encrypted_text);
+}
+
+export async function addUser(
+ id: bigint,
+ user: string,
+ actor: BackendActor,
+) {
+ await actor.add_user(id, user);
+}
+
+export async function removeUser(
+ id: bigint,
+ user: string,
+ actor: BackendActor,
+) {
+ await actor.remove_user(id, user);
+}
+
+auth.subscribe(async ($auth) => {
+ if ($auth.state === 'initialized') {
+ if (notePollerHandle !== null) {
+ clearInterval(notePollerHandle);
+ notePollerHandle = null;
+ }
+
+ notesStore.set({
+ state: 'loading',
+ });
+ try {
+ await refreshNotes($auth.actor, $auth.crypto).catch((e) =>
+ showError(e, 'Could not poll notes.')
+ );
+
+ notePollerHandle = setInterval(async () => {
+ await refreshNotes($auth.actor, $auth.crypto).catch((e) =>
+ showError(e, 'Could not poll notes.')
+ );
+ }, 3000);
+ } catch {
+ notesStore.set({
+ state: 'error',
+ });
+ }
+ } else if ($auth.state === 'anonymous' && notePollerHandle !== null) {
+ clearInterval(notePollerHandle);
+ notePollerHandle = null;
+ notesStore.set({
+ state: 'uninitialized',
+ });
+ }
+});
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notifications.ts b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notifications.ts
new file mode 100644
index 000000000..6b0aa0cad
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notifications.ts
@@ -0,0 +1,30 @@
+import { writable } from 'svelte/store';
+
+export interface Notification {
+ type: 'error' | 'info' | 'success';
+ message: string;
+ id: number;
+}
+
+export type NewNotification = Omit;
+
+let nextId = 0;
+
+export const notifications = writable([]);
+
+export function addNotification(notification: NewNotification, timeout = 2000) {
+ const id = nextId++;
+
+ notifications.update(($n) => [...$n, { ...notification, id }]);
+
+ setTimeout(() => {
+ notifications.update(($n) => $n.filter((n) => n.id != id));
+ }, timeout);
+}
+
+export function showError(e: any, message: string): never {
+ addNotification({ type: 'error', message });
+ console.error(e);
+ console.error(e.stack);
+ throw e;
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tailwind.config.js b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tailwind.config.js
new file mode 100644
index 000000000..e88d06d07
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tailwind.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ content: [
+ './public/index.html',
+ './src/**/*.svelte',
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [require('daisyui'), require('@tailwindcss/line-clamp')],
+};
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tsconfig.json b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tsconfig.json
new file mode 100644
index 000000000..6bfdec1e8
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+ "include": ["src/**/*"],
+ "exclude": ["node_modules/*", "__sapper__/*", "public/*", "src/declarations/**/*"]
+}
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/main.mo b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/main.mo
new file mode 100644
index 000000000..52b99f6e2
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/main.mo
@@ -0,0 +1,410 @@
+import Map "mo:base/HashMap";
+import Text "mo:base/Text";
+import Array "mo:base/Array";
+import Buffer "mo:base/Buffer";
+import List "mo:base/List";
+import Iter "mo:base/Iter";
+import Nat "mo:base/Nat";
+import Nat8 "mo:base/Nat8";
+import Bool "mo:base/Bool";
+import Principal "mo:base/Principal";
+import Option "mo:base/Option";
+import Debug "mo:base/Debug";
+import Blob "mo:base/Blob";
+import Hash "mo:base/Hash";
+import Hex "./utils/Hex";
+
+// Declare a shared actor class
+// Bind the caller and the initializer
+shared ({ caller = initializer }) persistent actor class (keyName: Text) {
+
+ // Currently, a single canister smart contract is limited to 4 GB of heap size.
+ // For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits.
+ // To ensure that our canister does not exceed the limit, we put various restrictions (e.g., max number of users) in place.
+ // This should keep us well below a memory usage of 2 GB because
+ // up to 2x memory may be needed for data serialization during canister upgrades.
+ // This is sufficient for this proof-of-concept, but in a production environment the actual
+ // memory usage must be calculated or monitored and the various restrictions adapted accordingly.
+
+ // Define dapp limits - important for security assurance
+ private transient let MAX_USERS = 500;
+ private transient let MAX_NOTES_PER_USER = 200;
+ private transient let MAX_NOTE_CHARS = 1000;
+ private transient let MAX_SHARES_PER_NOTE = 50;
+
+ private type PrincipalName = Text;
+ private type NoteId = Nat;
+
+ // Define public types
+ // Type of an encrypted note
+ // Attention: This canister does *not* perform any encryption.
+ // Here we assume that the notes are encrypted end-
+ // to-end by the front-end (at client side).
+ public type EncryptedNote = {
+ encrypted_text : Text;
+ id : Nat;
+ owner : PrincipalName;
+ // Principals with whom this note is shared. Does not include the owner.
+ // Needed to be able to efficiently show in the UI with whom this note is shared.
+ users : [PrincipalName];
+ };
+
+ // Define private fields
+ // Stable actor fields are automatically retained across canister upgrades.
+ // See https://internetcomputer.org/docs/current/motoko/main/upgrades/
+
+ // Design choice: Use globally unique note identifiers for all users.
+ //
+ // The keyword `stable` makes this (scalar) variable keep its value across canister upgrades.
+ //
+ // See https://internetcomputer.org/docs/current/developer-docs/setup/manage-canisters#upgrade-a-canister
+ private var nextNoteId : Nat = 1;
+
+ // Store notes by their ID, so that note-specific encryption keys can be derived.
+ private transient var notesById = Map.HashMap(0, Nat.equal, Hash.hash);
+ // Store which note IDs are owned by a particular principal
+ private transient var noteIdsByOwner = Map.HashMap>(0, Text.equal, Text.hash);
+ // Store which notes are shared with a particular principal. Does not include the owner, as this is tracked by `noteIdsByOwner`.
+ private transient var noteIdsByUser = Map.HashMap>(0, Text.equal, Text.hash);
+
+ // While accessing _heap_ data is more efficient, we use the following _stable memory_
+ // as a buffer to preserve data across canister upgrades.
+ // Stable memory is currently 96GB. For the current limits see
+ // https://internetcomputer.org/docs/current/developer-docs/production/resource-limits.
+ // See also: [preupgrade], [postupgrade]
+ private var stable_notesById : [(NoteId, EncryptedNote)] = [];
+ private var stable_noteIdsByOwner : [(PrincipalName, List.List)] = [];
+ private var stable_noteIdsByUser : [(PrincipalName, List.List)] = [];
+
+ // Utility function that helps writing assertion-driven code more concisely.
+ private func expect(opt : ?T, violation_msg : Text) : T {
+ switch (opt) {
+ case (null) {
+ Debug.trap(violation_msg);
+ };
+ case (?x) {
+ x;
+ };
+ };
+ };
+
+ private func is_authorized(user : PrincipalName, note : EncryptedNote) : Bool {
+ user == note.owner or Option.isSome(Array.find(note.users, func(x : PrincipalName) : Bool { x == user }));
+ };
+
+ public shared ({ caller }) func whoami() : async Text {
+ return Principal.toText(caller);
+ };
+
+ // Shared functions, i.e., those specified with [shared], are
+ // accessible to remote callers.
+ // The extra parameter [caller] is the caller's principal
+ // See https://internetcomputer.org/docs/current/motoko/main/actors-async
+
+ // Add new empty note for this [caller].
+ //
+ // Returns:
+ // Future of ID of new empty note
+ // Traps:
+ // [caller] is the anonymous identity
+ // [caller] already has [MAX_NOTES_PER_USER] notes
+ // This is the first note for [caller] and [MAX_USERS] is exceeded
+ public shared ({ caller }) func create_note() : async NoteId {
+ assert not Principal.isAnonymous(caller);
+ let owner = Principal.toText(caller);
+
+ let newNote : EncryptedNote = {
+ id = nextNoteId;
+ encrypted_text = "";
+ owner = owner;
+ users = [];
+ };
+
+ switch (noteIdsByOwner.get(owner)) {
+ case (?owner_nids) {
+ assert List.size(owner_nids) < MAX_NOTES_PER_USER;
+ noteIdsByOwner.put(owner, List.push(newNote.id, owner_nids));
+ };
+ case null {
+ assert noteIdsByOwner.size() < MAX_USERS;
+ noteIdsByOwner.put(owner, List.make(newNote.id));
+ };
+ };
+
+ notesById.put(newNote.id, newNote);
+ nextNoteId += 1;
+ newNote.id;
+ };
+
+ // Returns (a future of) this [caller]'s notes.
+ //
+ // --- Queries vs. Updates ---
+ // Note that this method is declared as an *update* call (see `shared`) rather than *query*.
+ //
+ // While queries are significantly faster than updates, they are not certified by the IC.
+ // Thus, we avoid using queries throughout this dapp, ensuring that the result of our
+ // functions gets through consensus. Otherwise, this function could e.g. omit some notes
+ // if it got executed by a malicious node. (To make the dapp more efficient, one could
+ // use an approach in which both queries and updates are combined.)
+ // See https://internetcomputer.org/docs/current/concepts/canisters-code#query-and-update-methods
+ //
+ // Returns:
+ // Future of array of EncryptedNote
+ // Traps:
+ // [caller] is the anonymous identity
+ public shared ({ caller }) func get_notes() : async [EncryptedNote] {
+ assert not Principal.isAnonymous(caller);
+ let user = Principal.toText(caller);
+
+ let owned_notes = List.map(
+ Option.get(noteIdsByOwner.get(user), List.nil()),
+ func(nid : NoteId) : EncryptedNote {
+ expect(notesById.get(nid), "missing note with ID " # Nat.toText(nid));
+ },
+ );
+ let shared_notes = List.map(
+ Option.get(noteIdsByUser.get(user), List.nil()),
+ func(nid : NoteId) : EncryptedNote {
+ expect(notesById.get(nid), "missing note with ID " # Nat.toText(nid));
+ },
+ );
+
+ let buf = Buffer.Buffer(List.size(owned_notes) + List.size(shared_notes));
+ buf.append(Buffer.fromArray(List.toArray(owned_notes)));
+ buf.append(Buffer.fromArray(List.toArray(shared_notes)));
+ Buffer.toArray(buf);
+ };
+
+ // Replaces the encrypted text of note with ID [id] with [encrypted_text].
+ //
+ // Returns:
+ // Future of unit
+ // Traps:
+ // [caller] is the anonymous identity
+ // note with ID [id] does not exist
+ // [caller] is not the note's owner and not a user with whom the note is shared
+ // [encrypted_text] exceeds [MAX_NOTE_CHARS]
+ public shared ({ caller }) func update_note(id : NoteId, encrypted_text : Text) : async () {
+ assert not Principal.isAnonymous(caller);
+ let caller_text = Principal.toText(caller);
+ let (?note_to_update) = notesById.get(id) else Debug.trap("note with id " # Nat.toText(id) # "not found");
+ if (not is_authorized(caller_text, note_to_update)) {
+ Debug.trap("unauthorized");
+ };
+ assert note_to_update.encrypted_text.size() <= MAX_NOTE_CHARS;
+ notesById.put(id, { note_to_update with encrypted_text });
+ };
+
+ // Shares the note with ID [note_id] with the [user].
+ // Has no effect if the note is already shared with that user.
+ //
+ // Returns:
+ // Future of unit
+ // Traps:
+ // [caller] is the anonymous identity
+ // note with ID [id] does not exist
+ // [caller] is not the note's owner
+ public shared ({ caller }) func add_user(note_id : NoteId, user : PrincipalName) : async () {
+ assert not Principal.isAnonymous(caller);
+ let caller_text = Principal.toText(caller);
+ let (?note) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found");
+ if (caller_text != note.owner) {
+ Debug.trap("unauthorized");
+ };
+ assert note.users.size() < MAX_SHARES_PER_NOTE;
+ if (not Option.isSome(Array.find(note.users, func(u : PrincipalName) : Bool { u == user }))) {
+ let users_buf = Buffer.fromArray(note.users);
+ users_buf.add(user);
+ let updated_note = { note with users = Buffer.toArray(users_buf) };
+ notesById.put(note_id, updated_note);
+ };
+ switch (noteIdsByUser.get(user)) {
+ case (?user_nids) {
+ if (not List.some(user_nids, func(nid : NoteId) : Bool { nid == note_id })) {
+ noteIdsByUser.put(user, List.push(note_id, user_nids));
+ };
+ };
+ case null {
+ noteIdsByUser.put(user, List.make(note_id));
+ };
+ };
+ };
+
+ // Unshares the note with ID [note_id] with the [user].
+ // Has no effect if the note is already shared with that user.
+ //
+ // Returns:
+ // Future of unit
+ // Traps:
+ // [caller] is the anonymous identity
+ // note with ID [id] does not exist
+ // [caller] is not the note's owner
+ public shared ({ caller }) func remove_user(note_id : NoteId, user : PrincipalName) : async () {
+ assert not Principal.isAnonymous(caller);
+ let caller_text = Principal.toText(caller);
+ let (?note) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found");
+ if (caller_text != note.owner) {
+ Debug.trap("unauthorized");
+ };
+ let users_buf = Buffer.fromArray(note.users);
+ users_buf.filterEntries(func(i : Nat, u : PrincipalName) : Bool { u != user });
+ let updated_note = { note with users = Buffer.toArray(users_buf) };
+ notesById.put(note_id, updated_note);
+
+ switch (noteIdsByUser.get(user)) {
+ case (?user_nids) {
+ let updated_nids = List.filter(user_nids, func(nid : NoteId) : Bool { nid != note_id });
+ if (not List.isNil(updated_nids)) {
+ noteIdsByUser.put(user, updated_nids);
+ } else {
+ let _ = noteIdsByUser.remove(user);
+ };
+ };
+ case null {};
+ };
+ };
+
+ // Delete the note with ID [id].
+ //
+ // Returns:
+ // Future of unit
+ // Traps:
+ // [caller] is the anonymous identity
+ // note with ID [id] does not exist
+ // [caller] is not the note's owner
+ public shared ({ caller }) func delete_note(note_id : NoteId) : async () {
+ assert not Principal.isAnonymous(caller);
+ let caller_text = Principal.toText(caller);
+ let (?note_to_delete) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found");
+ let owner = note_to_delete.owner;
+ if (owner != caller_text) {
+ Debug.trap("unauthorized");
+ };
+ switch (noteIdsByOwner.get(owner)) {
+ case (?owner_nids) {
+ let updated_nids = List.filter(owner_nids, func(nid : NoteId) : Bool { nid != note_id });
+ if (not List.isNil(updated_nids)) {
+ noteIdsByOwner.put(owner, updated_nids);
+ } else {
+ let _ = noteIdsByOwner.remove(owner);
+ };
+ };
+ case null {};
+ };
+ for (user in note_to_delete.users.vals()) {
+ switch (noteIdsByUser.get(user)) {
+ case (?user_nids) {
+ let updated_nids = List.filter(user_nids, func(nid : NoteId) : Bool { nid != note_id });
+ if (not List.isNil(updated_nids)) {
+ noteIdsByUser.put(user, updated_nids);
+ } else {
+ let _ = noteIdsByUser.remove(user);
+ };
+ };
+ case null {};
+ };
+ };
+ let _ = notesById.remove(note_id);
+ };
+
+ // Only the vetKD methods in the IC management canister are required here.
+ type VETKD_API = actor {
+ vetkd_public_key : ({
+ canister_id : ?Principal;
+ context : Blob;
+ key_id : { curve : { #bls12_381_g2 }; name : Text };
+ }) -> async ({ public_key : Blob });
+ vetkd_derive_key : ({
+ input : Blob;
+ context : Blob;
+ key_id : { curve : { #bls12_381_g2 }; name : Text };
+ transport_public_key : Blob;
+ }) -> async ({ encrypted_key : Blob });
+ };
+
+ transient let management_canister : VETKD_API = actor ("aaaaa-aa");
+
+ public shared func symmetric_key_verification_key_for_note() : async Text {
+ let { public_key } = await management_canister.vetkd_public_key({
+ canister_id = null;
+ context = Text.encodeUtf8("note_symmetric_key");
+ key_id = { curve = #bls12_381_g2; name = keyName };
+ });
+ Hex.encode(Blob.toArray(public_key));
+ };
+
+ public shared ({ caller }) func encrypted_symmetric_key_for_note(note_id : NoteId, transport_public_key : Blob) : async Text {
+ let caller_text = Principal.toText(caller);
+ let (?note) = notesById.get(note_id) else Debug.trap("note with id " # Nat.toText(note_id) # "not found");
+ if (not is_authorized(caller_text, note)) {
+ Debug.trap("unauthorized");
+ };
+
+ let buf = Buffer.Buffer(32);
+ buf.append(Buffer.fromArray(natToBigEndianByteArray(16, note_id))); // fixed-size encoding
+ buf.append(Buffer.fromArray(Blob.toArray(Text.encodeUtf8(note.owner))));
+ let input = Blob.fromArray(Buffer.toArray(buf)); // prefix-free
+
+ let { encrypted_key } = await (with cycles = 26_153_846_153) management_canister.vetkd_derive_key({
+ input;
+ context = Text.encodeUtf8("note_symmetric_key");
+ key_id = { curve = #bls12_381_g2; name = keyName };
+ transport_public_key;
+ });
+ Hex.encode(Blob.toArray(encrypted_key));
+ };
+
+ // Converts a nat to a fixed-size big-endian byte (Nat8) array
+ private func natToBigEndianByteArray(len : Nat, n : Nat) : [Nat8] {
+ let ith_byte = func(i : Nat) : Nat8 {
+ assert (i < len);
+ let shift : Nat = 8 * (len - 1 - i);
+ Nat8.fromIntWrap(n / 2 ** shift);
+ };
+ Array.tabulate(len, ith_byte);
+ };
+
+ // Below, we implement the upgrade hooks for our canister.
+ // See https://internetcomputer.org/docs/current/motoko/main/upgrades/
+
+ // The work required before a canister upgrade begins.
+ system func preupgrade() {
+ Debug.print("Starting pre-upgrade hook...");
+ stable_notesById := Iter.toArray(notesById.entries());
+ stable_noteIdsByOwner := Iter.toArray(noteIdsByOwner.entries());
+ stable_noteIdsByUser := Iter.toArray(noteIdsByUser.entries());
+ Debug.print("pre-upgrade finished.");
+ };
+
+ // The work required after a canister upgrade ends.
+ // See [nextNoteId], [stable_notesByUser]
+ system func postupgrade() {
+ Debug.print("Starting post-upgrade hook...");
+
+ notesById := Map.fromIter(
+ stable_notesById.vals(),
+ stable_notesById.size(),
+ Nat.equal,
+ Hash.hash,
+ );
+ stable_notesById := [];
+
+ noteIdsByOwner := Map.fromIter>(
+ stable_noteIdsByOwner.vals(),
+ stable_noteIdsByOwner.size(),
+ Text.equal,
+ Text.hash,
+ );
+ stable_noteIdsByOwner := [];
+
+ noteIdsByUser := Map.fromIter>(
+ stable_noteIdsByUser.vals(),
+ stable_noteIdsByUser.size(),
+ Text.equal,
+ Text.hash,
+ );
+ stable_noteIdsByUser := [];
+
+ Debug.print("post-upgrade finished.");
+ };
+};
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/utils/Hex.mo b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/utils/Hex.mo
new file mode 100644
index 000000000..310b72d6d
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/utils/Hex.mo
@@ -0,0 +1,105 @@
+/**
+ * Module : Hex.mo
+ * Description : Hexadecimal encoding and decoding routines.
+ * Copyright : 2022 Dfinity
+ * License : Apache 2.0>
+ */
+
+import Array "mo:base/Array";
+import Iter "mo:base/Iter";
+import Option "mo:base/Option";
+import Nat8 "mo:base/Nat8";
+import Char "mo:base/Char";
+import Result "mo:base/Result";
+import Text "mo:base/Text";
+import Prim "mo:⛔";
+
+module {
+
+ private type Result = Result.Result;
+
+ private let base : Nat8 = 0x10;
+
+ private let symbols = [
+ '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
+ ];
+
+ /**
+ * Define a type to indicate that the decoder has failed.
+ */
+ public type DecodeError = {
+ #msg : Text;
+ };
+
+ /**
+ * Encode an array of unsigned 8-bit integers in hexadecimal format.
+ */
+ public func encode(array : [Nat8]) : Text {
+ let encoded = Array.foldLeft(array, "", func (accum, w8) {
+ accum # encodeW8(w8);
+ });
+ // encode as lowercase
+ return Text.map(encoded, Prim.charToLower);
+ };
+
+ /**
+ * Encode an unsigned 8-bit integer in hexadecimal format.
+ */
+ private func encodeW8(w8 : Nat8) : Text {
+ let c1 = symbols[Nat8.toNat(w8 / base)];
+ let c2 = symbols[Nat8.toNat(w8 % base)];
+ Char.toText(c1) # Char.toText(c2);
+ };
+
+ /**
+ * Decode an array of unsigned 8-bit integers in hexadecimal format.
+ */
+ public func decode(text : Text) : Result<[Nat8], DecodeError> {
+ // Transform to uppercase for uniform decoding
+ let upper = Text.map(text, Prim.charToUpper);
+ let next = upper.chars().next;
+ func parse() : Result {
+ Option.get>(
+ do ? {
+ let c1 = next()!;
+ let c2 = next()!;
+ Result.chain(decodeW4(c1), func (x1) {
+ Result.chain(decodeW4(c2), func (x2) {
+ #ok (x1 * base + x2);
+ })
+ })
+ },
+ #err (#msg "Not enough input!"),
+ );
+ };
+ var i = 0;
+ let n = upper.size() / 2 + upper.size() % 2;
+ let array = Array.init(n, 0);
+ while (i != n) {
+ switch (parse()) {
+ case (#ok w8) {
+ array[i] := w8;
+ i += 1;
+ };
+ case (#err err) {
+ return #err err;
+ };
+ };
+ };
+ #ok (Array.freeze(array));
+ };
+
+ /**
+ * Decode an unsigned 4-bit integer in hexadecimal format.
+ */
+ private func decodeW4(char : Char) : Result {
+ for (i in Iter.range(0, 15)) {
+ if (symbols[i] == char) {
+ return #ok (Nat8.fromNat(i));
+ };
+ };
+ let str = "Unexpected character: " # Char.toText(char);
+ #err (#msg str);
+ };
+};
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/dfx.json b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/dfx.json
new file mode 100644
index 000000000..61d6507e6
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/dfx.json
@@ -0,0 +1,39 @@
+{
+ "canisters": {
+ "encrypted_notes": {
+ "main": "backend/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": ["encrypted_notes", "internet-identity"],
+ "build": [
+ "cd frontend && npm i --include=dev && npm run build && cd - && rm -r public > /dev/null 2>&1; cp -r frontend/public ./"
+ ],
+ "frontend": {
+ "entrypoint": "public/index.html"
+ },
+ "source": ["public/"],
+ "type": "assets",
+ "output_env_file": "frontend/.env"
+ }
+ },
+ "networks": {
+ "local": {
+ "bind": "127.0.0.1:8000",
+ "type": "ephemeral"
+ }
+ }
+ }
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/frontend b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/frontend
new file mode 120000
index 000000000..af288785f
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/frontend
@@ -0,0 +1 @@
+../frontend
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/Cargo.toml b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/Cargo.toml
new file mode 100644
index 000000000..ee7bb5992
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/Cargo.toml
@@ -0,0 +1,9 @@
+[workspace]
+members = [
+ 'backend',
+]
+
+[profile.release]
+lto = true
+opt-level = 'z'
+panic = 'abort'
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/Cargo.toml b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/Cargo.toml
new file mode 100644
index 000000000..25355a7fa
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "encrypted_notes_backend"
+version = "0.1.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+candid = "0.10"
+ic-cdk = "0.18.3"
+ic-stable-structures = "0.6.4"
+lazy_static = "1.4.0"
+serde_json = "1.0.108"
+anyhow = "1.0.75"
+serde = "1"
+ic-types = "0.7.0"
+hex = "0.4.3"
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/encrypted_notes_rust.did b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/encrypted_notes_rust.did
new file mode 100644
index 000000000..a590d79e4
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/encrypted_notes_rust.did
@@ -0,0 +1,17 @@
+type EncryptedNote = record {
+ id : nat;
+ encrypted_text : text;
+ owner : text;
+ users : vec text;
+};
+service : (text) -> {
+ add_user : (nat, text) -> ();
+ create_note : () -> (nat);
+ delete_note : (nat) -> ();
+ encrypted_symmetric_key_for_note : (nat, blob) -> (text);
+ get_notes : () -> (vec EncryptedNote);
+ remove_user : (nat, text) -> ();
+ symmetric_key_verification_key_for_note : () -> (text);
+ update_note : (nat, text) -> ();
+ whoami : () -> (text);
+}
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/lib.rs b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/lib.rs
new file mode 100644
index 000000000..4ea54bd55
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/lib.rs
@@ -0,0 +1,433 @@
+use candid::{CandidType, Decode, Deserialize, Encode, Principal};
+use ic_cdk::api::msg_caller;
+use ic_cdk::init;
+use ic_cdk::update;
+use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
+use ic_stable_structures::{
+ storable::Bound, DefaultMemoryImpl, StableBTreeMap, StableCell, Storable,
+};
+use std::borrow::Cow;
+use std::cell::RefCell;
+
+type PrincipalName = String;
+type Memory = VirtualMemory;
+type NoteId = u128;
+
+#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)]
+pub struct EncryptedNote {
+ id: NoteId,
+ encrypted_text: String,
+ owner: PrincipalName,
+ /// Principals with whom this note is shared. Does not include the owner.
+ /// Needed to be able to efficiently show in the UI with whom this note is shared.
+ users: Vec,
+}
+
+impl EncryptedNote {
+ pub fn is_authorized(&self, user: &PrincipalName) -> bool {
+ user == &self.owner || self.users.contains(user)
+ }
+}
+
+impl Storable for EncryptedNote {
+ fn to_bytes(&self) -> Cow<[u8]> {
+ Cow::Owned(Encode!(self).unwrap())
+ }
+ fn from_bytes(bytes: Cow<[u8]>) -> Self {
+ Decode!(bytes.as_ref(), Self).unwrap()
+ }
+ const BOUND: Bound = Bound::Unbounded;
+}
+
+#[derive(CandidType, Deserialize, Default)]
+pub struct NoteIds {
+ ids: Vec,
+}
+
+impl NoteIds {
+ pub fn iter(&self) -> impl std::iter::Iterator- {
+ self.ids.iter()
+ }
+}
+
+impl Storable for NoteIds {
+ fn to_bytes(&self) -> Cow<[u8]> {
+ Cow::Owned(Encode!(self).unwrap())
+ }
+ fn from_bytes(bytes: Cow<[u8]>) -> Self {
+ Decode!(bytes.as_ref(), Self).unwrap()
+ }
+ const BOUND: Bound = Bound::Unbounded;
+}
+
+// We use a canister's stable memory as storage. This simplifies the code and makes the appliation
+// more robust because no (potentially failing) pre_upgrade/post_upgrade hooks are needed.
+// Note that stable memory is less performant than heap memory, however.
+// Currently, a single canister smart contract is limited to 96 GB of stable memory.
+// For the current limits see https://internetcomputer.org/docs/current/developer-docs/production/resource-limits.
+// To ensure that our canister does not exceed the limit, we put various restrictions (e.g., number of users) in place.
+static MAX_USERS: u64 = 1_000;
+static MAX_NOTES_PER_USER: usize = 500;
+static MAX_NOTE_CHARS: usize = 1000;
+static MAX_SHARES_PER_NOTE: usize = 50;
+
+thread_local! {
+ static MEMORY_MANAGER: RefCell
> =
+ RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
+
+ static NEXT_NOTE_ID: RefCell> = RefCell::new(
+ StableCell::init(
+ MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(0))),
+ 1
+ ).expect("failed to init NEXT_NOTE_ID")
+ );
+
+ static NOTES: RefCell> = RefCell::new(
+ StableBTreeMap::init(
+ MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(1))),
+ )
+ );
+
+ static NOTE_OWNERS: RefCell> = RefCell::new(
+ StableBTreeMap::init(
+ MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(2))),
+ )
+ );
+
+ static NOTE_SHARES: RefCell> = RefCell::new(
+ StableBTreeMap::init(
+ MEMORY_MANAGER.with_borrow(|m| m.get(MemoryId::new(3))),
+ )
+ );
+ static KEY_NAME: RefCell> =
+ RefCell::new(StableCell::init(
+ MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))),
+ String::new(),
+ )
+ .expect("failed to initialize key name"));
+}
+
+#[init]
+fn init(key_name_string: String) {
+ KEY_NAME.with_borrow_mut(|key_name| {
+ key_name
+ .set(key_name_string)
+ .expect("failed to set key name");
+ });
+}
+
+/// Unlike Motoko, the caller identity is not built into Rust.
+/// Thus, we use the ic_cdk::caller() method inside this wrapper function.
+/// The wrapper prevents the use of the anonymous identity. Forbidding anonymous
+/// interactions is the recommended default behavior for IC canisters.
+fn caller() -> Principal {
+ let caller = msg_caller();
+ // The anonymous principal is not allowed to interact with the
+ // encrypted notes canister.
+ if caller == Principal::anonymous() {
+ panic!("Anonymous principal not allowed to make calls.")
+ }
+ caller
+}
+
+// --- Queries vs. Updates ---
+//
+// Note that our public methods are declared as an *updates* rather than *queries*, e.g.:
+// #[update(name = "notesCnt")] ...
+// rather than
+// #[query(name = "notesCnt")] ...
+//
+// While queries are significantly faster than updates, they are not certified by the IC.
+// Thus, we avoid using queries throughout this dapp, ensuring that the result of our
+// methods gets through consensus. Otherwise, this method could e.g. omit some notes
+// if it got executed by a malicious node. (To make the dapp more efficient, one could
+// use an approach in which both queries and updates are combined.)
+//
+// See https://internetcomputer.org/docs/current/concepts/canisters-code#query-and-update-methods
+
+/// Reflects the [caller]'s identity by returning (a future of) its principal.
+/// Useful for debugging.
+#[update]
+fn whoami() -> String {
+ msg_caller().to_string()
+}
+
+// General assumptions
+// -------------------
+// All the functions of this canister's public API should be available only to
+// registered users, with the exception of [whoami].
+
+/// Returns (a future of) this [caller]'s notes.
+/// Panics:
+/// [caller] is the anonymous identity
+#[update]
+fn get_notes() -> Vec {
+ let user_str = caller().to_string();
+ NOTES.with_borrow(|notes| {
+ let owned = NOTE_OWNERS.with_borrow(|ids| {
+ ids.get(&user_str)
+ .unwrap_or_default()
+ .iter()
+ .map(|id| notes.get(id).ok_or(format!("missing note with ID {id}")))
+ .collect::, _>>()
+ .unwrap_or_else(|err| ic_cdk::trap(&err))
+ });
+ let shared = NOTE_SHARES.with_borrow(|ids| {
+ ids.get(&user_str)
+ .unwrap_or_default()
+ .iter()
+ .map(|id| notes.get(id).ok_or(format!("missing note with ID {id}")))
+ .collect::, _>>()
+ .unwrap_or_else(|err| ic_cdk::trap(&err))
+ });
+ let mut result = Vec::with_capacity(owned.len() + shared.len());
+ result.extend(owned);
+ result.extend(shared);
+ result
+ })
+}
+
+/// Delete this [caller]'s note with given id. If none of the
+/// existing notes have this id, do nothing.
+/// [id]: the id of the note to be deleted
+///
+/// Returns:
+/// Future of unit
+/// Panics:
+/// [caller] is the anonymous identity
+/// [caller] is not the owner of note with id `note_id`
+#[update]
+fn delete_note(note_id: u128) {
+ let user_str = caller().to_string();
+ NOTES.with_borrow_mut(|notes| {
+ if let Some(note_to_delete) = notes.get(¬e_id) {
+ let owner = ¬e_to_delete.owner;
+ if owner != &user_str {
+ ic_cdk::trap("only the owner can delete notes");
+ }
+ NOTE_OWNERS.with_borrow_mut(|owner_to_nids| {
+ if let Some(mut owner_ids) = owner_to_nids.get(owner) {
+ owner_ids.ids.retain(|&id| id != note_id);
+ if !owner_ids.ids.is_empty() {
+ owner_to_nids.insert(owner.clone(), owner_ids);
+ } else {
+ owner_to_nids.remove(owner);
+ }
+ }
+ });
+ NOTE_SHARES.with_borrow_mut(|share_to_nids| {
+ for share in note_to_delete.users {
+ if let Some(mut share_ids) = share_to_nids.get(&share) {
+ share_ids.ids.retain(|&id| id != note_id);
+ if !share_ids.ids.is_empty() {
+ share_to_nids.insert(share, share_ids);
+ } else {
+ share_to_nids.remove(&share);
+ }
+ }
+ }
+ });
+ notes.remove(¬e_id);
+ }
+ });
+}
+
+/// Replaces the encrypted text of note with ID [id] with [encrypted_text].
+///
+/// Panics:
+/// [caller] is the anonymous identity
+/// [caller] is not the note's owner and not a user with whom the note is shared
+/// [encrypted_text] exceeds [MAX_NOTE_CHARS]
+#[update]
+fn update_note(id: NoteId, encrypted_text: String) {
+ let user_str = caller().to_string();
+
+ NOTES.with_borrow_mut(|notes| {
+ if let Some(mut note_to_update) = notes.get(&id) {
+ if !note_to_update.is_authorized(&user_str) {
+ ic_cdk::trap("unauthorized update");
+ }
+ assert!(encrypted_text.chars().count() <= MAX_NOTE_CHARS);
+ note_to_update.encrypted_text = encrypted_text;
+ notes.insert(id, note_to_update);
+ }
+ })
+}
+
+/// Add new empty note for this [caller].
+///
+/// Returns:
+/// Future of ID of new empty note
+/// Panics:
+/// [caller] is the anonymous identity
+/// User already has [MAX_NOTES_PER_USER] notes
+/// This is the first note for [caller] and [MAX_USERS] is exceeded
+#[update]
+fn create_note() -> NoteId {
+ let owner = caller().to_string();
+
+ NOTES.with_borrow_mut(|id_to_note| {
+ NOTE_OWNERS.with_borrow_mut(|owner_to_nids| {
+ let next_note_id = NEXT_NOTE_ID.with_borrow(|id| *id.get());
+ let new_note = EncryptedNote {
+ id: next_note_id,
+ owner: owner.clone(),
+ users: vec![],
+ encrypted_text: String::new(),
+ };
+
+ if let Some(mut owner_nids) = owner_to_nids.get(&owner) {
+ assert!(owner_nids.ids.len() < MAX_NOTES_PER_USER);
+ owner_nids.ids.push(new_note.id);
+ owner_to_nids.insert(owner, owner_nids);
+ } else {
+ assert!(owner_to_nids.len() < MAX_USERS);
+ owner_to_nids.insert(
+ owner,
+ NoteIds {
+ ids: vec![new_note.id],
+ },
+ );
+ }
+ assert_eq!(id_to_note.insert(new_note.id, new_note), None);
+
+ NEXT_NOTE_ID.with_borrow_mut(|next_note_id| {
+ let id_plus_one = next_note_id
+ .get()
+ .checked_add(1)
+ .expect("failed to increase NEXT_NOTE_ID: reached the maximum");
+ next_note_id
+ .set(id_plus_one)
+ .unwrap_or_else(|_e| ic_cdk::trap("failed to set NEXT_NOTE_ID"))
+ });
+ next_note_id
+ })
+ })
+}
+
+/// Shares the note with ID `note_id`` with the `user`.
+/// Has no effect if the note is already shared with that user.
+///
+/// Panics:
+/// [caller] is the anonymous identity
+/// [caller] is not the owner of note with id `note_id`
+#[update]
+fn add_user(note_id: NoteId, user: PrincipalName) {
+ let caller_str = caller().to_string();
+ NOTES.with_borrow_mut(|notes| {
+ NOTE_SHARES.with_borrow_mut(|user_to_nids| {
+ if let Some(mut note) = notes.get(¬e_id) {
+ let owner = ¬e.owner;
+ if owner != &caller_str {
+ ic_cdk::trap("only the owner can share the note");
+ }
+ assert!(note.users.len() < MAX_SHARES_PER_NOTE);
+ if !note.users.contains(&user) {
+ note.users.push(user.clone());
+ notes.insert(note_id, note);
+ }
+ if let Some(mut user_ids) = user_to_nids.get(&user) {
+ if !user_ids.ids.contains(¬e_id) {
+ user_ids.ids.push(note_id);
+ user_to_nids.insert(user, user_ids);
+ }
+ } else {
+ user_to_nids.insert(user, NoteIds { ids: vec![note_id] });
+ }
+ }
+ })
+ });
+}
+
+/// Unshares the note with ID `note_id`` with the `user`.
+/// Has no effect if the note is not shared with that user.
+///
+/// Panics:
+/// [caller] is the anonymous identity
+/// [caller] is not the owner of note with id `note_id`
+#[update]
+fn remove_user(note_id: NoteId, user: PrincipalName) {
+ let caller_str = caller().to_string();
+ NOTES.with_borrow_mut(|notes| {
+ NOTE_SHARES.with_borrow_mut(|user_to_nids| {
+ if let Some(mut note) = notes.get(¬e_id) {
+ let owner = ¬e.owner;
+ if owner != &caller_str {
+ ic_cdk::trap("only the owner can share the note");
+ }
+ note.users.retain(|u| u != &user);
+ notes.insert(note_id, note);
+
+ if let Some(mut user_ids) = user_to_nids.get(&user) {
+ user_ids.ids.retain(|&id| id != note_id);
+ if !user_ids.ids.is_empty() {
+ user_to_nids.insert(user, user_ids);
+ } else {
+ user_to_nids.remove(&user);
+ }
+ }
+ }
+ })
+ });
+}
+
+use ic_cdk::management_canister::{
+ VetKDCurve, VetKDDeriveKeyArgs, VetKDDeriveKeyResult, VetKDKeyId, VetKDPublicKeyArgs,
+ VetKDPublicKeyResult,
+};
+
+#[update]
+async fn symmetric_key_verification_key_for_note() -> String {
+ let request = VetKDPublicKeyArgs {
+ canister_id: None,
+ context: b"note_symmetric_key".to_vec(),
+ key_id: key_id(),
+ };
+
+ let response: VetKDPublicKeyResult = ic_cdk::management_canister::vetkd_public_key(&request)
+ .await
+ .expect("call to vetkd_public_key failed");
+
+ hex::encode(response.public_key)
+}
+
+#[update]
+async fn encrypted_symmetric_key_for_note(
+ note_id: NoteId,
+ transport_public_key: Vec,
+) -> String {
+ let user_str = caller().to_string();
+ let request = NOTES.with_borrow(|notes| {
+ if let Some(note) = notes.get(¬e_id) {
+ if !note.is_authorized(&user_str) {
+ ic_cdk::trap(format!("unauthorized key request by user {user_str}"));
+ }
+ VetKDDeriveKeyArgs {
+ input: {
+ let mut buf = vec![];
+ buf.extend_from_slice(¬e_id.to_be_bytes()); // fixed-size encoding
+ buf.extend_from_slice(note.owner.as_bytes());
+ buf // prefix-free
+ },
+ context: b"note_symmetric_key".to_vec(),
+ key_id: key_id(),
+ transport_public_key,
+ }
+ } else {
+ ic_cdk::trap(format!("note with ID {note_id} does not exist"));
+ }
+ });
+
+ let response: VetKDDeriveKeyResult = ic_cdk::management_canister::vetkd_derive_key(&request)
+ .await
+ .expect("call to vetkd_derive_key failed");
+
+ hex::encode(response.encrypted_key)
+}
+
+fn key_id() -> VetKDKeyId {
+ VetKDKeyId {
+ curve: VetKDCurve::Bls12_381_G2,
+ name: KEY_NAME.with_borrow(|key_name| key_name.get().clone()),
+ }
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/dfx.json b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/dfx.json
new file mode 100644
index 000000000..7cfa041f3
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/dfx.json
@@ -0,0 +1,39 @@
+{
+ "canisters": {
+ "encrypted_notes": {
+ "type": "rust",
+ "candid": "backend/src/encrypted_notes_rust.did",
+ "package": "encrypted_notes_backend",
+ "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": ["encrypted_notes", "internet-identity"],
+ "build": [
+ "cd frontend && npm i --include=dev && npm run build && cd - && rm -r public > /dev/null 2>&1; cp -r frontend/public ./"
+ ],
+ "frontend": {
+ "entrypoint": "public/index.html"
+ },
+ "source": ["public/"],
+ "type": "assets",
+ "output_env_file": "frontend/.env"
+ }
+ },
+ "networks": {
+ "local": {
+ "bind": "127.0.0.1:8000",
+ "type": "ephemeral"
+ }
+ }
+}
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/frontend b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/frontend
new file mode 120000
index 000000000..af288785f
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/frontend
@@ -0,0 +1 @@
+../frontend
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml
new file mode 100644
index 000000000..2a2058b04
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml
@@ -0,0 +1,4 @@
+[toolchain]
+channel = "1.88.0"
+targets = ["wasm32-unknown-unknown"]
+profile = "default"
\ No newline at end of file
diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/security-checklist.md b/rust/vetkeys/encrypted_notes_dapp_vetkd/security-checklist.md
new file mode 100644
index 000000000..7f2c2a9a5
--- /dev/null
+++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/security-checklist.md
@@ -0,0 +1,165 @@
+This document lists typical steps of a security review needed for production-ready IC dapps. We indicate whether the two backend implementations of Encrypted Notes comply with the corresponding requirements (marked as Done), do not yet comply (Future), or whether a particular requirement is not applicable to this backend (Not applicable).
+
+While this list might help creating better IC dapps, keep in mind that the list is potentially incomplete. In particular, each real-world dapp may have a different set of security requirements that depend on its target domain and intended use case.
+
+# 1. Authentication
+
+### 1.1. Make sure any action that only a specific user should be able to do requires authentication
+* Motoko: Done
+* Rust: Done
+
+### 1.2. Disallow the anonymous principal in authenticated calls
+* Motoko: Done
+* Rust: Done
+
+# 2. Consensus
+
+Avoid using uncertified queries in public canister APIs. Instead, either use certified update methods or design an eventual certification approach for performance-critical dapps.
+* Motoko: Done (no query methods)
+* Rust: Done (no query methods)
+
+# 3. Input Validation
+
+Each public API method should sanitize their arguments and gracefully handle exceptional situations.
+* Motoko: Done
+* Rust: Done
+
+# 4. Frontend security
+
+### 4.1. Frontend input validation
+* Motoko: Done
+* Rust: Done
+
+### 4.2. Avoid using deterministic encryption.
+For example, the initialization vector for AES-GCM encryption should be unique for each message (or chosen at random).
+* Motoko: Done
+* Rust: Done
+
+### 4.3. Do not load untrusted assets like CSS or fonts
+* Motoko: Done
+* Rust: Done
+
+### 4.4. Avoid logging sensitive data like private keys
+When generating the private key using `crypto.subtle.generateKey`, set `extractable=false`. Consider offloading the secret keys to a YubiKey or YubyHSM so that the secret keys never end up in the browser.
+* Motoko: Done
+* Rust: Done
+
+### 4.5. Avoid reusing the same public/private key pair for every identity in the same browser
+* Motoko: Future
+* Rust: Future
+
+### 4.6. Set reasonable session timeouts
+For example, a security-sensitive dapp like Encrypted Notes should set `maxTimeToLive` for Internet Identity delegation to 30 min rather than 24 h.
+* Motoko: Future
+* Rust: Future
+
+### 4.7. Regularly refresh symmetric encryption keys
+* Motoko: Future
+* Rust: Future
+
+# 5. Asset Certification
+
+### 5.1. Use HTTP asset certification and avoid serving your dapp through raw.ic0.app
+* Motoko: Done
+* Rust: Done
+
+# 6. Canister Storage
+
+### 6.1. Use thread_local! with Cell/RefCell for state variables and put all your globals in one basket
+* Motoko: Not applicable
+* Rust: Done
+
+### 6.2. Limit the amount of data that can be stored in a canister per user
+* Motoko: Done
+* Rust: Done
+
+### 6.3. Consider using stable memory, version it, test it
+* Motoko: Done (except versioning)
+* Rust: Done (except versioning)
+
+### 6.4. Don’t store sensitive data on canisters (unless it is encrypted)
+* Motoko: Done
+* Rust: Done
+
+### 6.5. Create backups
+* Motoko: Future
+* Rust: Future
+
+# 7. Inter-Canister Calls and Rollbacks
+
+### 5.1. Don’t panic after await and don’t lock shared resources across await boundaries
+* Motoko: Done (we don't use await)
+* Rust: Done (we don't use await)
+
+### 5.2. Be aware that state may change during inter-canister calls
+* Motoko: Done (we have no inter-canister calls)
+* Rust: Done (we have no inter-canister calls)
+
+### 5.3. Only make inter-canister calls to trustworthy canisters
+* Motoko: Done (we have no inter-canister calls)
+* Rust: Done (we have no inter-canister calls)
+
+### 5.4. Make sure there are no loops in call graphs
+* Motoko: Done
+* Rust: Done
+
+# 8. Canister Upgrades
+
+### 8.1. Don’t panic/trap during upgrades:
+* Motoko: Done, assuming that [`Iter.toArray`](https://github.com/dfinity/motoko-base/blob/master/src/Iter.mo) and [`Map.fromIter`](https://github.com/dfinity/motoko-base/blob/master/src/HashMap.mo) do not trap.
+* Rust: Done, assuming that [`borrow_mut`](https://doc.rust-lang.org/std/borrow/trait.BorrowMut.html#tymethod.borrow_mut), [`std::mem::take`](https://doc.rust-lang.org/stable/std/mem/fn.take.html), and [`ic_cdk::storage::stable_save`](https://docs.rs/ic-cdk/latest/ic_cdk/storage/fn.stable_save.html) do not panic.
+
+### 8.2. Ensure upgradeability
+If the canister storage becomes too big, the canister will no longer be upgradable because `pre_upgrade` will time out or the canister will run out of cycles. The recommended remedy is to use stable memory directly rather than serializing data upon upgrade.
+* Motoko: Future
+* Rust: Future
+
+# 9. Rust-specific issues
+
+### 9.1. Don’t use unsafe Rust code:
+* Rust: Done
+
+### 9.2. Avoid integer overflows:
+* Rust: Done
+
+# 10. Miscellaneous
+
+### 10.1. For expensive calls, consider using captchas or proof of work
+* Motoko: Future
+* Rust: Future
+
+### 10.2. Test your canister code even in presence of System API calls
+* Motoko: Future
+* Rust: Future
+
+### 10.3. Make canister builds reproducible
+* Motoko: Done (via Docker)
+* Rust: Done (via Docker)
+
+### 10.4. Expose metrics from your canister
+* Motoko: Future
+* Rust: Future
+
+### 10.5. Don’t rely on time being strictly monotonic
+* Motoko: Done
+* Rust: Done
+
+### 10.6. Protect against draining the cycles balance
+* Motoko: Future
+* Rust: Future
+
+
+# 11. Efficiency considerations
+
+### 11.1. `submit_ciphertexts`
+* Adding submit_ciphertexts is currently O(C*D) where `C = ciphertexts.size()` and `D = store.device_list.size()`
+
+# 12. Usability
+
+### 12.1. Confirm user's intention before executing potentially irreversible actions like device removal
+* Motoko: Future
+* Rust: Future
+
+### 12.2. Prevent account lockout scenarios
+* Motoko: Future
+* Rust: Future