From 5ca9c481ce4a0fdfe3c2ca233f522de69e6e014a Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 10:53:44 +0200 Subject: [PATCH 1/7] feat: add vetkeys basic examples Add three basic vetkeys examples from the dfinity/vetkeys repository: - basic_bls_signing: BLS signature creation and verification - basic_ibe: Identity-Based Encryption messaging - basic_timelock_ibe: Time-locked secret bid auction (Rust only) Each includes frontend, Rust backend, and Motoko backend (where applicable). --- motoko/vetkeys/README.md | 18 +- rust/vetkeys/basic_bls_signing/README.md | 60 +++ .../frontend/eslint.config.mjs | 30 ++ .../basic_bls_signing/frontend/index.html | 13 + .../basic_bls_signing/frontend/package.json | 31 ++ .../frontend/public/.ic-assets.json5 | 10 + .../frontend/scripts/gen_bindings.sh | 15 + .../basic_bls_signing/frontend/src/main.ts | 280 ++++++++++ .../basic_bls_signing/frontend/src/style.css | 374 ++++++++++++++ .../frontend/src/vite-end.d.ts | 1 + .../basic_bls_signing/frontend/tsconfig.json | 25 + .../basic_bls_signing/frontend/vite.config.ts | 27 + .../motoko/backend/src/Main.mo | 132 +++++ .../vetkeys/basic_bls_signing/motoko/dfx.json | 51 ++ .../vetkeys/basic_bls_signing/motoko/frontend | 1 + .../basic_bls_signing/motoko/mops.toml | 4 + .../vetkeys/basic_bls_signing/rust/Cargo.toml | 8 + .../basic_bls_signing/rust/backend/Cargo.toml | 23 + .../basic_bls_signing/rust/backend/Makefile | 15 + .../rust/backend/backend.did | 6 + .../basic_bls_signing/rust/backend/src/lib.rs | 145 ++++++ .../rust/backend/src/types.rs | 28 + rust/vetkeys/basic_bls_signing/rust/dfx.json | 45 ++ rust/vetkeys/basic_bls_signing/rust/frontend | 1 + .../rust/rust-toolchain.toml | 1 + .../basic_bls_signing/ui_screenshot.png | Bin 0 -> 109893 bytes rust/vetkeys/basic_ibe/README.md | 67 +++ rust/vetkeys/basic_ibe/frontend/.prettierrc | 4 + .../basic_ibe/frontend/eslint.config.mjs | 30 ++ rust/vetkeys/basic_ibe/frontend/index.html | 13 + rust/vetkeys/basic_ibe/frontend/package.json | 31 ++ .../frontend/public/.ic-assets.json5 | 10 + .../basic_ibe/frontend/public/vite.svg | 1 + .../frontend/scripts/gen_bindings.sh | 15 + rust/vetkeys/basic_ibe/frontend/src/main.ts | 339 ++++++++++++ rust/vetkeys/basic_ibe/frontend/src/style.css | 304 +++++++++++ .../basic_ibe/frontend/src/vite-env.d.ts | 1 + rust/vetkeys/basic_ibe/frontend/tsconfig.json | 24 + .../vetkeys/basic_ibe/frontend/vite.config.ts | 27 + .../basic_ibe/motoko/backend/src/Main.mo | 169 ++++++ rust/vetkeys/basic_ibe/motoko/dfx.json | 49 ++ rust/vetkeys/basic_ibe/motoko/frontend | 1 + rust/vetkeys/basic_ibe/motoko/mops.toml | 3 + rust/vetkeys/basic_ibe/rust/Cargo.toml | 3 + .../vetkeys/basic_ibe/rust/backend/Cargo.toml | 24 + rust/vetkeys/basic_ibe/rust/backend/Makefile | 15 + .../basic_ibe/rust/backend/backend.did | 18 + .../vetkeys/basic_ibe/rust/backend/src/lib.rs | 131 +++++ .../basic_ibe/rust/backend/src/types.rs | 50 ++ rust/vetkeys/basic_ibe/rust/dfx.json | 43 ++ rust/vetkeys/basic_ibe/rust/frontend | 1 + .../basic_ibe/rust/rust-toolchain.toml | 1 + rust/vetkeys/basic_ibe/ui_screenshot.png | Bin 0 -> 84227 bytes rust/vetkeys/basic_timelock_ibe/Cargo.toml | 8 + rust/vetkeys/basic_timelock_ibe/README.md | 73 +++ .../basic_timelock_ibe/backend/Cargo.toml | 26 + .../basic_timelock_ibe/backend/Makefile | 15 + .../basic_timelock_ibe/backend/backend.did | 27 + .../basic_timelock_ibe/backend/src/lib.rs | 432 ++++++++++++++++ .../basic_timelock_ibe/backend/src/types.rs | 109 ++++ rust/vetkeys/basic_timelock_ibe/dfx.json | 43 ++ .../basic_timelock_ibe/frontend/.prettierrc | 4 + .../frontend/eslint.config.mjs | 30 ++ .../basic_timelock_ibe/frontend/index.html | 13 + .../basic_timelock_ibe/frontend/package.json | 32 ++ .../frontend/public/.ic-assets.json5 | 10 + .../frontend/public/vite.svg | 1 + .../frontend/scripts/gen_bindings.sh | 8 + .../basic_timelock_ibe/frontend/src/main.ts | 483 ++++++++++++++++++ .../basic_timelock_ibe/frontend/src/style.css | 452 ++++++++++++++++ .../frontend/src/vite-env.d.ts | 1 + .../basic_timelock_ibe/frontend/tsconfig.json | 24 + .../frontend/vite.config.ts | 26 + .../basic_timelock_ibe/rust-toolchain.toml | 1 + .../basic_timelock_ibe/ui_screenshot.png | Bin 0 -> 115541 bytes 75 files changed, 4527 insertions(+), 9 deletions(-) create mode 100644 rust/vetkeys/basic_bls_signing/README.md create mode 100644 rust/vetkeys/basic_bls_signing/frontend/eslint.config.mjs create mode 100644 rust/vetkeys/basic_bls_signing/frontend/index.html create mode 100644 rust/vetkeys/basic_bls_signing/frontend/package.json create mode 100644 rust/vetkeys/basic_bls_signing/frontend/public/.ic-assets.json5 create mode 100755 rust/vetkeys/basic_bls_signing/frontend/scripts/gen_bindings.sh create mode 100644 rust/vetkeys/basic_bls_signing/frontend/src/main.ts create mode 100644 rust/vetkeys/basic_bls_signing/frontend/src/style.css create mode 100644 rust/vetkeys/basic_bls_signing/frontend/src/vite-end.d.ts create mode 100644 rust/vetkeys/basic_bls_signing/frontend/tsconfig.json create mode 100644 rust/vetkeys/basic_bls_signing/frontend/vite.config.ts create mode 100644 rust/vetkeys/basic_bls_signing/motoko/backend/src/Main.mo create mode 100644 rust/vetkeys/basic_bls_signing/motoko/dfx.json create mode 120000 rust/vetkeys/basic_bls_signing/motoko/frontend create mode 100644 rust/vetkeys/basic_bls_signing/motoko/mops.toml create mode 100644 rust/vetkeys/basic_bls_signing/rust/Cargo.toml create mode 100644 rust/vetkeys/basic_bls_signing/rust/backend/Cargo.toml create mode 100644 rust/vetkeys/basic_bls_signing/rust/backend/Makefile create mode 100644 rust/vetkeys/basic_bls_signing/rust/backend/backend.did create mode 100644 rust/vetkeys/basic_bls_signing/rust/backend/src/lib.rs create mode 100644 rust/vetkeys/basic_bls_signing/rust/backend/src/types.rs create mode 100644 rust/vetkeys/basic_bls_signing/rust/dfx.json create mode 120000 rust/vetkeys/basic_bls_signing/rust/frontend create mode 120000 rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml create mode 100644 rust/vetkeys/basic_bls_signing/ui_screenshot.png create mode 100644 rust/vetkeys/basic_ibe/README.md create mode 100644 rust/vetkeys/basic_ibe/frontend/.prettierrc create mode 100644 rust/vetkeys/basic_ibe/frontend/eslint.config.mjs create mode 100644 rust/vetkeys/basic_ibe/frontend/index.html create mode 100644 rust/vetkeys/basic_ibe/frontend/package.json create mode 100644 rust/vetkeys/basic_ibe/frontend/public/.ic-assets.json5 create mode 100644 rust/vetkeys/basic_ibe/frontend/public/vite.svg create mode 100755 rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh create mode 100644 rust/vetkeys/basic_ibe/frontend/src/main.ts create mode 100644 rust/vetkeys/basic_ibe/frontend/src/style.css create mode 100644 rust/vetkeys/basic_ibe/frontend/src/vite-env.d.ts create mode 100644 rust/vetkeys/basic_ibe/frontend/tsconfig.json create mode 100644 rust/vetkeys/basic_ibe/frontend/vite.config.ts create mode 100644 rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo create mode 100644 rust/vetkeys/basic_ibe/motoko/dfx.json create mode 120000 rust/vetkeys/basic_ibe/motoko/frontend create mode 100644 rust/vetkeys/basic_ibe/motoko/mops.toml create mode 100644 rust/vetkeys/basic_ibe/rust/Cargo.toml create mode 100644 rust/vetkeys/basic_ibe/rust/backend/Cargo.toml create mode 100644 rust/vetkeys/basic_ibe/rust/backend/Makefile create mode 100644 rust/vetkeys/basic_ibe/rust/backend/backend.did create mode 100644 rust/vetkeys/basic_ibe/rust/backend/src/lib.rs create mode 100644 rust/vetkeys/basic_ibe/rust/backend/src/types.rs create mode 100644 rust/vetkeys/basic_ibe/rust/dfx.json create mode 120000 rust/vetkeys/basic_ibe/rust/frontend create mode 120000 rust/vetkeys/basic_ibe/rust/rust-toolchain.toml create mode 100644 rust/vetkeys/basic_ibe/ui_screenshot.png create mode 100644 rust/vetkeys/basic_timelock_ibe/Cargo.toml create mode 100644 rust/vetkeys/basic_timelock_ibe/README.md create mode 100644 rust/vetkeys/basic_timelock_ibe/backend/Cargo.toml create mode 100644 rust/vetkeys/basic_timelock_ibe/backend/Makefile create mode 100644 rust/vetkeys/basic_timelock_ibe/backend/backend.did create mode 100644 rust/vetkeys/basic_timelock_ibe/backend/src/lib.rs create mode 100644 rust/vetkeys/basic_timelock_ibe/backend/src/types.rs create mode 100644 rust/vetkeys/basic_timelock_ibe/dfx.json create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/.prettierrc create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/eslint.config.mjs create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/index.html create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/package.json create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/public/.ic-assets.json5 create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/public/vite.svg create mode 100755 rust/vetkeys/basic_timelock_ibe/frontend/scripts/gen_bindings.sh create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/src/main.ts create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/src/style.css create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/src/vite-env.d.ts create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/tsconfig.json create mode 100644 rust/vetkeys/basic_timelock_ibe/frontend/vite.config.ts create mode 120000 rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml create mode 100644 rust/vetkeys/basic_timelock_ibe/ui_screenshot.png diff --git a/motoko/vetkeys/README.md b/motoko/vetkeys/README.md index 6bc9c7d17..822311636 100644 --- a/motoko/vetkeys/README.md +++ b/motoko/vetkeys/README.md @@ -1,12 +1,12 @@ -# vetKeys Examples +# VetKeys Examples (Motoko) -## Basic Examples -- **[Password Manager](https://github.com/dfinity/vetkeys/tree/main/examples/password_manager)** - A secure, decentralized password manager using Encrypted Maps for vault-based password storage and sharing. -- **[Password Manager with Metadata](https://github.com/dfinity/vetkeys/tree/main/examples/password_manager_with_metadata)** - Extends the basic password manager to support unencrypted metadata alongside encrypted passwords. -- **[Encrypted Notes](https://github.com/dfinity/vetkeys/tree/main/examples/encrypted_notes_dapp_vetkd)** - A secure note-taking application that uses vetKeys for encryption and enables sharing notes between users without device management. +The VetKeys examples (including Motoko backends) are located in [`rust/vetkeys/`](../../rust/vetkeys/). -## Advanced Examples +Each example that supports a Motoko backend has a `motoko/` subdirectory alongside its `rust/` backend: -- **[Threshold BLS Signature](https://github.com/dfinity/vetkeys/tree/main/examples/basic_bls_signing)** - Demonstrates how to use vetKeys to create a threshold BLS signing service. - -- **[Identity-Basic Encryption (IBE)](https://github.com/dfinity/vetkeys/tree/main/examples/basic_ibe)** - Shows how to implement secure messaging using Identity Based Encryption with Internet Identity Principals as encryption keys. \ No newline at end of file +- [Basic BLS Signing](../../rust/vetkeys/basic_bls_signing/) — Motoko + Rust +- [Basic IBE](../../rust/vetkeys/basic_ibe/) — Motoko + Rust +- [Encrypted Notes](../../rust/vetkeys/encrypted_notes_dapp_vetkd/) — Motoko + Rust +- [Password Manager](../../rust/vetkeys/password_manager/) — Motoko + Rust +- [Password Manager with Metadata](../../rust/vetkeys/password_manager_with_metadata/) — Motoko + Rust +- [Basic Timelock IBE](../../rust/vetkeys/basic_timelock_ibe/) — Rust only diff --git a/rust/vetkeys/basic_bls_signing/README.md b/rust/vetkeys/basic_bls_signing/README.md new file mode 100644 index 000000000..6d86b5212 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/README.md @@ -0,0 +1,60 @@ +# Threshold BLS Signatures + +| Motoko backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/basic_bls_signing/motoko)| +| --- | --- | +| Rust backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/basic_bls_signing/rust) | + +The **Basic BLS signing** example demonstrates how to use **[vetKeys](https://internetcomputer.org/docs/building-apps/network-features/vetkeys/introduction)** to implement a threshold BLS signing service on the **Internet Computer (IC)**, where every authenticated user can ask the canister (IC smart contract) to produce signatures, where the **Internet Identity Principal** identifies the signer. This canister ensures that users can only produce signature for their own principal and not for someone else's principal. Furthermore, the vetKeys in this dapp can only be produced upon a user request, as specified in the canister code, meaning that the canister cannot produce signatures for arbitrary users or messages. + +For confirming that the canister can only produce signatures in the intended way, users need to inspect the code installed in the canister. For this, it is crucial that canisters using VetKeys have their code public. + +![UI Screenshot](ui_screenshot.png) + +## Features + +- **Signer Authorization**: Only authorized users can produce signatures and only for their own identity. +- **Frontend Signature Verification**: Any user can publish any signature from their principal in the canister storage and the frontend automatically checks the signature validity. + +## Setup + +### Prerequisites + +- [Internet Computer software development kit](https://internetcomputer.org/docs/building-apps/getting-started/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: +* Produces signatures upon a user request. +* Allows users to retrieve the root public key that can be used to check any user's signature for this canister. +* Allows users to store signatures (real or fake) in a log datastructure. + +### Frontend + +The frontend is a vanilla typescript application providing a simple interface for signing, showing the signatures stored in the canister, and publishing a signature. + +To run the frontend in development mode with hot reloading (after running `dfx deploy`): + +```bash +npm run dev +``` + +## Additional Resources + +- **[What are VetKeys](https://internetcomputer.org/docs/building-apps/network-features/encryption/vetkeys)** - For more information about VetKeys and VetKD. diff --git a/rust/vetkeys/basic_bls_signing/frontend/eslint.config.mjs b/rust/vetkeys/basic_bls_signing/frontend/eslint.config.mjs new file mode 100644 index 000000000..39313b167 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/eslint.config.mjs @@ -0,0 +1,30 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + ignores: [ + "dist/", + "src/declarations", + "coverage/", + "*.config.js", + "*.config.cjs", + "*.config.mjs", + "*.config.ts", + ], + } +); diff --git a/rust/vetkeys/basic_bls_signing/frontend/index.html b/rust/vetkeys/basic_bls_signing/frontend/index.html new file mode 100644 index 000000000..7240c1828 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + VetKeys: Basic BLS Signing + + +
+ + + diff --git a/rust/vetkeys/basic_bls_signing/frontend/package.json b/rust/vetkeys/basic_bls_signing/frontend/package.json new file mode 100644 index 000000000..f103fde2a --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "basic_bls_signing_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "npm run build:bindings && vite", + "build": "npm run build:bindings && tsc && vite build", + "build:bindings": "cd scripts && ./gen_bindings.sh", + "preview": "vite preview", + "lint": "eslint" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^24.0.10", + "eslint": "^9.24.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.0", + "tslib": "^2.8.1", + "typescript": "~5.7.2", + "typescript-eslint": "^8.35.1", + "vite": "^6.4.1", + "vite-plugin-environment": "^1.1.3" + }, + "dependencies": { + "@dfinity/auth-client": "^2.4.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/vetkeys": "^0.3.0" + } +} diff --git a/rust/vetkeys/basic_bls_signing/frontend/public/.ic-assets.json5 b/rust/vetkeys/basic_bls_signing/frontend/public/.ic-assets.json5 new file mode 100644 index 000000000..58968d288 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/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';object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + }, + allow_raw_access: false + }, +] diff --git a/rust/vetkeys/basic_bls_signing/frontend/scripts/gen_bindings.sh b/rust/vetkeys/basic_bls_signing/frontend/scripts/gen_bindings.sh new file mode 100755 index 000000000..8eef59506 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/scripts/gen_bindings.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cd ../../backend && make extract-candid + +cd .. && dfx generate basic_bls_signing || exit 1 + +rm -r frontend/src/declarations/basic_bls_signing > /dev/null 2>&1 || true + +mkdir -p frontend/src/declarations/basic_bls_signing +mv src/declarations/basic_bls_signing frontend/src/declarations +rmdir -p src/declarations > /dev/null 2>&1 || true + +# dfx 0.31+ generates @icp-sdk/core imports; rewrite to @dfinity/* to match deps +find frontend/src/declarations -type f \( -name '*.ts' -o -name '*.js' \) -exec \ + perl -i -pe 's|\@icp-sdk/core/agent|\@dfinity/agent|g; s|\@icp-sdk/core/principal|\@dfinity/principal|g; s|\@icp-sdk/core/candid|\@dfinity/candid|g' {} + \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/frontend/src/main.ts b/rust/vetkeys/basic_bls_signing/frontend/src/main.ts new file mode 100644 index 000000000..ef5b3f247 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/src/main.ts @@ -0,0 +1,280 @@ +// Required to run `npm run dev`. +if (!window.global) { + window.global = window; +} + +import "./style.css"; +import { createActor } from "./declarations/basic_bls_signing"; +import { Principal } from "@dfinity/principal"; +import { AuthClient } from "@dfinity/auth-client"; +import type { ActorSubclass } from "@dfinity/agent"; +import { _SERVICE } from "./declarations/basic_bls_signing/basic_bls_signing.did"; +import { DerivedPublicKey, verifyBlsSignature } from "@dfinity/vetkeys"; +import type { Signature } from "./declarations/basic_bls_signing/basic_bls_signing.did"; + +let myPrincipal: Principal | undefined = undefined; +let authClient: AuthClient | undefined; +let basicBlsSigningCanister: ActorSubclass<_SERVICE> | undefined; +// let canisterPublicKey: DerivedPublicKey | undefined; +let myVerificationKey: DerivedPublicKey | undefined; + +function getBasicBlsSigningCanister(): ActorSubclass<_SERVICE> { + if (basicBlsSigningCanister) return basicBlsSigningCanister; + if (!process.env.CANISTER_ID_BASIC_BLS_SIGNING) { + throw Error("CANISTER_ID_BASIC_BLS_SIGNING is not set"); + } + if (!authClient) { + throw Error("Auth client is not initialized"); + } + const host = + process.env.DFX_NETWORK === "ic" + ? `https://${process.env.CANISTER_ID_BASIC_BLS_SIGNING}.ic0.app` + : "http://localhost:8000"; + + basicBlsSigningCanister = createActor( + process.env.CANISTER_ID_BASIC_BLS_SIGNING, + { + agentOptions: { + identity: authClient.getIdentity(), + host, + }, + }, + ); + + return basicBlsSigningCanister!; +} + +export function login(client: AuthClient) { + void 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: () => { + myPrincipal = client.getIdentity().getPrincipal(); + updateUI(true); + }, + onError: (error) => { + alert("Authentication failed: " + error); + }, + }); +} + +export function logout() { + void authClient?.logout(); + myPrincipal = undefined; + myVerificationKey = undefined; + basicBlsSigningCanister = undefined; + updateUI(false); + document.getElementById("signaturesList")!.classList.toggle("hidden", true); +} + +async function initAuth() { + authClient = await AuthClient.create(); + const isAuthenticated = await authClient.isAuthenticated(); + + if (isAuthenticated) { + myPrincipal = authClient.getIdentity().getPrincipal(); + updateUI(true); + } else { + updateUI(false); + } +} + +function updateUI(isAuthenticated: boolean) { + const loginButton = document.getElementById("loginButton")!; + const principalDisplay = document.getElementById("principalDisplay")!; + const logoutButton = document.getElementById("logoutButton")!; + const signingActions = document.getElementById("signingActions")!; + const customSignatureForm = document.getElementById("customSignatureForm")!; + const signaturesList = document.getElementById("signaturesList")!; + + loginButton.classList.toggle("hidden", isAuthenticated); + principalDisplay.classList.toggle("hidden", !isAuthenticated); + logoutButton.classList.toggle("hidden", !isAuthenticated); + signingActions.classList.toggle("hidden", !isAuthenticated); + customSignatureForm.classList.toggle("hidden", true); + signaturesList.classList.toggle("hidden", true); + + if (isAuthenticated && myPrincipal) { + principalDisplay.textContent = `Principal: ${myPrincipal.toString()}`; + } +} + +function handleLogin() { + if (!authClient) { + alert("Auth client not initialized"); + return; + } + login(authClient); +} + +document.querySelector("#app")!.innerHTML = ` +
+

Basic BLS Signing using VetKeys

+
+
+ +
+ +
+ + + +
+
+

Verify Custom Signature

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

My Signatures

+
+
+
+`; + +// Add event listeners +document.getElementById("loginButton")!.addEventListener("click", handleLogin); +document.getElementById("logoutButton")!.addEventListener("click", logout); +document.getElementById("signMessageButton")!.addEventListener("click", () => { + void (async () => { + const message = prompt("Enter message to sign:"); + if (message) { + try { + await getBasicBlsSigningCanister().sign_message(message); + alert("Created and stored signature successfully."); + } catch (error) { + alert(`Error: ${error as Error}`); + } + } + })(); +}); + +document + .getElementById("customSignatureButton")! + .addEventListener("click", () => { + document + .getElementById("customSignatureForm")! + .classList.toggle("hidden", false); + document.getElementById("signaturesList")!.classList.toggle("hidden", true); + }); + +document + .getElementById("listSignaturesButton")! + .addEventListener("click", () => { + void listSignatures(); + }); + +document + .getElementById("submitSignatureForm")! + .addEventListener("submit", (e) => { + e.preventDefault(); + const message = (document.getElementById("message") as HTMLInputElement) + .value; + const signatureHex = ( + document.getElementById("signature") as HTMLInputElement + ).value; + const pubkeyHex = (document.getElementById("pubkey") as HTMLInputElement) + .value; + const messageBytes = new TextEncoder().encode(message); + + try { + const signatureBytes = new Uint8Array( + signatureHex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)), + ); + const pubkeyBytes = new Uint8Array( + pubkeyHex.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)), + ); + + const verificationKey = DerivedPublicKey.deserialize(pubkeyBytes); + + const result = verifyBlsSignature( + verificationKey, + messageBytes, + signatureBytes, + ); + alert(`Verification result: ${result ? "Valid" : "INVALID"}`); + } catch { + alert("Verification failed."); + } + }); + +async function listSignatures() { + const signatures: Array = + await getBasicBlsSigningCanister().get_my_signatures(); + const signaturesDiv = document.getElementById("signatures")!; + signaturesDiv.innerHTML = ""; + + if (signatures.length === 0) { + signaturesDiv.innerHTML = ` +
+

No signatures have been published yet.

+
+ `; + } else { + if (!myVerificationKey) { + const myVerificationKeyRaw = + await getBasicBlsSigningCanister().get_my_verification_key(); + myVerificationKey = DerivedPublicKey.deserialize( + Uint8Array.from(myVerificationKeyRaw), + ); + } + const myVerificationKeyHex = Array.from(myVerificationKey.publicKeyBytes()) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + + for (const signatureData of signatures.slice().reverse()) { + const signatureHex = Array.from(signatureData.signature) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + + // Convert nanoseconds to milliseconds and create a Date object + const timestamp = new Date(Number(signatureData.timestamp) / 1_000_000); + const formattedDate = timestamp.toLocaleString(); + + const signatureElement = document.createElement("div"); + signatureElement.className = "signature"; + + const isValid = verifyBlsSignature( + myVerificationKey, + new TextEncoder().encode(signatureData.message), + Uint8Array.from(signatureData.signature), + ); + + signatureElement.innerHTML = ` +
Signed message: ${signatureData.message}
+

Signature: ${signatureHex}

+

Public key: ${myVerificationKeyHex}

+

Verification: ${isValid ? "Valid" : "Invalid"}

+

Added: ${formattedDate}

+ `; + + signaturesDiv.appendChild(signatureElement); + } + } + + document.getElementById("signaturesList")!.classList.toggle("hidden", false); + document + .getElementById("customSignatureForm")! + .classList.toggle("hidden", true); +} + +// Initialize auth +void initAuth(); diff --git a/rust/vetkeys/basic_bls_signing/frontend/src/style.css b/rust/vetkeys/basic_bls_signing/frontend/src/style.css new file mode 100644 index 000000000..632201518 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/src/style.css @@ -0,0 +1,374 @@ +/* Base styles */ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +h1 { + font-size: 2.2em; + line-height: 1.1; + margin-bottom: 2rem; +} + +h3 { + color: var(--text-color); + font-size: 1.5rem; + margin-bottom: 1.5rem; + font-weight: 600; +} + +h4 { + color: var(--text-color); + font-size: 1.25rem; + margin-bottom: 1rem; + font-weight: 600; +} + +h5 { + color: var(--text-color); + font-size: 1.1rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +/* Principal container */ +.principal-container { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + justify-content: center; +} + +.principal-display { + font-family: monospace; + background-color: rgba(0, 0, 0, 0.2); + padding: 0.5rem 1rem; + border-radius: 8px; + color: #a8a6a6; + white-space: pre-wrap; + word-break: break-all; + max-width: 600px; +} + +/* Buttons */ +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +#loginButton { + background-color: #28a745; + color: white; + min-width: 120px; +} + +#loginButton:hover { + background-color: #218838; +} + +#logoutButton { + background-color: #dc3545; + color: white; +} + +#logoutButton:hover { + background-color: #c82333; +} + +/* Signing actions */ +#signingActions { + display: flex; + gap: 1rem; + justify-content: center; + margin: 2rem 0; +} + +#signingActions button { + background-color: #1a1a1a; + color: white; +} + +#signingActions button:hover { + background-color: #2a2a2a; +} + +/* Custom signature form */ +#customSignatureForm { + max-width: 600px; + margin: 2rem auto; + padding: 2rem 3rem; + background-color: #1a1a1a; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +#customSignatureForm div { + margin-bottom: 1.5rem; +} + +#customSignatureForm label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.87); +} + +#customSignatureForm input { + width: 100%; + padding: 0.75rem; + background-color: #242424; + border: 1px solid #333; + border-radius: 8px; + font-size: 1rem; +} + +#customSignatureForm input:focus { + outline: none; + border-color: #646cff; +} + +#customSignatureForm button { + width: 100%; + background-color: #28a745; + color: white; +} + +#customSignatureForm button:hover { + background-color: #218838; +} + +/* Signatures list */ +#signaturesList { + margin: 2rem 0; +} + +.signature { + margin: 1.5rem 0; + padding: 1.5rem; + background-color: #1a1a1a; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 0.2s ease; + position: relative; +} + +.signature:hover { + transform: translateY(-2px); +} + +.signature h5 { + margin: 0 0 1rem 0; + color: rgba(255, 255, 255, 0.87); +} + +.signature p { + margin: 0.5rem 0; + color: #a8a6a6; +} + +.signature-hex { + font-family: monospace; + word-break: break-all; + background-color: rgba(0, 0, 0, 0.2); + padding: 0.5rem; + border-radius: 4px; +} + +.verification-key-hex { + font-family: monospace; + word-break: break-all; + background-color: rgba(0, 0, 0, 0.2); + padding: 0.5rem; + border-radius: 4px; +} + +.verification-status { + font-weight: 500; +} + +.verification-status.valid { + color: #2ecc71; + font-weight: bold; +} + +.verification-status.invalid { + color: #e03926; + font-weight: bold; +} + +.principal { + font-family: monospace; + color: #a8a6a6; +} + +.login-container { + display: flex; + justify-content: center; + margin: 2rem 0; + width: 100%; +} + +/* Responsive design */ +@media (max-width: 768px) { + #app { + padding: 1rem; + } + + h1 { + font-size: 1.8em; + } + + .principal-container { + flex-direction: column; + } + + #signingActions { + flex-direction: column; + } +} + +/* Light theme */ +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + button { + background-color: #f9f9f9; + } + + .principal-display { + background-color: rgba(0, 0, 0, 0.05); + } + + #customSignatureForm { + background-color: #f9f9f9; + } + + #customSignatureForm label { + color: #213547; + } + + #customSignatureForm input { + background-color: #ffffff; + border-color: #ddd; + } + + .signature { + background-color: #f9f9f9; + } + + .signature h5 { + color: #213547; + } + + .signature p { + color: #666; + } + + .signature-hex { + background-color: rgba(0, 0, 0, 0.05); + } + + .verification-key-hex { + background-color: rgba(0, 0, 0, 0.05); + } + + .principal { + color: #666; + } +} + +.no-signatures { + text-align: center; + padding: 2rem; + background-color: #1a1a1a; + border-radius: 8px; + margin: 1rem 0; +} + +.no-signatures p { + color: #a8a6a6; + font-size: 1.1rem; + margin: 0; +} + +.timestamp { + color: #a8a6a6; + font-size: 0.9rem; + margin-top: 0.5rem; + font-style: italic; +} + +/* Auth state classes */ +.hidden { + display: none !important; +} + +/* Initial state classes for auth elements */ +#loginButton { + display: block; +} + +#signingActions { + display: flex; +} + +#principalDisplay { + display: block; +} + +#logoutButton { + display: block; +} + +#customSignatureForm { + display: block; +} + +#signaturesList { + display: block; +} \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/frontend/src/vite-end.d.ts b/rust/vetkeys/basic_bls_signing/frontend/src/vite-end.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/src/vite-end.d.ts @@ -0,0 +1 @@ +/// diff --git a/rust/vetkeys/basic_bls_signing/frontend/tsconfig.json b/rust/vetkeys/basic_bls_signing/frontend/tsconfig.json new file mode 100644 index 000000000..55cf3a6ef --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] + } + \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/frontend/vite.config.ts b/rust/vetkeys/basic_bls_signing/frontend/vite.config.ts new file mode 100644 index 000000000..27bf81575 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/frontend/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import typescript from '@rollup/plugin-typescript'; +import environment from 'vite-plugin-environment'; +import path from 'path'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + typescript({ + inlineSources: true, + }), + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + root: "./", + server: { + hmr: false + } +}) \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/motoko/backend/src/Main.mo b/rust/vetkeys/basic_bls_signing/motoko/backend/src/Main.mo new file mode 100644 index 000000000..a610ebaf0 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/motoko/backend/src/Main.mo @@ -0,0 +1,132 @@ +import Principal "mo:core/Principal"; +import Text "mo:core/Text"; +import Blob "mo:core/Blob"; +import Nat64 "mo:core/Nat64"; +import Time "mo:core/Time"; +import Map "mo:core/Map"; +import Array "mo:core/Array"; +import List "mo:core/List"; +import Nat8 "mo:core/Nat8"; +import Runtime "mo:core/Runtime"; +import Nat "mo:core/Nat"; +import VetKeys "mo:ic-vetkeys"; +import Order "mo:core/Order"; + +shared persistent actor class (keyName : Text) = { + // Types + type Signature = { + message : Text; + signature : Blob; + timestamp : Nat64; + }; + + type SignatureKey = { + signer : Principal; + timestamp : Nat64; + }; + + type VetKdKeyid = { + curve : { #bls12_381_g2 }; + name : Text; + }; + + // Compare function for SignatureKey + private func signatureKeyCompare(a : SignatureKey, b : SignatureKey) : Order.Order { + switch (Principal.compare(a.signer, b.signer)) { + case (#equal) { Nat64.compare(a.timestamp, b.timestamp) }; + case (other) { other }; + } + }; + + // Stable storage for signatures + private var signatures = Map.empty(); + + // Helper function to get current timestamp + private func getTimestamp() : Nat64 { + Nat64.fromIntWrap(Time.now()); + }; + + // Helper function to create context for vetKD + private func context(signer : Principal) : Blob { + // Domain separator for this dapp + let domainSeparator : [Nat8] = Blob.toArray(Text.encodeUtf8("basic_bls_signing_dapp")); + let domainSeparatorLength : [Nat8] = [Nat8.fromNat(domainSeparator.size())]; // Length of domain separator + + // Combine domain separator length, domain separator, and signer principal + let signerBytes = Principal.toBlob(signer); + let signerArray = Blob.toArray(signerBytes); + + let contextArray = Array.concat( + Array.concat(domainSeparatorLength, domainSeparator), + signerArray, + ); + + Blob.fromArray(contextArray); + }; + + // Helper function to get key ID + private func keyId() : VetKdKeyid { + { + curve = #bls12_381_g2; + name = keyName; + }; + }; + + // Sign a message using BLS + public shared ({ caller }) func sign_message(message : Text) : async Blob { + // TODO(CRP-2874): return only the signature bytes, not the entire vetKey bytes + let bytes = await VetKeys.ManagementCanister.signWithBls( + Text.encodeUtf8(message), + context(caller), + keyId(), + ); + + let BYTES_SIZE : Nat = 192; + let SIGNATURE_SIZE : Nat = 48; + + if (bytes.size() != BYTES_SIZE) { + Runtime.trap("Expected " # Nat.toText(BYTES_SIZE) # " signature bytes, but got " # Nat.toText(bytes.size())); + }; + + let signatureBytes = Blob.fromArray(Array.sliceToArray(Blob.toArray(bytes), BYTES_SIZE - SIGNATURE_SIZE, BYTES_SIZE)); + + let timestamp = getTimestamp(); + let signature : Signature = { + message = message; + signature = signatureBytes; + timestamp = timestamp; + }; + + // Handle potential timestamp collisions by incrementing until we find a free slot + var timestampForMapKey = timestamp; + while (Map.get(signatures, signatureKeyCompare, { signer = caller; timestamp = timestampForMapKey }) != null) { + timestampForMapKey += 1; + }; + + ignore Map.insert(signatures, signatureKeyCompare, { signer = caller; timestamp = timestampForMapKey }, signature); + + signatureBytes; + }; + + // Get all signatures for the current caller + public shared query ({ caller }) func get_my_signatures() : async [Signature] { + var callerSignatures = List.empty(); + + for ((key, value) in Map.entries(signatures)) { + if (Principal.equal(key.signer, caller)) { + List.add(callerSignatures, value); + }; + }; + + List.toArray(callerSignatures); + }; + + // Get verification key for the current caller + public shared ({ caller }) func get_my_verification_key() : async Blob { + await VetKeys.ManagementCanister.blsPublicKey( + null, + context(caller), + keyId(), + ); + }; +}; diff --git a/rust/vetkeys/basic_bls_signing/motoko/dfx.json b/rust/vetkeys/basic_bls_signing/motoko/dfx.json new file mode 100644 index 000000000..0631e6027 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/motoko/dfx.json @@ -0,0 +1,51 @@ +{ + "canisters": { + "basic_bls_signing": { + "main": "backend/src/Main.mo", + "type": "motoko", + "args": "--enhanced-orthogonal-persistence", + "init_arg": "(\"test_key_1\")", + "metadata": [ + { + "name": "candid:service", + "visibility": "public" + } + ] + }, + "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": ["basic_bls_signing", "internet-identity"], + "build": [ + "cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./" + ], + "frontend": { + "entrypoint": "dist/index.html" + }, + "source": ["dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + }, + "defaults": { + "build": { + "packtool": "npx ic-mops sources", + "args": "" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + } +} diff --git a/rust/vetkeys/basic_bls_signing/motoko/frontend b/rust/vetkeys/basic_bls_signing/motoko/frontend new file mode 120000 index 000000000..af288785f --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/motoko/frontend @@ -0,0 +1 @@ +../frontend \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/motoko/mops.toml b/rust/vetkeys/basic_bls_signing/motoko/mops.toml new file mode 100644 index 000000000..668ffda6c --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/motoko/mops.toml @@ -0,0 +1,4 @@ +[dependencies] +core = "1.0.0" +ic-vetkeys = "0.3.0" +sha2 = "0.1.2" \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/rust/Cargo.toml b/rust/vetkeys/basic_bls_signing/rust/Cargo.toml new file mode 100644 index 000000000..dc6548c19 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = ["backend"] +resolver = "2" + +[profile.release] +lto = true +opt-level = 'z' +panic = 'abort' diff --git a/rust/vetkeys/basic_bls_signing/rust/backend/Cargo.toml b/rust/vetkeys/basic_bls_signing/rust/backend/Cargo.toml new file mode 100644 index 000000000..5e7a26140 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/backend/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ic-vetkeys-example-basic-bls-signing-backend" +authors = ["DFINITY Stiftung"] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "Basic BLS Signing" +repository = "https://github.com/dfinity/vetkeys" +rust-version = "1.85.0" + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10.2" +getrandom = { version = "0.2", features = ["custom"] } +ic-cdk = "0.18.3" +ic-stable-structures = "0.6.8" +ic-vetkeys = "0.3.0" +serde = "1.0.217" +serde_bytes = "0.11.15" +serde_cbor = "0.11.2" diff --git a/rust/vetkeys/basic_bls_signing/rust/backend/Makefile b/rust/vetkeys/basic_bls_signing/rust/backend/Makefile new file mode 100644 index 000000000..458eba5ad --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/backend/Makefile @@ -0,0 +1,15 @@ +.PHONY: compile-wasm +.SILENT: compile-wasm +compile-wasm: + cargo build --release --target wasm32-unknown-unknown + +.PHONY: extract-candid +.SILENT: extract-candid +extract-candid: compile-wasm + candid-extractor ../target/wasm32-unknown-unknown/release/ic_vetkeys_example_basic_bls_signing_backend.wasm > backend.did + +.PHONY: clean +.SILENT: clean +clean: + cargo clean + rm -rf .dfx \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/rust/backend/backend.did b/rust/vetkeys/basic_bls_signing/rust/backend/backend.did new file mode 100644 index 000000000..b1ae80149 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/backend/backend.did @@ -0,0 +1,6 @@ +type Signature = record { signature : blob; message : text; timestamp : nat64 }; +service : (text) -> { + get_my_signatures : () -> (vec Signature) query; + get_my_verification_key : () -> (blob); + sign_message : (text) -> (blob); +} diff --git a/rust/vetkeys/basic_bls_signing/rust/backend/src/lib.rs b/rust/vetkeys/basic_bls_signing/rust/backend/src/lib.rs new file mode 100644 index 000000000..59a644b7b --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/backend/src/lib.rs @@ -0,0 +1,145 @@ +pub mod types; +use candid::Principal; +use ic_cdk::management_canister::{VetKDCurve, VetKDKeyId, VetKDPublicKeyArgs}; +use ic_cdk::{init, query, update}; +use ic_stable_structures::{ + memory_manager::{MemoryId, MemoryManager, VirtualMemory}, + Cell as StableCell, DefaultMemoryImpl, StableBTreeMap, +}; +use serde_bytes::ByteBuf; +use std::cell::RefCell; +use types::Signature; + +type Memory = VirtualMemory; + +type VetKeyPublicKey = ByteBuf; +type RawSignature = ByteBuf; +type RawMessage = String; +type Timestamp = u64; + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static SIGNATURES: 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(2))), + 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"); + }); +} + +#[update] +async fn sign_message(message: RawMessage) -> RawSignature { + let signer = ic_cdk::api::msg_caller(); + let signature_bytes = ic_vetkeys::management_canister::sign_with_bls( + message.as_bytes().to_vec(), + context(&signer), + key_id(), + ) + .await + .expect("ic_vetkeys' sign_with_bls failed"); + + SIGNATURES.with_borrow_mut(|sigs| { + let timestamp = ic_cdk::api::time(); + let sig = Signature { + message, + signature: signature_bytes.clone(), + timestamp, + }; + + // In rare cases a user may call `sign_message` in quick succession + // so that multiple signature requests are in a single consensus round, + // which leads to ic_cdk::api::time() returning the same value for all + // of the requests. In that case, we just keep increasing the timestamp + // used in the map key until we hit a slot this is available. + let mut timestamp_for_mapkey = timestamp; + while sigs.get(&(signer, timestamp_for_mapkey)).is_some() { + timestamp_for_mapkey += 1; + } + + assert!(sigs.insert((signer, timestamp_for_mapkey), sig).is_none()); + }); + + ByteBuf::from(signature_bytes) +} + +#[query] +fn get_my_signatures() -> Vec { + let me = ic_cdk::api::msg_caller(); + SIGNATURES.with_borrow(|signer_and_timestamp_to_sig| { + signer_and_timestamp_to_sig + .range((me, 0)..) + .take_while(|((signer, _ts), _sig)| signer == &me) + .map(|((_, _), sig)| sig) + .collect() + }) +} + +#[update] +async fn get_my_verification_key() -> VetKeyPublicKey { + let request = VetKDPublicKeyArgs { + canister_id: None, + context: context(&ic_cdk::api::msg_caller()), + key_id: key_id(), + }; + let result = ic_cdk::management_canister::vetkd_public_key(&request) + .await + .expect("call to vetkd_public_key failed"); + + VetKeyPublicKey::from(result.public_key) +} + +fn context(signer: &Principal) -> Vec { + // A domain separator is not strictly necessary in this dapp, but having one is considered a good practice. + const DOMAIN_SEPARATOR: [u8; 22] = *b"basic_bls_signing_dapp"; + const DOMAIN_SEPARATOR_LENGTH: u8 = DOMAIN_SEPARATOR.len() as u8; + [DOMAIN_SEPARATOR_LENGTH] + .into_iter() + .chain(DOMAIN_SEPARATOR) + .chain(signer.as_ref().iter().cloned()) + .collect() +} + +fn key_id() -> VetKDKeyId { + VetKDKeyId { + curve: VetKDCurve::Bls12_381_G2, + name: KEY_NAME.with_borrow(|key_name| key_name.get().clone()), + } +} + +// In the following, we register a custom getrandom implementation because +// otherwise getrandom (which is a dependency of some other dependencies) fails to compile. +// This is necessary because getrandom by default fails to compile for the +// wasm32-unknown-unknown target (which is required for deploying a canister). +// Our custom implementation always fails, which is sufficient here because +// the used RNGs are _manually_ seeded rather than by the system. +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +getrandom::register_custom_getrandom!(always_fail); +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) +} + +ic_cdk::export_candid!(); diff --git a/rust/vetkeys/basic_bls_signing/rust/backend/src/types.rs b/rust/vetkeys/basic_bls_signing/rust/backend/src/types.rs new file mode 100644 index 000000000..dd27e10ac --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/backend/src/types.rs @@ -0,0 +1,28 @@ +use std::borrow::Cow; + +use candid::{CandidType, Principal}; +use ic_stable_structures::{storable::Bound, Storable}; +use serde::{Deserialize, Serialize}; + +pub type CanisterId = Principal; + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + +pub struct Signature { + pub message: String, + #[serde(with = "serde_bytes")] + pub signature: Vec, + pub timestamp: u64, +} + +impl Storable for Signature { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} diff --git a/rust/vetkeys/basic_bls_signing/rust/dfx.json b/rust/vetkeys/basic_bls_signing/rust/dfx.json new file mode 100644 index 000000000..fec54f439 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/dfx.json @@ -0,0 +1,45 @@ +{ + "canisters": { + "basic_bls_signing": { + "candid": "backend/backend.did", + "package": "ic-vetkeys-example-basic-bls-signing-backend", + "type": "rust", + "init_arg": "(\"test_key_1\")", + "metadata": [ + { + "name": "candid:service", + "visibility": "public" + } + ] + }, + "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": ["basic_bls_signing", "internet-identity"], + "build": [ + "cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./" + ], + "frontend": { + "entrypoint": "dist/index.html" + }, + "source": ["dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + } +} diff --git a/rust/vetkeys/basic_bls_signing/rust/frontend b/rust/vetkeys/basic_bls_signing/rust/frontend new file mode 120000 index 000000000..af288785f --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/frontend @@ -0,0 +1 @@ +../frontend \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml b/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml new file mode 120000 index 000000000..4e9e6489d --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml @@ -0,0 +1 @@ +../../../rust-toolchain.toml \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/ui_screenshot.png b/rust/vetkeys/basic_bls_signing/ui_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..a59025e841438525856312045612fb7cead31fa9 GIT binary patch literal 109893 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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%0A}<+BEZVef1~Ccs?|q2w7$d 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 v-zANo*BX4e + + + + + + VetKeys: Basic IBE + + +
+ + + diff --git a/rust/vetkeys/basic_ibe/frontend/package.json b/rust/vetkeys/basic_ibe/frontend/package.json new file mode 100644 index 000000000..ca7932ea7 --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/package.json @@ -0,0 +1,31 @@ +{ + "name": "basic-ibe-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "npm run build:bindings && vite", + "build": "npm run build:bindings && tsc && vite build", + "build:bindings": "cd scripts && ./gen_bindings.sh", + "preview": "vite preview", + "lint": "eslint" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^24.0.4", + "eslint": "^9.24.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.0", + "tslib": "^2.8.1", + "typescript": "~5.7.2", + "typescript-eslint": "^8.35.1", + "vite": "^6.4.1", + "vite-plugin-environment": "^1.1.3" + }, + "dependencies": { + "@dfinity/auth-client": "^2.4.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/vetkeys": "^0.3.0" + } +} diff --git a/rust/vetkeys/basic_ibe/frontend/public/.ic-assets.json5 b/rust/vetkeys/basic_ibe/frontend/public/.ic-assets.json5 new file mode 100644 index 000000000..2997d66d2 --- /dev/null +++ b/rust/vetkeys/basic_ibe/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';object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + }, + allow_raw_access: false + }, +] diff --git a/rust/vetkeys/basic_ibe/frontend/public/vite.svg b/rust/vetkeys/basic_ibe/frontend/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh b/rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh new file mode 100755 index 000000000..91ce67a86 --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +cd ../../backend && make extract-candid + +cd .. && dfx generate basic_ibe || exit 1 + +rm -r frontend/src/declarations/basic_ibe > /dev/null 2>&1 || true + +mkdir -p frontend/src/declarations/basic_ibe +mv src/declarations/basic_ibe frontend/src/declarations +rmdir -p src/declarations > /dev/null 2>&1 || true + +# dfx 0.31+ generates @icp-sdk/core imports; rewrite to @dfinity/* to match deps +find frontend/src/declarations -type f \( -name '*.ts' -o -name '*.js' \) -exec \ + perl -i -pe 's|\@icp-sdk/core/agent|\@dfinity/agent|g; s|\@icp-sdk/core/principal|\@dfinity/principal|g; s|\@icp-sdk/core/candid|\@dfinity/candid|g' {} + \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/frontend/src/main.ts b/rust/vetkeys/basic_ibe/frontend/src/main.ts new file mode 100644 index 000000000..d15d165f5 --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/src/main.ts @@ -0,0 +1,339 @@ +import "./style.css"; +import { createActor } from "./declarations/basic_ibe"; +import { Principal } from "@dfinity/principal"; +import { + TransportSecretKey, + DerivedPublicKey, + EncryptedVetKey, + VetKey, + IbeCiphertext, + IbeIdentity, + IbeSeed, +} from "@dfinity/vetkeys"; +import { Inbox, _SERVICE } from "./declarations/basic_ibe/basic_ibe.did"; +import { AuthClient } from "@dfinity/auth-client"; +import type { ActorSubclass } from "@dfinity/agent"; + +let ibePrivateKey: VetKey | undefined = undefined; +let ibePublicKey: DerivedPublicKey | undefined = undefined; +let myPrincipal: Principal | undefined = undefined; +let authClient: AuthClient | undefined; +let basicIbeCanister: ActorSubclass<_SERVICE> | undefined; + +function getBasicIbeCanister(): ActorSubclass<_SERVICE> { + if (basicIbeCanister) return basicIbeCanister; + if (!process.env.CANISTER_ID_BASIC_IBE) { + throw Error("CANISTER_ID_BASIC_IBE is not set"); + } + if (!authClient) { + throw Error("Auth client is not initialized"); + } + const host = + process.env.DFX_NETWORK === "ic" + ? `https://${process.env.CANISTER_ID_BASIC_IBE}.ic0.app` + : "http://localhost:8000"; + + basicIbeCanister = createActor(process.env.CANISTER_ID_BASIC_IBE, { + agentOptions: { + identity: authClient.getIdentity(), + host, + }, + }); + + return basicIbeCanister!; +} + +async function getIbePublicKey(): Promise { + if (ibePublicKey) return ibePublicKey; + ibePublicKey = DerivedPublicKey.deserialize( + new Uint8Array(await getBasicIbeCanister().get_ibe_public_key()), + ); + return ibePublicKey; +} + +async function encrypt( + cleartext: Uint8Array, + receiver: Principal, +): Promise { + const publicKey = await getIbePublicKey(); + const ciphertext = IbeCiphertext.encrypt( + publicKey, + IbeIdentity.fromPrincipal(receiver), + cleartext, + IbeSeed.random(), + ); + return ciphertext.serialize(); +} + +async function getMyIbePrivateKey(): Promise { + if (ibePrivateKey) return ibePrivateKey; + + if (!myPrincipal) { + throw Error("My principal is not set"); + } else { + const transportSecretKey = TransportSecretKey.random(); + const encryptedKey = Uint8Array.from( + await getBasicIbeCanister().get_my_encrypted_ibe_key( + transportSecretKey.publicKeyBytes(), + ), + ); + ibePrivateKey = EncryptedVetKey.deserialize( + encryptedKey, + ).decryptAndVerify( + transportSecretKey, + await getIbePublicKey(), + new Uint8Array(myPrincipal.toUint8Array()), + ); + return ibePrivateKey; + } +} + +async function decryptMessage(encryptedMessage: Uint8Array): Promise { + const ibeKey = await getMyIbePrivateKey(); + const ciphertext = IbeCiphertext.deserialize(encryptedMessage); + const plaintext = ciphertext.decrypt(ibeKey); + return new TextDecoder().decode(plaintext); +} + +async function sendMessage() { + const message = prompt("Enter your message:"); + if (!message) throw Error("Message is required"); + + const receiver = prompt("Enter receiver principal:"); + if (!receiver) throw Error("Receiver is required"); + + const receiverPrincipal = Principal.fromText(receiver); + + try { + const encryptedMessage = await encrypt( + new TextEncoder().encode(message), + receiverPrincipal, + ); + + const result = await getBasicIbeCanister().send_message({ + encrypted_message: encryptedMessage, + receiver: receiverPrincipal, + }); + + if ("Err" in result) { + alert("Error sending message: " + result.Err); + } else { + alert("Message sent successfully!"); + } + } catch (error) { + alert("Error sending message: " + (error as Error).message); + } +} + +async function showMessages() { + const inbox = await getBasicIbeCanister().get_my_messages(); + await displayMessages(inbox); +} + +function createMessageElement( + sender: Principal, + timestamp: bigint, + plaintextString: string, + index: number, +): HTMLDivElement { + const messageElement = document.createElement("div"); + messageElement.className = "message"; + + const messageContent = document.createElement("div"); + messageContent.className = "message-content"; + + const messageText = document.createElement("div"); + messageText.className = "message-text"; + messageText.textContent = plaintextString; + + const messageInfo = document.createElement("div"); + messageInfo.className = "message-info"; + + const senderInfo = document.createElement("div"); + senderInfo.className = "sender"; + senderInfo.textContent = `From: ${sender.toString()}`; + + const timestampInfo = document.createElement("div"); + timestampInfo.className = "timestamp"; + const date = new Date(Number(timestamp) / 1_000_000); + timestampInfo.textContent = `Sent: ${date.toLocaleString()}`; + + const messageActions = document.createElement("div"); + messageActions.className = "message-actions"; + + const deleteButton = document.createElement("button"); + deleteButton.className = "delete-button"; + deleteButton.textContent = "Delete"; + deleteButton.dataset.index = index.toString(); + + messageActions.appendChild(deleteButton); + messageInfo.appendChild(senderInfo); + messageInfo.appendChild(timestampInfo); + messageContent.appendChild(messageText); + messageContent.appendChild(messageInfo); + messageContent.appendChild(messageActions); + messageElement.appendChild(messageContent); + + return messageElement; +} + +async function displayMessages(inbox: Inbox) { + const messagesDiv = document.getElementById("messages")!; + messagesDiv.innerHTML = ""; + + if (inbox.messages.length === 0) { + const noMessagesDiv = document.createElement("div"); + noMessagesDiv.className = "no-messages"; + noMessagesDiv.textContent = "No messages in the inbox."; + messagesDiv.appendChild(noMessagesDiv); + return; + } + + // Iterate through messages in reverse order + for (let i = inbox.messages.length - 1; i >= 0; i--) { + const message = inbox.messages[i]; + const plaintextString = await decryptMessage( + new Uint8Array(message.encrypted_message), + ); + + const messageElement = createMessageElement( + message.sender, + message.timestamp, + plaintextString, + i, + ); + messagesDiv.appendChild(messageElement); + } + + // Add event listeners to delete buttons + const deleteButtons = document.querySelectorAll(".delete-button"); + deleteButtons.forEach((button) => { + button.addEventListener("click", (e) => { + const target = e.target as HTMLButtonElement; + const index = parseInt(target.dataset.index!); + + // Disable all delete buttons + deleteButtons.forEach( + (btn) => ((btn as HTMLButtonElement).disabled = true), + ); + + void (async () => { + try { + const result = + await getBasicIbeCanister().remove_my_message_by_index( + BigInt(index), + ); + if ("Err" in result) { + alert("Error deleting message: " + result.Err); + } else { + // Re-load all messages to refresh message indices + await showMessages(); + } + } catch (error) { + alert( + "Error deleting message: " + (error as Error).message, + ); + } + })(); + }); + }); +} + +export function login(client: AuthClient) { + void 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: () => { + myPrincipal = client.getIdentity().getPrincipal(); + updateUI(true); + }, + onError: (error) => { + alert("Authentication failed: " + error); + }, + }); +} + +export function logout() { + void authClient?.logout(); + const messagesDiv = document.getElementById("messages")!; + messagesDiv.innerHTML = ""; + ibePrivateKey = undefined; + myPrincipal = undefined; + basicIbeCanister = undefined; + updateUI(false); +} + +async function initAuth() { + authClient = await AuthClient.create(); + const isAuthenticated = await authClient.isAuthenticated(); + + if (isAuthenticated) { + myPrincipal = authClient.getIdentity().getPrincipal(); + updateUI(true); + } else { + updateUI(false); + } +} + +function updateUI(isAuthenticated: boolean) { + const loginButton = document.getElementById("loginButton")!; + const messageButtons = document.getElementById("messageButtons")!; + const principalDisplay = document.getElementById("principalDisplay")!; + const logoutButton = document.getElementById("logoutButton")!; + + loginButton.classList.toggle("hidden", isAuthenticated); + messageButtons.classList.toggle("hidden", !isAuthenticated); + principalDisplay.classList.toggle("hidden", !isAuthenticated); + logoutButton.classList.toggle("hidden", !isAuthenticated); + + if (isAuthenticated && myPrincipal) { + principalDisplay.textContent = `Principal: ${myPrincipal.toString()}`; + } +} + +function handleLogin() { + if (!authClient) { + alert("Auth client not initialized"); + return; + } + + login(authClient); +} + +document.querySelector("#app")!.innerHTML = ` +
+

Basic IBE Message System with VetKeys

+
+
+ +
+ +
+ + +
+
+
+`; + +// Add event listeners +document.getElementById("loginButton")!.addEventListener("click", handleLogin); +document.getElementById("logoutButton")!.addEventListener("click", logout); +document.getElementById("sendMessage")!.addEventListener("click", () => { + void (async () => { + await sendMessage(); + })(); +}); +document.getElementById("showMessages")!.addEventListener("click", () => { + void (async () => { + await showMessages(); + })(); +}); + +// Initialize auth +void initAuth(); diff --git a/rust/vetkeys/basic_ibe/frontend/src/style.css b/rust/vetkeys/basic_ibe/frontend/src/style.css new file mode 100644 index 000000000..cde19eb89 --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/src/style.css @@ -0,0 +1,304 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 2.2em; + line-height: 1.1; + margin-bottom: 2rem; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #3178c6aa); +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} + +.buttons { + display: flex; + gap: 1rem; + justify-content: center; + margin-bottom: 2rem; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +#messages { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 2rem; +} + +.message { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1rem; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #f9f9f9; +} + +.message input[type="checkbox"] { + width: 1.2em; + height: 1.2em; + margin-top: 0.3em; +} + +.message-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.message-text { + font-size: 1.1em; + line-height: 1.4; + margin-bottom: 0.5rem; + color: #000; +} + +.message-info { + display: flex; + flex-direction: column; + gap: 0.2rem; + font-size: 0.9em; + color: #666; +} + +.sender { + font-weight: 500; +} + +.timestamp { + color: #888; +} + +.message-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.delete-button { + padding: 0.25rem 0.5rem; + font-size: 0.9em; + background-color: #dc3545; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.delete-button:hover { + background-color: #c82333; +} + +.delete-button:disabled { + background-color: #6c757d; + cursor: not-allowed; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +.principal-container { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.principal-display { + font-family: monospace; + background-color: rgba(0, 0, 0, 0.05); + padding: 0.5rem; + border-radius: 4px; + color: #a8a6a6; + white-space: pre-wrap; + word-break: break-all; + max-width: 600px; +} + +#logoutButton { + padding: 0.5rem 1rem; + background-color: #dc3545; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +#logoutButton:hover { + background-color: #c82333; +} + +.login-container { + display: flex; + justify-content: center; + margin: 20px 0; +} + +#loginButton { + padding: 10px 20px; + font-size: 16px; + cursor: pointer; +} + +.no-messages { + text-align: center; + padding: 2rem; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 8px; + color: #666; + font-size: 1.1em; + margin: 1rem 0; +} + +.message.deleted { + border-color: #dc3545; + background-color: rgba(220, 53, 69, 0.1); +} + +.message.deleted .message-text { + color: #dc3545; +} + +.message.deleted .deleted-label { + display: inline-block; + background-color: #dc3545; + color: white; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.8em; + margin-bottom: 0.5rem; +} + +.message.new { + border-color: #0d6efd; + background-color: rgba(13, 110, 253, 0.1); +} + +.message.new .message-text { + color: #0d6efd; +} + +.message.new .deleted-label { + background-color: #0d6efd; +} + +.message.deleted-new { + border-color: #dc3545; + background-color: rgba(13, 110, 253, 0.1); +} + +.message.deleted-new .message-text { + color: #0d6efd; +} + +.message.deleted-new .deleted-label { + background-color: #0d6efd; +} + +/* Initial state classes for auth elements */ +#loginButton { + display: block; +} + +#messageButtons { + display: flex; +} + +#principalDisplay { + display: block; +} + +#logoutButton { + display: block; +} + +/* Auth state classes */ +.hidden { + display: none !important; +} diff --git a/rust/vetkeys/basic_ibe/frontend/src/vite-env.d.ts b/rust/vetkeys/basic_ibe/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/rust/vetkeys/basic_ibe/frontend/tsconfig.json b/rust/vetkeys/basic_ibe/frontend/tsconfig.json new file mode 100644 index 000000000..a4883f28e --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/rust/vetkeys/basic_ibe/frontend/vite.config.ts b/rust/vetkeys/basic_ibe/frontend/vite.config.ts new file mode 100644 index 000000000..27bf81575 --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import typescript from '@rollup/plugin-typescript'; +import environment from 'vite-plugin-environment'; +import path from 'path'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + typescript({ + inlineSources: true, + }), + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + root: "./", + server: { + hmr: false + } +}) \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo b/rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo new file mode 100644 index 000000000..71f3bd6ba --- /dev/null +++ b/rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo @@ -0,0 +1,169 @@ +import Principal "mo:core/Principal"; +import Time "mo:core/Time"; +import Map "mo:core/Map"; +import Text "mo:core/Text"; +import Blob "mo:core/Blob"; +import Array "mo:core/Array"; +import List "mo:core/List"; +import Nat64 "mo:core/Nat64"; +import Nat "mo:core/Nat"; +import Result "mo:core/Result"; +import Int "mo:core/Int"; + +persistent actor class (keyNameString : Text) { + // Types + type Message = { + sender : Principal; + encrypted_message : Blob; + timestamp : Nat64; + }; + + type Inbox = { + messages : [Message]; + }; + + type SendMessageRequest = { + receiver : Principal; + encrypted_message : Blob; + }; + + type Result = { + #Ok : T; + #Err : E; + }; + + type VetKdKeyId = { + curve : { #bls12_381_g2 }; + name : Text; + }; + + type VetKdPublicKeyArgs = { + canister_id : ?Principal; + context : Blob; + key_id : VetKdKeyId; + }; + + type VetKdDeriveKeyArgs = { + context : Blob; + input : Blob; + key_id : VetKdKeyId; + transport_public_key : Blob; + }; + + type VetKdSystemApi = actor { + vetkd_public_key : (VetKdPublicKeyArgs) -> async { public_key : Blob }; + vetkd_derive_key : (VetKdDeriveKeyArgs) -> async { + encrypted_key : Blob; + }; + }; + + // Constants + let MAX_MESSAGES_PER_INBOX : Nat = 1000; + let DOMAIN_SEPARATOR : Text = "basic_ibe_example_dapp"; + + // State + var inboxes = Map.empty(); + + // Management canister actor + let vetKdSystemApi : VetKdSystemApi = actor ("aaaaa-aa"); + + // Send a message to a receiver + public shared ({ caller }) func send_message(request : SendMessageRequest) : async Result<(), Text> { + let message : Message = { + sender = caller; + encrypted_message = request.encrypted_message; + timestamp = Nat64.fromNat(Int.abs(Time.now())); + }; + + let receiver = request.receiver; + let current_inbox = switch (Map.get(inboxes, Principal.compare, receiver)) { + case (?inbox) { inbox }; + case null { { messages = [] } }; + }; + + if (current_inbox.messages.size() >= MAX_MESSAGES_PER_INBOX) { + return #Err("Inbox for " # Principal.toText(receiver) # " is full"); + }; + + let new_messages = Array.concat(current_inbox.messages, [message]); + let new_inbox : Inbox = { messages = new_messages }; + ignore Map.insert(inboxes, Principal.compare, receiver, new_inbox); + + #Ok(); + }; + + // Get the IBE public key + public shared func get_ibe_public_key() : async Blob { + let key_id : VetKdKeyId = { + curve = #bls12_381_g2; + name = keyNameString; + }; + + let context = Text.encodeUtf8(DOMAIN_SEPARATOR); + let request : VetKdPublicKeyArgs = { + canister_id = null; + context = context; + key_id = key_id; + }; + + let result = await vetKdSystemApi.vetkd_public_key(request); + result.public_key; + }; + + // Get the caller's encrypted IBE key + public shared ({ caller }) func get_my_encrypted_ibe_key(transport_key : Blob) : async Blob { + let key_id : VetKdKeyId = { + curve = #bls12_381_g2; + name = keyNameString; + }; + + let context = Text.encodeUtf8(DOMAIN_SEPARATOR); + let input = Principal.toBlob(caller); + let request : VetKdDeriveKeyArgs = { + context = context; + input = input; + key_id = key_id; + transport_public_key = transport_key; + }; + + let result = await (with cycles = 26_153_846_153) vetKdSystemApi.vetkd_derive_key(request); + result.encrypted_key; + }; + + // Get the caller's messages + public shared query ({ caller }) func get_my_messages() : async Inbox { + switch (Map.get(inboxes, Principal.compare, caller)) { + case (?inbox) { inbox }; + case null { { messages = [] } }; + }; + }; + + // Remove a message by index + public shared ({ caller }) func remove_my_message_by_index(message_index : Nat64) : async Result<(), Text> { + let current_inbox = switch (Map.get(inboxes, Principal.compare, caller)) { + case (?inbox) { inbox }; + case null { { messages = [] } }; + }; + + let index = Nat64.toNat(message_index); + if (index >= current_inbox.messages.size()) { + return #Err("Message index out of bounds"); + }; + + // Create a new array without the specified index + let messages = current_inbox.messages; + let new_messages_list = List.empty(); + + for (i in messages.keys()) { + if (i != index) { + List.add(new_messages_list, messages[i]); + }; + }; + + let new_messages = List.toArray(new_messages_list); + let new_inbox : Inbox = { messages = new_messages }; + ignore Map.insert(inboxes, Principal.compare, caller, new_inbox); + + #Ok(); + }; +}; diff --git a/rust/vetkeys/basic_ibe/motoko/dfx.json b/rust/vetkeys/basic_ibe/motoko/dfx.json new file mode 100644 index 000000000..438929689 --- /dev/null +++ b/rust/vetkeys/basic_ibe/motoko/dfx.json @@ -0,0 +1,49 @@ +{ + "canisters": { + "basic_ibe": { + "main": "backend/src/Main.mo", + "type": "motoko", + "args": "--enhanced-orthogonal-persistence", + "init_arg": "(\"test_key_1\")", + "metadata": [ + { + "name": "candid:service", + "visibility": "public" + } + ] + }, + "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": ["basic_ibe", "internet-identity"], + "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"], + "frontend": { + "entrypoint": "dist/index.html" + }, + "source": ["dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + }, + "defaults": { + "build": { + "packtool": "npx ic-mops sources", + "args": "" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + } + } \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/motoko/frontend b/rust/vetkeys/basic_ibe/motoko/frontend new file mode 120000 index 000000000..af288785f --- /dev/null +++ b/rust/vetkeys/basic_ibe/motoko/frontend @@ -0,0 +1 @@ +../frontend \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/motoko/mops.toml b/rust/vetkeys/basic_ibe/motoko/mops.toml new file mode 100644 index 000000000..7de5a57da --- /dev/null +++ b/rust/vetkeys/basic_ibe/motoko/mops.toml @@ -0,0 +1,3 @@ +[dependencies] +core = "1.0.0" +ic-vetkeys = "0.3.0" diff --git a/rust/vetkeys/basic_ibe/rust/Cargo.toml b/rust/vetkeys/basic_ibe/rust/Cargo.toml new file mode 100644 index 000000000..d1e49e317 --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["backend"] +resolver = "2" diff --git a/rust/vetkeys/basic_ibe/rust/backend/Cargo.toml b/rust/vetkeys/basic_ibe/rust/backend/Cargo.toml new file mode 100644 index 000000000..3a46c29ca --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/backend/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ic-vetkd-example-basic-ibe-backend" +authors = ["DFINITY Stiftung"] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "Basic Identity Based Encryption" +repository = "https://github.com/dfinity/vetkeys" +rust-version = "1.85.0" + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10.2" +ic-cdk = "0.18.3" +ic-dummy-getrandom-for-wasm = "0.1.0" +ic-stable-structures = "0.6.8" +ic-vetkeys = "0.3.0" +serde = "1.0.217" +serde_bytes = "0.11.15" +serde_cbor = "0.11.2" +serde_with = "3.11.0" diff --git a/rust/vetkeys/basic_ibe/rust/backend/Makefile b/rust/vetkeys/basic_ibe/rust/backend/Makefile new file mode 100644 index 000000000..dc7cfae2d --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/backend/Makefile @@ -0,0 +1,15 @@ +.PHONY: compile-wasm +.SILENT: compile-wasm +compile-wasm: + cargo build --release --target wasm32-unknown-unknown + +.PHONY: extract-candid +.SILENT: extract-candid +extract-candid: compile-wasm + candid-extractor ../target/wasm32-unknown-unknown/release/ic_vetkd_example_basic_ibe_backend.wasm > backend.did + +.PHONY: clean +.SILENT: clean +clean: + cargo clean + rm -rf ../dfx \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/rust/backend/backend.did b/rust/vetkeys/basic_ibe/rust/backend/backend.did new file mode 100644 index 000000000..70a70f982 --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/backend/backend.did @@ -0,0 +1,18 @@ +type Inbox = record { messages : vec Message }; +type Message = record { + sender : principal; + timestamp : nat64; + encrypted_message : blob; +}; +type Result = variant { Ok; Err : text }; +type SendMessageRequest = record { + encrypted_message : blob; + receiver : principal; +}; +service : (text) -> { + get_ibe_public_key : () -> (blob); + get_my_encrypted_ibe_key : (blob) -> (blob); + get_my_messages : () -> (Inbox) query; + remove_my_message_by_index : (nat64) -> (Result); + send_message : (SendMessageRequest) -> (Result); +} diff --git a/rust/vetkeys/basic_ibe/rust/backend/src/lib.rs b/rust/vetkeys/basic_ibe/rust/backend/src/lib.rs new file mode 100644 index 000000000..be32c1118 --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/backend/src/lib.rs @@ -0,0 +1,131 @@ +use candid::Principal; +use ic_cdk::management_canister::{VetKDCurve, VetKDDeriveKeyArgs, VetKDKeyId, VetKDPublicKeyArgs}; +use ic_cdk::{init, query, update}; +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; +use ic_stable_structures::{BTreeMap as StableBTreeMap, Cell as StableCell, DefaultMemoryImpl}; +use serde_bytes::ByteBuf; +use std::cell::RefCell; + +mod types; +use types::*; + +type Memory = VirtualMemory; +type EncryptedVetKey = ByteBuf; +type VetKeyPublicKey = ByteBuf; +type TransportPublicKey = ByteBuf; + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + static INBOXES: RefCell> = RefCell::new(StableBTreeMap::init( + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), + )); + 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")); +} + +static DOMAIN_SEPARATOR: &str = "basic_ibe_example_dapp"; + +#[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"); + }); +} + +#[update] +fn send_message(request: SendMessageRequest) -> Result<(), String> { + let sender = ic_cdk::api::msg_caller(); + let SendMessageRequest { + receiver, + encrypted_message, + } = request; + let timestamp = ic_cdk::api::time(); + + let message = Message { + sender, + encrypted_message, + timestamp, + }; + + INBOXES.with_borrow_mut(|inboxes| { + let mut inbox = inboxes.get(&receiver).unwrap_or_default(); + + if inbox.messages.len() >= MAX_MESSAGES_PER_INBOX { + Err(format!("Inbox for {} is full", receiver)) + } else { + inbox.messages.push(message); + inboxes.insert(receiver, inbox); + Ok(()) + } + }) +} + +#[update] +async fn get_ibe_public_key() -> VetKeyPublicKey { + let request = VetKDPublicKeyArgs { + canister_id: None, + context: DOMAIN_SEPARATOR.as_bytes().to_vec(), + key_id: key_id(), + }; + + let result = ic_cdk::management_canister::vetkd_public_key(&request) + .await + .expect("call to vetkd_public_key failed"); + + VetKeyPublicKey::from(result.public_key) +} + +#[update] +/// Retrieves the caller's encrypted private IBE key for message decryption. +async fn get_my_encrypted_ibe_key(transport_key: TransportPublicKey) -> EncryptedVetKey { + let caller = ic_cdk::api::msg_caller(); + let request = VetKDDeriveKeyArgs { + input: caller.as_ref().to_vec(), + context: DOMAIN_SEPARATOR.as_bytes().to_vec(), + key_id: key_id(), + transport_public_key: transport_key.into_vec(), + }; + + let result = ic_cdk::management_canister::vetkd_derive_key(&request) + .await + .expect("call to vetkd_derive_key failed"); + + EncryptedVetKey::from(result.encrypted_key) +} + +#[query] +fn get_my_messages() -> Inbox { + let caller = ic_cdk::api::msg_caller(); + INBOXES.with_borrow(|inboxes| inboxes.get(&caller).unwrap_or_default()) +} + +#[update] +fn remove_my_message_by_index(message_index: usize) -> Result<(), String> { + let caller = ic_cdk::api::msg_caller(); + INBOXES.with_borrow_mut(|inboxes| { + let mut inbox = inboxes.get(&caller).unwrap_or_default(); + if message_index >= inbox.messages.len() { + Err("Message index out of bounds".to_string()) + } else { + inbox.messages.remove(message_index); + inboxes.insert(caller, inbox); + Ok(()) + } + }) +} + +fn key_id() -> VetKDKeyId { + VetKDKeyId { + curve: VetKDCurve::Bls12_381_G2, + name: KEY_NAME.with_borrow(|key_name| key_name.get().clone()), + } +} + +ic_cdk::export_candid!(); diff --git a/rust/vetkeys/basic_ibe/rust/backend/src/types.rs b/rust/vetkeys/basic_ibe/rust/backend/src/types.rs new file mode 100644 index 000000000..81204ee31 --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/backend/src/types.rs @@ -0,0 +1,50 @@ +use candid::{CandidType, Principal}; +use ic_stable_structures::{storable::Bound, Storable}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; + +pub const MAX_MESSAGES_PER_INBOX: usize = 1000; + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Message { + pub sender: Principal, + #[serde(with = "serde_bytes")] + pub encrypted_message: Vec, + pub timestamp: u64, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Default)] +pub struct Inbox { + pub messages: Vec, +} + +impl Storable for Inbox { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} + +impl Storable for Message { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} + +#[derive(CandidType, Deserialize, Clone, Debug)] +pub struct SendMessageRequest { + pub receiver: Principal, + #[serde(with = "serde_bytes")] + pub encrypted_message: Vec, +} diff --git a/rust/vetkeys/basic_ibe/rust/dfx.json b/rust/vetkeys/basic_ibe/rust/dfx.json new file mode 100644 index 000000000..09ba32f7e --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/dfx.json @@ -0,0 +1,43 @@ +{ + "canisters": { + "basic_ibe": { + "candid": "backend/backend.did", + "package": "ic-vetkd-example-basic-ibe-backend", + "type": "rust", + "init_arg": "(\"test_key_1\")", + "metadata": [ + { + "name": "candid:service", + "visibility": "public" + } + ] + }, + "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": ["basic_ibe", "internet-identity"], + "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"], + "frontend": { + "entrypoint": "dist/index.html" + }, + "source": ["dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + } + } \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/rust/frontend b/rust/vetkeys/basic_ibe/rust/frontend new file mode 120000 index 000000000..af288785f --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/frontend @@ -0,0 +1 @@ +../frontend \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml b/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml new file mode 120000 index 000000000..4e9e6489d --- /dev/null +++ b/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml @@ -0,0 +1 @@ +../../../rust-toolchain.toml \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/ui_screenshot.png b/rust/vetkeys/basic_ibe/ui_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee53804b5e0d95172db7ce1da2fb76d6c48440f GIT binary patch literal 84227 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 backend.did + +.PHONY: clean +.SILENT: clean +clean: + cargo clean + rm -rf ../.dfx \ No newline at end of file diff --git a/rust/vetkeys/basic_timelock_ibe/backend/backend.did b/rust/vetkeys/basic_timelock_ibe/backend/backend.did new file mode 100644 index 000000000..303e567f2 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/backend/backend.did @@ -0,0 +1,27 @@ +type ClosedLotsResponse = record { + bids : vec vec record { principal; nat }; + lots : vec LotInformation; +}; +type LotInformation = record { + id : nat; + status : LotStatus; + creator : principal; + name : text; + description : text; + end_time : nat64; + start_time : nat64; +}; +type LotStatus = variant { Open; ClosedWithWinner : principal; ClosedNoBids }; +type OpenLotsResponse = record { + lots : vec LotInformation; + bidders : vec vec principal; +}; +type Result = variant { Ok : nat; Err : text }; +type Result_1 = variant { Ok; Err : text }; +service : (text) -> { + create_lot : (text, text, nat16) -> (Result); + get_ibe_public_key : () -> (blob); + get_lots : () -> (OpenLotsResponse, ClosedLotsResponse) query; + place_bid : (nat, blob) -> (Result_1); + start_lot_closing_timer_job_with_interval_secs : (nat64) -> (); +} diff --git a/rust/vetkeys/basic_timelock_ibe/backend/src/lib.rs b/rust/vetkeys/basic_timelock_ibe/backend/src/lib.rs new file mode 100644 index 000000000..1d0596645 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/backend/src/lib.rs @@ -0,0 +1,432 @@ +use crate::types::{ + BidCounter, DecryptedBid, EncryptedBid, LotId, LotInformation, VetKeyPublicKey, +}; +use candid::Principal; +use ic_cdk::management_canister::{VetKDCurve, VetKDDeriveKeyArgs, VetKDKeyId, VetKDPublicKeyArgs}; +use ic_cdk::{init, post_upgrade, query, update}; +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; +use ic_stable_structures::{BTreeMap as StableBTreeMap, Cell as StableCell, DefaultMemoryImpl}; +use ic_vetkeys::{DerivedPublicKey, EncryptedVetKey}; +use std::cell::RefCell; + +mod types; +use types::*; + +type Memory = VirtualMemory; + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + + static LOTS: RefCell> = RefCell::new(StableBTreeMap::init( + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(0))), + )); + /// The bids include a bid counter to ensure that if multiple users provide the same highest bid, the bid that was placed first wins. + /// The counter is not unique for a lot, it is monotonically increasing for all bids. + static BIDS_ON_LOTS: RefCell> = RefCell::new(StableBTreeMap::init( + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(1))), + )); + static OPEN_LOTS_DEADLINES: RefCell> = RefCell::new(StableBTreeMap::init( + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(2))), + )); + + static IBE_PUBLIC_KEY: RefCell> = const { RefCell::new(None) }; + + static BID_COUNTER: RefCell = const { RefCell::new(0) }; + + 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")); +} + +const DOMAIN_SEPARATOR: &str = "basic_timelock_ibe_example_dapp"; +const TIMER_INTERVAL_SECS: u64 = 5; + +#[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"); + }); + + start_lot_closing_timer_job_with_interval_secs(TIMER_INTERVAL_SECS); +} + +#[post_upgrade] +fn post_upgrade() { + start_lot_closing_timer_job_with_interval_secs(TIMER_INTERVAL_SECS); +} + +#[update(guard = "is_authenticated")] +fn create_lot(name: String, description: String, duration_seconds: u16) -> Result { + let caller = ic_cdk::api::msg_caller(); + + if duration_seconds == 0 { + return Err("Duration must be greater than 0".to_string()); + } + + let lot_id = LOTS.with_borrow_mut(|lots| { + let lot_id = lots.len() as u128; + + const NANOS_IN_SEC: u64 = 1_000_000_000; + let start_time = ic_cdk::api::time(); + + let lot = LotInformation { + id: lot_id, + name, + description, + start_time, + end_time: start_time + duration_seconds as u64 * NANOS_IN_SEC, + creator: caller, + status: LotStatus::Open, + }; + + OPEN_LOTS_DEADLINES.with_borrow_mut(|open_lots_deadlines| { + open_lots_deadlines.insert((lot.end_time, lot_id), ()); + }); + + lots.insert(lot_id, lot); + + lot_id + }); + + Ok(lot_id) +} + +#[update(guard = "is_authenticated")] +async fn get_ibe_public_key() -> VetKeyPublicKey { + if let Some(key) = IBE_PUBLIC_KEY.with_borrow(|key| key.clone()) { + return key; + } + + let request = VetKDPublicKeyArgs { + canister_id: None, + context: DOMAIN_SEPARATOR.as_bytes().to_vec(), + key_id: key_id(), + }; + + let result = ic_cdk::management_canister::vetkd_public_key(&request) + .await + .expect("call to vetkd_public_key failed"); + + IBE_PUBLIC_KEY.with_borrow_mut(|key| { + key.replace(VetKeyPublicKey::from(result.public_key.clone())); + }); + + VetKeyPublicKey::from(result.public_key) +} + +#[query(guard = "is_authenticated")] +fn get_lots() -> (OpenLotsResponse, ClosedLotsResponse) { + let mut open_lots = OpenLotsResponse::default(); + let mut closed_lots = ClosedLotsResponse::default(); + + LOTS.with_borrow(|lots| { + for (lot_id, lot) in lots.iter() { + match lot.status { + LotStatus::Open => { + open_lots.lots.push(lot); + let bidders: Vec = BIDS_ON_LOTS.with_borrow(|bids| { + bids.range((lot_id, 0, Principal::management_canister())..) + .take_while(|((this_lot_id, _, _), _)| *this_lot_id == lot_id) + .map(|((_, _, bidder), _)| bidder) + .collect() + }); + open_lots.bidders.push(bidders); + } + _ => { + closed_lots.lots.push(lot); + + let bids: Vec<(Principal, u128)> = BIDS_ON_LOTS.with_borrow(|bids| { + bids.range((lot_id, 0, Principal::management_canister())..) + .take_while(|((this_lot_id, _, _), _)| *this_lot_id == lot_id) + .map(|((_, _, bidder), bid)| match bid { + Bid::Encrypted(_) => { + panic!("bug: encrypted bid in a closed lot") + } + Bid::Decrypted(decrypted_bid) => (bidder, decrypted_bid.amount), + }) + .collect() + }); + closed_lots.bids.push(bids); + } + } + } + }); + (open_lots, closed_lots) +} + +#[update(guard = "is_authenticated")] +fn place_bid(lot_id: u128, encrypted_amount: Vec) -> Result<(), String> { + let bidder = ic_cdk::api::msg_caller(); + let now = ic_cdk::api::time(); + + LOTS.with_borrow(|lots| match lots.get(&lot_id) { + Some(LotInformation { + status: LotStatus::Open, + creator, + end_time, + .. + }) if creator != bidder && now < end_time => Ok(()), + Some(LotInformation { creator, .. }) if creator == bidder => { + Err("lot creator cannot bid".to_string()) + } + Some(_) => Err("lot is closed".to_string()), + None => Err("lot not found".to_string()), + })?; + + if encrypted_amount.len() > 1000 { + return Err("encrypted amount is too large to be valid".to_string()); + } + + BIDS_ON_LOTS.with_borrow_mut(|bids| { + if let Some((existing_bid_key, _existing_bid)) = bids + .range((lot_id, 0, Principal::management_canister())..) + .take_while(|((this_lot_id, _, _), _)| *this_lot_id == lot_id) + .find(|((_, _, this_bidder), _)| *this_bidder == bidder) + { + bids.remove(&existing_bid_key); + } + + let bid_counter = BID_COUNTER.with_borrow_mut(|bid_counter| { + let old_bid_counter = *bid_counter; + *bid_counter += 1; + old_bid_counter + }); + + bids.insert( + (lot_id, bid_counter, bidder), + Bid::Encrypted(EncryptedBid { + encrypted_amount, + bidder, + }), + ); + }); + + Ok(()) +} + +#[update(guard = "is_self_call")] +fn start_lot_closing_timer_job_with_interval_secs(secs: u64) { + let secs = std::time::Duration::from_secs(secs); + ic_cdk_timers::set_timer_interval(secs, || { + ic_cdk::futures::spawn(close_one_lot_if_any_is_open_and_expired()) + }); +} + +async fn close_one_lot_if_any_is_open_and_expired() { + // get the lot with the earliest deadline (if any) + let maybe_deadline_and_lot_id = + OPEN_LOTS_DEADLINES.with_borrow(|open_lots_deadlines| open_lots_deadlines.iter().next()); + + let lot_id = match maybe_deadline_and_lot_id { + // if there was a lot and its deadline has passed, remove it from the open lots deadlines to prevent double processing + Some(((deadline, lot_id), ())) if deadline <= ic_cdk::api::time() => { + OPEN_LOTS_DEADLINES.with_borrow_mut(|open_lots_deadlines| { + open_lots_deadlines + .remove(&(deadline, lot_id)) + .expect("failed to remove deadline from open lots deadlines") + }); + lot_id + } + // if there was no lot or the deadline has not passed, return without doing anything + _ => return, + }; + + let (bid_counters, encrypted_bids) = get_encrypted_bids_on_lot(lot_id); + let decrypted_bids = decrypt_bids(lot_id, encrypted_bids).await; + close_lot(lot_id, bid_counters, decrypted_bids); +} + +async fn decrypt_bids(lot_id: LotId, encrypted_bids: Vec) -> Vec { + let decrypted_values = decrypt_ciphertexts( + lot_id.to_le_bytes().to_vec(), + encrypted_bids + .iter() + .map(|bid| bid.encrypted_amount.as_slice()) + .collect::>(), + ) + .await; + + convert_decrypted_values_to_decrypted_bids(lot_id, encrypted_bids, decrypted_values) +} + +/// In the canister, using the IBE key derived from the identity decrypt a vector of ciphertexts, which makes them public. +/// Returns a vector, where each value is either a decrypted plaintext or an error message. +async fn decrypt_ciphertexts( + identity: Vec, + encrypted_values: Vec<&[u8]>, +) -> Vec, String>> { + let dummy_seed = vec![0; 32]; + let transport_secret_key = ic_vetkeys::TransportSecretKey::from_seed(dummy_seed.clone()) + .expect("failed to create transport secret key"); + + let request = VetKDDeriveKeyArgs { + context: DOMAIN_SEPARATOR.as_bytes().to_vec(), + input: identity.clone(), + key_id: key_id(), + transport_public_key: transport_secret_key.public_key().to_vec(), + }; + + let result = ic_cdk::management_canister::vetkd_derive_key(&request) + .await + .expect("call to vetkd_derive_key failed"); + + let ibe_public_key = + DerivedPublicKey::deserialize(&get_ibe_public_key().await.into_vec()).unwrap(); + let encrypted_vetkey = EncryptedVetKey::deserialize(&result.encrypted_key).unwrap(); + + let ibe_decryption_key = encrypted_vetkey + .decrypt_and_verify(&transport_secret_key, &ibe_public_key, identity.as_ref()) + .expect("failed to decrypt ibe key"); + + let mut decrypted_values = Vec::new(); + + for encrypted_value in encrypted_values.into_iter() { + let decrypted_value = ic_vetkeys::IbeCiphertext::deserialize(encrypted_value) + .map_err(|e| format!("failed to deserialize ibe ciphertext: {e}")) + .and_then(|c| { + c.decrypt(&ibe_decryption_key) + .map_err(|_| "failed to decrypt ibe ciphertext".to_string()) + }); + decrypted_values.push(decrypted_value); + } + decrypted_values +} + +fn convert_decrypted_values_to_decrypted_bids( + lot_id: LotId, + encrypted_bids: Vec, + decrypted_values: Vec, String>>, +) -> Vec { + let mut decrypted_bids = Vec::with_capacity(encrypted_bids.len()); + for decrypted_value in decrypted_values { + let decrypted_bid: Result = decrypted_value + .and_then(|v| { + v.as_slice() + .try_into() + .map_err(|_| "failed to convert amount to u128".to_string()) + }) + .map(u128::from_le_bytes); + decrypted_bids.push(decrypted_bid); + } + + encrypted_bids + .into_iter() + .zip(decrypted_bids) + .inspect(|(encrypted_bid, decrypted_bid)| { + if let Err(e) = decrypted_bid { + ic_cdk::println!( + "Failed to decrypt bid for lot id {lot_id} by {}: {e}", + encrypted_bid.bidder + ); + } + }) + .filter_map(|(encrypted_bid, decrypted_bid)| { + decrypted_bid.ok().map(|amount| DecryptedBid { + amount, + bidder: encrypted_bid.bidder, + }) + }) + .collect() +} + +fn get_encrypted_bids_on_lot(lot_id: LotId) -> (Vec, Vec) { + BIDS_ON_LOTS.with_borrow(|bids| { + bids.range((lot_id, 0, Principal::management_canister())..) + .take_while(|((this_lot_id, _, _), _)| *this_lot_id == lot_id) + .map(|((_, bid_counter, _), bid)| { + let encrypted_bid = match bid { + Bid::Encrypted(encrypted_bid) => encrypted_bid, + Bid::Decrypted(_) => panic!("bug: decrypted bid in a closed lot"), + }; + (bid_counter, encrypted_bid) + }) + .collect() + }) +} + +fn close_lot(lot_id: LotId, bid_counters: Vec, decrypted_bids: Vec) { + let status = match decrypted_bids + .iter() + .rev() // reverse the bids to get the *oldest* maximum bid + .max_by(|x, y| x.amount.cmp(&y.amount)) + { + Some(winner_bid) => LotStatus::ClosedWithWinner(winner_bid.bidder), + None => LotStatus::ClosedNoBids, + }; + + BIDS_ON_LOTS.with_borrow_mut(|bids| { + for (bid_counter, decrypted_bid) in bid_counters.into_iter().zip(decrypted_bids.into_iter()) + { + // replace the encrypted bid with the decrypted bid + bids.insert( + (lot_id, bid_counter, decrypted_bid.bidder), + Bid::Decrypted(decrypted_bid), + ); + } + }); + + LOTS.with_borrow_mut(|lots| { + let lot_information = lots.get(&lot_id).unwrap(); + lots.insert( + lot_id, + LotInformation { + id: lot_id, + status, + ..lot_information + }, + ); + }); +} + +fn is_authenticated() -> Result<(), String> { + let caller = ic_cdk::api::msg_caller(); + if caller != Principal::anonymous() { + Ok(()) + } else { + Err("the caller must be authenticated".to_string()) + } +} + +fn is_self_call() -> Result<(), String> { + if ic_cdk::api::msg_caller() == ic_cdk::api::canister_self() { + Ok(()) + } else { + Err("the caller must be the canister itself".to_string()) + } +} + +fn key_id() -> VetKDKeyId { + VetKDKeyId { + curve: VetKDCurve::Bls12_381_G2, + name: KEY_NAME.with_borrow(|key_name| key_name.get().clone()), + } +} + +// In the following, we register a custom getrandom implementation because +// otherwise getrandom (which is a dependency of some other dependencies) fails to compile. +// This is necessary because getrandom by default fails to compile for the +// wasm32-unknown-unknown target (which is required for deploying a canister). +// Our custom implementation always fails, which is sufficient here because +// the used RNGs are _manually_ seeded rather than by the system. +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +getrandom::register_custom_getrandom!(always_fail); +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown" +))] +fn always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) +} + +ic_cdk::export_candid!(); diff --git a/rust/vetkeys/basic_timelock_ibe/backend/src/types.rs b/rust/vetkeys/basic_timelock_ibe/backend/src/types.rs new file mode 100644 index 000000000..afbb6d1ef --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/backend/src/types.rs @@ -0,0 +1,109 @@ +use candid::{CandidType, Principal}; +use ic_stable_structures::{storable::Bound, Storable}; +use serde::{Deserialize, Serialize}; +use serde_bytes::ByteBuf; +use std::borrow::Cow; + +pub type LotId = u128; +pub type VetKeyPublicKey = ByteBuf; +pub type BidCounter = u128; + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct EncryptedBid { + #[serde(with = "serde_bytes")] + pub encrypted_amount: Vec, + pub bidder: Principal, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct DecryptedBid { + pub amount: u128, + pub bidder: Principal, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub enum Bid { + Encrypted(EncryptedBid), + Decrypted(DecryptedBid), +} + +impl Storable for EncryptedBid { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} + +impl Storable for DecryptedBid { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} + +impl Storable for Bid { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct LotInformation { + pub id: u128, + pub name: String, + pub description: String, + pub start_time: u64, + pub end_time: u64, + pub creator: Principal, + pub status: LotStatus, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum LotStatus { + /// The auction is still open + Open, + /// The auction is closed and the winner is the principal in the tuple + ClosedWithWinner(Principal), + /// The auction is closed and no bids were made + ClosedNoBids, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Default)] +pub struct OpenLotsResponse { + pub lots: Vec, + pub bidders: Vec>, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Default)] +pub struct ClosedLotsResponse { + pub lots: Vec, + pub bids: Vec>, +} + +impl Storable for LotInformation { + fn to_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_cbor::to_vec(self).expect("failed to serialize")) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_cbor::from_slice(&bytes).expect("failed to deserialize") + } + + const BOUND: Bound = Bound::Unbounded; +} diff --git a/rust/vetkeys/basic_timelock_ibe/dfx.json b/rust/vetkeys/basic_timelock_ibe/dfx.json new file mode 100644 index 000000000..65e4c4397 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/dfx.json @@ -0,0 +1,43 @@ +{ + "canisters": { + "basic_timelock_ibe": { + "candid": "backend/backend.did", + "package": "ic-vetkd-example-basic-timelock-ibe-backend", + "type": "rust", + "init_arg": "(\"test_key_1\")", + "metadata": [ + { + "name": "candid:service", + "visibility": "public" + } + ] + }, + "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": ["basic_timelock_ibe", "internet-identity"], + "build": ["cd frontend && npm i --include=dev && npm run build"], + "frontend": { + "entrypoint": "frontend/dist/index.html" + }, + "source": ["frontend/dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + }, + "networks": { + "local": { + "bind": "127.0.0.1:8000", + "type": "ephemeral" + } + } +} diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/.prettierrc b/rust/vetkeys/basic_timelock_ibe/frontend/.prettierrc new file mode 100644 index 000000000..fa1d0d324 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/.prettierrc @@ -0,0 +1,4 @@ +{ + "plugins": [], + "tabWidth": 4 +} diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/eslint.config.mjs b/rust/vetkeys/basic_timelock_ibe/frontend/eslint.config.mjs new file mode 100644 index 000000000..975515449 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/eslint.config.mjs @@ -0,0 +1,30 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + ignores: [ + "dist/", + "src/declarations", + "coverage/", + "*.config.js", + "*.config.cjs", + "*.config.mjs", + "*.config.ts", + ], + }, +); diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/index.html b/rust/vetkeys/basic_timelock_ibe/frontend/index.html new file mode 100644 index 000000000..fd05b9c7d --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + VetKeys: Basic Timelock IBE + + +
+ + + diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/package.json b/rust/vetkeys/basic_timelock_ibe/frontend/package.json new file mode 100644 index 000000000..45ea14ca9 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "basic-timelock-ibe-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "npm run build:bindings && vite", + "build": "npm run build:bindings && tsc && vite build", + "build:bindings": "cd scripts && ./gen_bindings.sh", + "preview": "vite preview", + "lint": "eslint" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@rollup/plugin-typescript": "^12.1.2", + "@types/node": "^24.0.4", + "eslint": "^9.24.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "tslib": "^2.8.1", + "typescript": "~5.7.2", + "typescript-eslint": "^8.35.0", + "vite": "^6.4.1", + "vite-plugin-environment": "^1.1.3" + }, + "dependencies": { + "@dfinity/agent": "^2.4.1", + "@dfinity/auth-client": "^2.4.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/vetkeys": "^0.3.0" + } +} diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/public/.ic-assets.json5 b/rust/vetkeys/basic_timelock_ibe/frontend/public/.ic-assets.json5 new file mode 100644 index 000000000..2997d66d2 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/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';object-src 'none';base-uri 'self';frame-ancestors 'none';form-action 'self';upgrade-insecure-requests;", + }, + allow_raw_access: false + }, +] diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/public/vite.svg b/rust/vetkeys/basic_timelock_ibe/frontend/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/scripts/gen_bindings.sh b/rust/vetkeys/basic_timelock_ibe/frontend/scripts/gen_bindings.sh new file mode 100755 index 000000000..be1cfcb90 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/scripts/gen_bindings.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cd ../../backend && make extract-candid && dfx generate basic_timelock_ibe && cd ../frontend && rm -r ./src/declarations >> /dev/null 2>&1 +mv ../src/declarations ./src && rmdir ../src + +# dfx 0.31+ generates @icp-sdk/core imports; rewrite to @dfinity/* to match deps +find ./src/declarations -type f \( -name '*.ts' -o -name '*.js' \) -exec \ + perl -i -pe 's|\@icp-sdk/core/agent|\@dfinity/agent|g; s|\@icp-sdk/core/principal|\@dfinity/principal|g; s|\@icp-sdk/core/candid|\@dfinity/candid|g' {} + \ No newline at end of file diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/src/main.ts b/rust/vetkeys/basic_timelock_ibe/frontend/src/main.ts new file mode 100644 index 000000000..e5cd3a357 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/src/main.ts @@ -0,0 +1,483 @@ +// Required to run `npm run dev`. +if (!window.global) { + window.global = window; +} + +import "./style.css"; +import { createActor } from "./declarations/basic_timelock_ibe"; +import { Principal } from "@dfinity/principal"; +import { + DerivedPublicKey, + IbeCiphertext, + IbeIdentity, + IbeSeed, +} from "@dfinity/vetkeys"; +import { + _SERVICE, + LotInformation, +} from "./declarations/basic_timelock_ibe/basic_timelock_ibe.did"; +import { AuthClient } from "@dfinity/auth-client"; +import type { ActorSubclass } from "@dfinity/agent"; + +let ibePublicKey: DerivedPublicKey | undefined = undefined; +let myPrincipal: Principal | undefined = undefined; +let authClient: AuthClient | undefined; +let basicTimelockIbeCanister: ActorSubclass<_SERVICE> | undefined; + +function getBasicTimelockIbeCanister(): ActorSubclass<_SERVICE> { + if (basicTimelockIbeCanister) return basicTimelockIbeCanister; + if (!process.env.CANISTER_ID_BASIC_TIMELOCK_IBE) { + throw Error("CANISTER_ID_BASIC_TIMELOCK_IBE is not set"); + } + if (!authClient) { + throw Error("Auth client is not initialized"); + } + const host = + process.env.DFX_NETWORK === "ic" + ? `https://${process.env.CANISTER_ID_BASIC_TIMELOCK_IBE}.ic0.app` + : "http://localhost:8000"; + + basicTimelockIbeCanister = createActor( + process.env.CANISTER_ID_BASIC_TIMELOCK_IBE, + { + agentOptions: { + identity: authClient.getIdentity(), + host, + }, + }, + ); + + return basicTimelockIbeCanister!; +} + +export function login(client: AuthClient) { + void 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: () => { + myPrincipal = client.getIdentity().getPrincipal(); + updateUI(true); + }, + onError: (error) => { + alert("Authentication failed: " + error); + }, + }); +} + +export function logout() { + void authClient?.logout(); + myPrincipal = undefined; + basicTimelockIbeCanister = undefined; + updateUI(false); + + // Reset the lots list and form visibility + document.getElementById("lotsList")!.classList.toggle("hidden", true); + document.getElementById("lotForm")!.classList.toggle("hidden", true); +} + +async function initAuth() { + authClient = await AuthClient.create(); + const isAuthenticated = await authClient.isAuthenticated(); + + if (isAuthenticated) { + myPrincipal = authClient.getIdentity().getPrincipal(); + updateUI(true); + } else { + updateUI(false); + } +} + +function updateUI(isAuthenticated: boolean) { + const loginButton = document.getElementById("loginButton")!; + const principalDisplay = document.getElementById("principalDisplay")!; + const logoutButton = document.getElementById("logoutButton")!; + const lotActions = document.getElementById("lotActions")!; + const lotForm = document.getElementById("lotForm")!; + const lotsList = document.getElementById("lotsList")!; + + loginButton.classList.toggle("hidden", isAuthenticated); + principalDisplay.classList.toggle("hidden", !isAuthenticated); + logoutButton.classList.toggle("hidden", !isAuthenticated); + lotActions.classList.toggle("hidden", !isAuthenticated); + lotForm.classList.toggle("hidden", true); + lotsList.classList.toggle("hidden", true); + + if (isAuthenticated && myPrincipal) { + principalDisplay.textContent = `Principal: ${myPrincipal.toString()}`; + } +} + +function handleLogin() { + if (!authClient) { + alert("Auth client not initialized"); + return; + } + + login(authClient); +} + +document.querySelector("#app")!.innerHTML = ` +
+

Secret Bid Auction using VetKeys (Basic Timelock IBE)

+
+
+ +
+ +
+ + +
+
+

Create New Lot

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+
+`; + +// Add event listeners +document.getElementById("loginButton")!.addEventListener("click", handleLogin); +document.getElementById("logoutButton")!.addEventListener("click", logout); +document.getElementById("createLotButton")!.addEventListener("click", () => { + document.getElementById("lotForm")!.classList.toggle("hidden", false); + document.getElementById("lotsList")!.classList.toggle("hidden", true); +}); +document.getElementById("listLotsButton")!.addEventListener("click", () => { + void (async () => { + await listLots(); + })(); +}); +document.getElementById("createLotForm")!.addEventListener("submit", (e) => { + (e as Event).preventDefault(); + const name = (document.getElementById("lotName") as HTMLInputElement).value; + const description = ( + document.getElementById("lotDescription") as HTMLTextAreaElement + ).value; + const duration = parseInt( + (document.getElementById("lotDuration") as HTMLInputElement).value, + ); + void createLot(name, description, duration); +}); + +async function getIbePublicKey(): Promise { + if (ibePublicKey) return ibePublicKey; + ibePublicKey = DerivedPublicKey.deserialize( + new Uint8Array( + await getBasicTimelockIbeCanister().get_ibe_public_key(), + ), + ); + return ibePublicKey; +} + +async function encrypt( + cleartext: Uint8Array, + identity: Uint8Array, +): Promise { + const publicKey = await getIbePublicKey(); + const ciphertext = IbeCiphertext.encrypt( + publicKey, + IbeIdentity.fromBytes(identity), + cleartext, + IbeSeed.random(), + ); + return ciphertext.serialize(); +} + +async function createLot( + name: string, + description: string, + durationSeconds: number, +) { + const result = await getBasicTimelockIbeCanister().create_lot( + name, + description, + durationSeconds, + ); + if ("Ok" in result) { + alert(`Lot created successfully with ID: ${result.Ok.toString()}`); + } else { + alert(`Failed to create lot: ${result.Err}`); + } + document.getElementById("lotForm")!.classList.toggle("hidden", true); +} + +function getStatusForOpenLot( + lot: LotInformation, + bidders: Principal[], +): string { + if ( + bidders.find( + (bidder) => bidder.compareTo(myPrincipal as Principal) === "eq", + ) + ) { + return 'BID PLACED'; + } else if (lot.creator.compareTo(myPrincipal as Principal) === "eq") { + return 'OWNER'; + } + return ""; +} + +function getStatusForClosedLot( + lot: LotInformation, + bids: [Principal, bigint][], +): string { + const myBid = bids.find( + (bid) => bid[0].compareTo(myPrincipal as Principal) === "eq", + ); + const isCreator = lot.creator.compareTo(myPrincipal as Principal) === "eq"; + + if (isCreator) { + return 'OWNER'; + } + + if ("ClosedWithWinner" in lot.status) { + if ( + lot.status.ClosedWithWinner.compareTo(myPrincipal as Principal) === + "eq" + ) { + return 'WON'; + } else if (myBid) { + return 'LOST'; + } else { + return 'SKIPPED'; + } + } else { + return 'SKIPPED'; + } +} + +function formatPrincipal( + principal: Principal, + isWinner: boolean = false, +): string { + const classes = []; + if (isWinner) classes.push("principal-winner"); + if (myPrincipal && principal.compareTo(myPrincipal) === "eq") + classes.push("principal-me"); + return `${principal.toString()}`; +} + +function formatCountdown(endTime: bigint): string { + const now = BigInt(Date.now() * 1_000_000); + const remaining = endTime - now; + + if (remaining <= 0n) { + return 'Ended'; + } + + const seconds = Number(remaining / 1_000_000_000n); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + const remainingHours = hours % 24; + const remainingMinutes = minutes % 60; + const remainingSeconds = seconds % 60; + + return `${days}d ${remainingHours}h ${remainingMinutes}m ${remainingSeconds}s`; +} + +async function listLots() { + try { + const [openLots, closedLots] = + await getBasicTimelockIbeCanister().get_lots(); + const openLotsDiv = document.getElementById("openLots")!; + const closedLotsDiv = document.getElementById("closedLots")!; + + // Clear both containers first + openLotsDiv.innerHTML = ""; + closedLotsDiv.innerHTML = ""; + + if (openLots.lots.length === 0) { + openLotsDiv.innerHTML = "

Open Lots

No open lots

"; + } else { + const fragment = document.createDocumentFragment(); + const heading = document.createElement("h4"); + heading.textContent = "Open Lots"; + fragment.appendChild(heading); + + openLots.lots.reverse(); + openLots.bidders.reverse(); + + openLots.lots.forEach((lot, index) => { + const lotDiv = document.createElement("div"); + lotDiv.className = "lot"; + const isCreator = + lot.creator.compareTo(myPrincipal as Principal) === "eq"; + const status = getStatusForOpenLot( + openLots.lots[index], + openLots.bidders[index], + ); + + lotDiv.innerHTML = ` +
Name: ${lot.name}
+

Description: ${lot.description}

+

Creator: ${lot.creator.toText()}

+

Closing in: ${formatCountdown(lot.end_time)}

+ ${status} +

Bidders:${openLots.bidders[index].length === 0 ? " no bidders yet" : openLots.bidders[index].map((bidder) => "
" + formatPrincipal(bidder)).join("")}

+ ${ + !isCreator + ? ` +
+
+ + +
+ +
+ ` + : "" + } + `; + + if (!isCreator) { + const bidForm = lotDiv.querySelector(`#bidForm-${lot.id}`); + if (bidForm) { + bidForm.addEventListener("submit", (e) => { + e.preventDefault(); + const amount = parseInt( + ( + document.getElementById( + `bidAmount-${lot.id}`, + ) as HTMLInputElement + ).value, + ); + void placeBid(lot.id, amount); + }); + } + } + + fragment.appendChild(lotDiv); + }); + + openLotsDiv.innerHTML = ""; + openLotsDiv.appendChild(fragment); + } + + if (closedLots.lots.length === 0) { + closedLotsDiv.innerHTML = + "

Closed Lots

No closed lots

"; + } else { + const fragment = document.createDocumentFragment(); + const heading = document.createElement("h4"); + heading.textContent = "Closed Lots"; + fragment.appendChild(heading); + + closedLots.lots.reverse(); + closedLots.bids.reverse(); + + closedLots.lots.forEach((lot, index) => { + const lotDiv = document.createElement("div"); + lotDiv.className = "lot"; + const isWinner = + "ClosedWithWinner" in lot.status && + lot.status.ClosedWithWinner.compareTo( + myPrincipal as Principal, + ) === "eq"; + const status = getStatusForClosedLot( + lot, + closedLots.bids[index], + ); + + lotDiv.innerHTML = ` +
Name: ${lot.name}
+

Description: ${lot.description}

+

Creator: ${formatPrincipal(lot.creator)}

+

Winner: ${"ClosedWithWinner" in lot.status ? formatPrincipal(lot.status.ClosedWithWinner, isWinner) : "No winner"}

+

Ended at: ${new Date(Number(lot.end_time) / 1000000).toLocaleString()}

+ ${status} +

Bids: ${ + closedLots.bids[index].length === 0 + ? " no bids" + : closedLots.bids[index] + .map( + (bid) => + `
${formatPrincipal( + bid[0], + "ClosedWithWinner" in lot.status && + lot.status.ClosedWithWinner.compareTo( + bid[0], + ) === "eq", + )}: ${bid[1]}`, + ) + .join("") + }

+ `; + + fragment.appendChild(lotDiv); + }); + + closedLotsDiv.innerHTML = ""; + closedLotsDiv.appendChild(fragment); + } + + document.getElementById("lotsList")!.classList.toggle("hidden", false); + } catch (error) { + alert(`Failed to list lots: ${error as Error}`); + } +} + +async function placeBid(lotId: bigint, amount: number) { + try { + const lotIdBytes = u128ToLeBytes(lotId); + const amountBytes = u128ToLeBytes(BigInt(amount)); + + // Encrypt the bid amount using IBE + const encryptedAmount = await encrypt(amountBytes, lotIdBytes); + + // Place the bid + const result = await getBasicTimelockIbeCanister().place_bid( + lotId, + encryptedAmount, + ); + if ("Err" in result) { + alert(`Failed to place bid: ${result.Err}`); + return; + } + + alert("Bid placed successfully!"); + // Refresh the lots list + await listLots(); + } catch (error) { + alert(`Failed to place bid: ${error as Error}`); + } +} + +function u128ToLeBytes(value: bigint): Uint8Array { + const bytes = new Uint8Array(16); + let temp = value; + + for (let i = 0; i < 16; i++) { + bytes[i] = Number(temp & 0xffn); + temp >>= 8n; + } + + return bytes; +} + +// Initialize auth +void initAuth(); diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/src/style.css b/rust/vetkeys/basic_timelock_ibe/frontend/src/style.css new file mode 100644 index 000000000..14de04b52 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/src/style.css @@ -0,0 +1,452 @@ +/* Base styles */ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +h1 { + font-size: 2.2em; + line-height: 1.1; + margin-bottom: 2rem; +} + +h3 { + color: var(--text-color); + font-size: 1.5rem; + margin-bottom: 1.5rem; + font-weight: 600; +} + +h4 { + color: var(--text-color); + font-size: 1.25rem; + margin-bottom: 1rem; + font-weight: 600; +} + +h5 { + color: var(--text-color); + font-size: 1.1rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +/* Principal container */ +.principal-container { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + justify-content: center; +} + +.principal-display { + font-family: monospace; + background-color: rgba(0, 0, 0, 0.2); + padding: 0.5rem 1rem; + border-radius: 8px; + color: #a8a6a6; + white-space: pre-wrap; + word-break: break-all; + max-width: 600px; +} + +/* Buttons */ +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +#loginButton { + background-color: #28a745; + color: white; +} + +#loginButton:hover { + background-color: #218838; +} + +#logoutButton { + background-color: #dc3545; + color: white; +} + +#logoutButton:hover { + background-color: #c82333; +} + +/* Lot actions */ +#lotActions { + display: flex; + gap: 1rem; + justify-content: center; + margin: 2rem 0; +} + +#lotActions button { + background-color: #1a1a1a; + color: white; +} + +#lotActions button:hover { + background-color: #2a2a2a; +} + +/* Lot form */ +#lotForm { + max-width: 600px; + margin: 2rem auto; + padding: 2rem 3rem; + background-color: #1a1a1a; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +#lotForm div { + margin-bottom: 1.5rem; +} + +#lotForm label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.87); +} + +#lotForm input, +#lotForm textarea { + width: 100%; + padding: 0.75rem; + background-color: #242424; + border: 1px solid #333; + border-radius: 8px; + color: rgba(255, 255, 255, 0.87); + font-size: 1rem; +} + +#lotForm input:focus, +#lotForm textarea:focus { + outline: none; + border-color: #646cff; +} + +#lotForm textarea { + height: 120px; + resize: vertical; +} + +#lotForm button { + width: 100%; + background-color: #28a745; + color: white; +} + +#lotForm button:hover { + background-color: #218838; +} + +/* Lots list */ +#lotsList { + margin: 2rem 0; +} + +.lot { + margin: 1.5rem 0; + padding: 1.5rem; + background-color: #1a1a1a; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 0.2s ease; + position: relative; +} + +.lot:hover { + transform: translateY(-2px); +} + +.lot h5 { + margin: 0 0 1rem 0; + color: rgba(255, 255, 255, 0.87); +} + +.lot p { + margin: 0.5rem 0; + color: #a8a6a6; +} + +.lot-status { + position: absolute; + top: 1rem; + right: 1rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.9rem; + font-weight: 500; + text-transform: uppercase; +} + +.status-placed { + background-color: rgba(40, 167, 69, 0.2); + color: #28a745; +} + +.status-owner { + background-color: rgba(13, 110, 253, 0.2); + color: #0d6efd; +} + +.status-won { + background-color: rgba(255, 193, 7, 0.2); + color: #ffc107; +} + +.status-lost { + background-color: rgba(220, 53, 69, 0.2); + color: #dc3545; +} + +.status-skipped { + background-color: rgba(108, 117, 125, 0.2); + color: #6c757d; +} + +.lot-countdown { + font-family: monospace; + color: #ffc107; + margin-bottom: 0.5rem; +} + +.principal-indicator { + display: inline-block; + margin-right: 0.5rem; +} + +.principal-winner::before { + content: "🏆"; + margin-right: 0.5rem; +} + +.principal-me::before { + content: "ME"; + background-color: #646cff; + color: white; + padding: 0.1rem 0.3rem; + border-radius: 4px; + font-size: 0.8rem; + margin-right: 0.5rem; +} + +/* Handle combinations of classes */ +.principal-me.principal-winner::before { + content: "ME 🏆"; + background-color: #646cff; + color: white; + padding: 0.1rem 0.3rem; + border-radius: 4px; + font-size: 0.8rem; + margin-right: 0.5rem; +} + +/* Bid form */ +.bid-form { + margin-top: 1.5rem; + padding: 1.5rem; + background-color: #242424; + border-radius: 8px; + max-width: 300px; + margin-left: auto; + margin-right: auto; +} + +.bid-form div { + margin-bottom: 1rem; +} + +.bid-form label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.87); +} + +.bid-form input { + width: 100%; + padding: 0.5rem; + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + color: rgba(255, 255, 255, 0.87); + font-size: 1rem; + box-sizing: border-box; +} + +.bid-form button { + width: 100%; + background-color: #28a745; + color: white; +} + +.bid-form button:hover { + background-color: #218838; +} + +/* Responsive design */ +@media (max-width: 768px) { + #app { + padding: 1rem; + } + + h1 { + font-size: 2rem; + } + + .principal-container { + flex-direction: column; + gap: 1rem; + } + + #lotActions { + flex-direction: column; + } +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } + .principal-display { + background-color: rgba(0, 0, 0, 0.05); + color: #666; + } + #lotForm { + background-color: #f9f9f9; + } + #lotForm label { + color: #213547; + } + #lotForm input, + #lotForm textarea { + background-color: #ffffff; + color: #213547; + border-color: #ddd; + } + .lot { + background-color: #f9f9f9; + } + .lot h5 { + color: #213547; + } + .lot p { + color: #666; + } + .bid-form { + background-color: #ffffff; + } + .bid-form label { + color: #213547; + } + .bid-form input { + background-color: #f9f9f9; + color: #213547; + border-color: #ddd; + } +} + +.login-container { + display: flex; + justify-content: center; + margin: 1rem 0; +} + +#openLots h4, +#closedLots h4 { + color: rgba(255, 255, 255, 0.95); + font-size: 1.5rem; + margin-bottom: 1.5rem; + font-weight: 600; + text-align: left; + padding-bottom: 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Auth state classes */ +.hidden { + display: none !important; +} + +/* Initial state classes for auth elements */ +#loginButton { + display: block; +} + +#messageButtons { + display: flex; +} + +#principalDisplay { + display: block; +} + +#logoutButton { + display: block; +} + +#lotActions { + display: flex; +} + +#lotForm { + display: block; +} + +#lotsList { + display: block; +} diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/src/vite-env.d.ts b/rust/vetkeys/basic_timelock_ibe/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/tsconfig.json b/rust/vetkeys/basic_timelock_ibe/frontend/tsconfig.json new file mode 100644 index 000000000..a4883f28e --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/rust/vetkeys/basic_timelock_ibe/frontend/vite.config.ts b/rust/vetkeys/basic_timelock_ibe/frontend/vite.config.ts new file mode 100644 index 000000000..044e5ebf3 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/frontend/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite' +import typescript from '@rollup/plugin-typescript'; +import environment from 'vite-plugin-environment'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + typescript({ + inlineSources: true, + }), + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + root: "./", + server: { + hmr: false + } +}) \ No newline at end of file diff --git a/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml b/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml new file mode 120000 index 000000000..e01fe10ab --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml @@ -0,0 +1 @@ +../../rust-toolchain.toml \ No newline at end of file diff --git a/rust/vetkeys/basic_timelock_ibe/ui_screenshot.png b/rust/vetkeys/basic_timelock_ibe/ui_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..7c250d060927465e417650f25613101b6ad852ab GIT binary patch literal 115541 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 literal 0 HcmV?d00001 From d9f6a8b7cdc89733bfb4a03004fbe5467daac198 Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 17:06:36 +0200 Subject: [PATCH 2/7] fix: replace broken rust-toolchain.toml symlinks with actual files --- rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml | 5 ++++- rust/vetkeys/basic_ibe/rust/rust-toolchain.toml | 5 ++++- rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) mode change 120000 => 100644 rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml mode change 120000 => 100644 rust/vetkeys/basic_ibe/rust/rust-toolchain.toml mode change 120000 => 100644 rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml diff --git a/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml b/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml deleted file mode 120000 index 4e9e6489d..000000000 --- a/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml +++ /dev/null @@ -1 +0,0 @@ -../../../rust-toolchain.toml \ No newline at end of file diff --git a/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml b/rust/vetkeys/basic_bls_signing/rust/rust-toolchain.toml new file mode 100644 index 000000000..2a2058b04 --- /dev/null +++ b/rust/vetkeys/basic_bls_signing/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/basic_ibe/rust/rust-toolchain.toml b/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml deleted file mode 120000 index 4e9e6489d..000000000 --- a/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml +++ /dev/null @@ -1 +0,0 @@ -../../../rust-toolchain.toml \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml b/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml new file mode 100644 index 000000000..2a2058b04 --- /dev/null +++ b/rust/vetkeys/basic_ibe/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/basic_timelock_ibe/rust-toolchain.toml b/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml deleted file mode 120000 index e01fe10ab..000000000 --- a/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml +++ /dev/null @@ -1 +0,0 @@ -../../rust-toolchain.toml \ No newline at end of file diff --git a/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml b/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml new file mode 100644 index 000000000..2a2058b04 --- /dev/null +++ b/rust/vetkeys/basic_timelock_ibe/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.88.0" +targets = ["wasm32-unknown-unknown"] +profile = "default" \ No newline at end of file From 8a15943e19aed6f5a0b5bf0161ba166cfbc5e323 Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 10:54:11 +0200 Subject: [PATCH 3/7] feat: add vetkeys encrypted notes example Add the encrypted_notes_dapp_vetkd example from dfinity/vetkeys. A secure note-taking application that uses vetKeys for encryption and enables sharing notes between users without device management. Includes Rust and Motoko backends. --- .../encrypted_notes_dapp_vetkd/README.md | 58 +++ .../frontend/package.json | 81 ++++ .../frontend/public/.ic-assets.json5 | 10 + .../frontend/public/favicon.png | Bin 0 -> 3127 bytes ...wered-by-crypto_label-stripe-dark-text.png | Bin 0 -> 6265 bytes ...ered-by-crypto_label-stripe-white-text.png | Bin 0 -> 7672 bytes ...owered-by-crypto_transparent-dark-text.png | Bin 0 -> 12190 bytes ...wered-by-crypto_transparent-white-text.png | Bin 0 -> 11664 bytes .../frontend/public/index.html | 16 + .../frontend/rollup.config.js | 121 +++++ .../frontend/scripts/gen_bindings.sh | 9 + .../frontend/src/App.svelte | 19 + .../frontend/src/components/Disclaimer.svelte | 26 ++ .../src/components/DisclaimerCopy.svelte | 3 + .../frontend/src/components/EditNote.svelte | 162 +++++++ .../frontend/src/components/Header.svelte | 25 + .../frontend/src/components/Hero.svelte | 70 +++ .../src/components/LayoutAuthenticated.svelte | 26 ++ .../frontend/src/components/NewNote.svelte | 93 ++++ .../frontend/src/components/Note.svelte | 45 ++ .../frontend/src/components/NoteEditor.svelte | 68 +++ .../frontend/src/components/Notes.svelte | 75 +++ .../src/components/Notifications.svelte | 22 + .../src/components/SharingEditor.svelte | 128 ++++++ .../src/components/SidebarLayout.svelte | 75 +++ .../frontend/src/components/Spinner.svelte | 3 + .../frontend/src/components/TagEditor.svelte | 74 +++ .../frontend/src/global.d.ts | 1 + .../frontend/src/lib/actor.ts | 54 +++ .../frontend/src/lib/crypto.ts | 109 +++++ .../frontend/src/lib/enums.ts | 8 + .../frontend/src/lib/note.ts | 106 +++++ .../frontend/src/lib/sleep.ts | 3 + .../frontend/src/main.ts | 9 + .../frontend/src/store/auth.ts | 140 ++++++ .../frontend/src/store/draft.ts | 31 ++ .../frontend/src/store/notes.ts | 132 ++++++ .../frontend/src/store/notifications.ts | 30 ++ .../frontend/tailwind.config.js | 10 + .../frontend/tsconfig.json | 5 + .../motoko/backend/main.mo | 410 +++++++++++++++++ .../motoko/backend/utils/Hex.mo | 105 +++++ .../motoko/dfx.json | 39 ++ .../motoko/frontend | 1 + .../rust/Cargo.toml | 9 + .../rust/backend/Cargo.toml | 19 + .../rust/backend/src/encrypted_notes_rust.did | 17 + .../rust/backend/src/lib.rs | 433 ++++++++++++++++++ .../encrypted_notes_dapp_vetkd/rust/dfx.json | 39 ++ .../encrypted_notes_dapp_vetkd/rust/frontend | 1 + .../rust/rust-toolchain.toml | 1 + .../security-checklist.md | 165 +++++++ 52 files changed, 3086 insertions(+) create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/README.md create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/package.json create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/.ic-assets.json5 create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/favicon.png create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/public/index.html create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/rollup.config.js create mode 100755 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/scripts/gen_bindings.sh create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/App.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Disclaimer.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/DisclaimerCopy.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/EditNote.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Header.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Hero.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/LayoutAuthenticated.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NewNote.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Note.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/NoteEditor.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notes.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Notifications.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SharingEditor.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/SidebarLayout.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/Spinner.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/components/TagEditor.svelte create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/global.d.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/actor.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/crypto.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/enums.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/note.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/lib/sleep.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/main.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/auth.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/draft.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notes.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/src/store/notifications.ts create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tailwind.config.js create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/frontend/tsconfig.json create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/main.mo create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/backend/utils/Hex.mo create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/dfx.json create mode 120000 rust/vetkeys/encrypted_notes_dapp_vetkd/motoko/frontend create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/Cargo.toml create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/Cargo.toml create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/encrypted_notes_rust.did create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/backend/src/lib.rs create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/dfx.json create mode 120000 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/frontend create mode 120000 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml create mode 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/security-checklist.md 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 | [![](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 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 0000000000000000000000000000000000000000..7e6f5eb5a2f1f1c882d265cf479de25caa925645 GIT binary patch literal 3127 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 k5>+ca(Es=%(OZCZ_*S7t&NA}<0giOF*Z=?k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..90029c464bf6080f78d19b48c0aea12073448600 GIT binary patch literal 12190 zcmX9^WmFu^5(N_6-5nMW?z;FA++BjZyL)hVmmtC2J!o)uf@E=bf8={Vrl+Q>`c79( zpPoMV+z3T^DP#l!1PBNSWRSGDG6V$V$cOz64(6ksR}x$NxWPL}YdJ$e;86d&AVJDh z7ayIF&dO4v5Y>}JhaV50&4F@22#C5k#1|uI2nc3&kT_7)1M)P-H=bzNb=hF*43bPd z5;5Wd?KcIORyio$$-x051%tOwzfIZkO3I^Fe8{BOuqII^yEu`b_AiEMLOX8+N5TvF zx*El(6Mc8^>30keHVQV4qw1H<*;ut|Y1UZz$PgiW5jFs%^pMgY*np0lN$*$IId=YM z&u2%nP1N+Y$AiPev%^E~^V?$*Nc=BPsA&?7tmGo=QCUJ9KsZQc8Ewe?ExE9$ga=g2 zX|nou9@;nB#H0B3c(l>BNw>TB&C(*-L52xC1rw-4S3*WiHH>lHGOgB5Mot8cG$DsE z)uWvL>yUBrY}`^_vOe|<>v%l)!}^4bS~h1C+bB57JK28Kx1`U-XZ}Qag2FjlH_h

B-^J2-2d7W%HLj zUiX<9yDsx)vrKUhKF_k$!^)3IfMJTP;k}o6p(L{L8tE_zyya5j;Ap?Xoo}lTsTliI}TC9lF7wmg93&n1remsmY?Oa;iTbP`TDZF z6m2{0+S%ZtTLM8>X_4$|X-VNct}&{5EuP%e@#x8ppKO?Oy)31a*7T>i#80zT{275%$nEIjf1eUL4e<$9uF7i($@*~t$Tb>eUcjCNFU$xJ z3}Z$aWKIc>2G2|G=QLcJLOEs2Echkh1JTINq5G;^pp7zEpY z8DMk15xrw+dH?gIf978iMgJEP7EuQ^`{|!yq?>$AZyo;;i?oZDv7)0T@^4&pFb8I4 zL%@GF2eZ*O3*jki(Dh>e@}L_@ku#)Jh*pcGT)M zC2`T>GA2l)(~aG(|BN(n18u3j#dOj8?jUZ3}3FRH=( zyvGa;GB8ZZ)K=~RWg`oe=QM92m1gjoL*Ou*fXZ!{tV%WGQu2h~1VIH%*HkPoQu5yY)vrR^ zc)?(Gs`Tn5nf1%Sk^_X1Tpv&%$Rq-k?oq@l-SoO)zUQ=OBD1YS&~~PhDrf5rIAa<( zXa|29aDjLApONL;tcCShzut>NOizXggRrc)7kIrA6>c2;n$j`cnL}x=Xd>6DU36xj zd|5aL*?;P!s@;X{A%iCRO<$s@b&*8Sd7bB66O!QkKBy)3eH{uZZw}mogv5@w{CUC# z;RlaQCM^Wvu90Ot6;T~~Bb3FlCG+Ok+BqJB0frmXxmVjfz#mPoA?3v41En? z*1!mGa4K7@WC!Aku(O^w)m;8=qE5z4cu*|!7QZ2T41PD)z-^Kj`c||%adZHQV^P;e z;J5q(f=e7~`#9~FvENX|HF|8G>2?@WDlAjQ523NAdas^g7kCr9-|kizCsy>#>2UF))G8{QUD_!BF>&>UEgVB zjMwUn*G7Gq(+g)R%*D*eVA)P~a5nwCw@d1TBlnbXJV!1Acoi#_;3ha

kGJ?@15| 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 literal 0 HcmV?d00001 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 $notesStore.state === 'loaded'} + + addTag(e.detail)} + on:remove={(e) => removeTag(e.detail)} + disabled={updating || deleting} + /> + +
+ + {:else if $notesStore.state === 'loading'} + Loading notes... + {/if} +
+{:else} +
+ Edit note +
+
+ {#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'} + + {:else if auth.state === 'error'} +
An error occurred.
+ {/if} + +
+ +
+
+
+
+ + Powered by the Internet Computer +
+
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 @@ + + + + +
+ New note +
+ +
+ + addTag(e.detail)} + on:remove={(e) => removeTag(e.detail)} + disabled={creating} + /> + +
+ + 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} + + {/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)} +
+

{n.message}

+
+ {/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} + + {/each} + + +
+
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 @@ + + +
+ +
+
+ +
+ +
+
+
+
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} + + {/each} + + +
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 120000 index 000000000..4e9e6489d --- /dev/null +++ b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml @@ -0,0 +1 @@ +../../../rust-toolchain.toml \ 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 From deadd935380a1b90a3cf596ee7e42b9b9257d871 Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 17:06:46 +0200 Subject: [PATCH 4/7] fix: replace broken rust-toolchain.toml symlink with actual file --- .../encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) mode change 120000 => 100644 rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml diff --git a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml b/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml deleted file mode 120000 index 4e9e6489d..000000000 --- a/rust/vetkeys/encrypted_notes_dapp_vetkd/rust/rust-toolchain.toml +++ /dev/null @@ -1 +0,0 @@ -../../../rust-toolchain.toml \ 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 From e87914045f3e2d068e6ea3472adf19d82d9b6c21 Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 17:06:26 +0200 Subject: [PATCH 5/7] fix: replace broken symlinks with actual canister source The password_manager example had symlinks to the vetkeys repo's shared backend canisters. Copy the actual source (rust and motoko) to make the example self-contained. Also replace the rust-toolchain.toml symlink with the actual file. --- .../motoko/backend/.prettierrc | 8 + .../password_manager/motoko/backend/Makefile | 14 + .../password_manager/motoko/backend/README.md | 7 + .../password_manager/motoko/backend/icp.yaml | 7 + .../password_manager/motoko/backend/mops.toml | 16 + .../motoko/backend/src/Main.mo | 221 +++++++++++++ .../password_manager/rust/backend/Cargo.toml | 29 ++ .../password_manager/rust/backend/Makefile | 22 ++ .../password_manager/rust/backend/README.md | 7 + .../ic_vetkeys_encrypted_maps_canister.did | 41 +++ .../password_manager/rust/backend/src/lib.rs | 298 ++++++++++++++++++ .../password_manager/rust/rust-toolchain.toml | 4 + 12 files changed, 674 insertions(+) create mode 100644 rust/vetkeys/password_manager/motoko/backend/.prettierrc create mode 100644 rust/vetkeys/password_manager/motoko/backend/Makefile create mode 100644 rust/vetkeys/password_manager/motoko/backend/README.md create mode 100644 rust/vetkeys/password_manager/motoko/backend/icp.yaml create mode 100644 rust/vetkeys/password_manager/motoko/backend/mops.toml create mode 100644 rust/vetkeys/password_manager/motoko/backend/src/Main.mo create mode 100644 rust/vetkeys/password_manager/rust/backend/Cargo.toml create mode 100644 rust/vetkeys/password_manager/rust/backend/Makefile create mode 100644 rust/vetkeys/password_manager/rust/backend/README.md create mode 100644 rust/vetkeys/password_manager/rust/backend/ic_vetkeys_encrypted_maps_canister.did create mode 100644 rust/vetkeys/password_manager/rust/backend/src/lib.rs create mode 100644 rust/vetkeys/password_manager/rust/rust-toolchain.toml diff --git a/rust/vetkeys/password_manager/motoko/backend/.prettierrc b/rust/vetkeys/password_manager/motoko/backend/.prettierrc new file mode 100644 index 000000000..a42bc32c3 --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/backend/.prettierrc @@ -0,0 +1,8 @@ +{ + "overrides": [{ + "files": "*.mo", + "options": { + "tabWidth": 4 + } + }] + } \ No newline at end of file diff --git a/rust/vetkeys/password_manager/motoko/backend/Makefile b/rust/vetkeys/password_manager/motoko/backend/Makefile new file mode 100644 index 000000000..ffaeb9642 --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/backend/Makefile @@ -0,0 +1,14 @@ +PWD:=$(shell pwd) + +.PHONY: compile-wasm +.SILENT: compile-wasm +compile-wasm: + icp build + +# Test the APIs of this canister using the respective Rust canister tests. +# This has the advantage that the tests are consistent (less room for bugs by having only one implementation of the tests) and the checked expected behavior is consistent across Rust and Motoko. +.PHONY: test +.SILENT: test +test: compile-wasm + @echo "Testing Motoko canister WASM: $(PWD)/.icp/cache/artifacts/ic_vetkeys_encrypted_maps_canister" + CUSTOM_WASM_PATH=$(PWD)/.icp/cache/artifacts/ic_vetkeys_encrypted_maps_canister cargo test -p ic-vetkeys-encrypted-maps-canister diff --git a/rust/vetkeys/password_manager/motoko/backend/README.md b/rust/vetkeys/password_manager/motoko/backend/README.md new file mode 100644 index 000000000..71f0ac924 --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/backend/README.md @@ -0,0 +1,7 @@ +# ic-vetkeys-encrypted-maps-canister + +The canister implemented in this folder directly exposes the methods of the encrypted maps. +This is useful for: + +1. running canister tests +2. implementing dapps that only require encrypted maps \ No newline at end of file diff --git a/rust/vetkeys/password_manager/motoko/backend/icp.yaml b/rust/vetkeys/password_manager/motoko/backend/icp.yaml new file mode 100644 index 000000000..b4101fe9c --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/backend/icp.yaml @@ -0,0 +1,7 @@ +canisters: + - name: ic_vetkeys_encrypted_maps_canister + recipe: + type: "@dfinity/motoko@v4.1.0" + configuration: + main: src/Main.mo + args: --enhanced-orthogonal-persistence diff --git a/rust/vetkeys/password_manager/motoko/backend/mops.toml b/rust/vetkeys/password_manager/motoko/backend/mops.toml new file mode 100644 index 000000000..8cc38c25d --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/backend/mops.toml @@ -0,0 +1,16 @@ +[toolchain] +moc = "1.5.0" + +[package] +name = "ic-vetkeys-encrypted-maps-canister" +version = "0.1.0" +repository = "https://github.com/dfinity/vetkeys/backend/mo/canisters/ic_vetkeys_encrypted_maps_canister" +keywords = [ + "vetkeys,vetkd,encryption,privacy,signature,BLS,key ", + "derivation,IBE" +] +license = "Apache-2.0" + +[dependencies] +base = "0.14.6" +ic-vetkeys = "0.4.0" diff --git a/rust/vetkeys/password_manager/motoko/backend/src/Main.mo b/rust/vetkeys/password_manager/motoko/backend/src/Main.mo new file mode 100644 index 000000000..ad0c468bf --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/backend/src/Main.mo @@ -0,0 +1,221 @@ +import IcVetkeys "mo:ic-vetkeys"; +import Types "mo:ic-vetkeys/Types"; +import Principal "mo:base/Principal"; +import Text "mo:base/Text"; +import Blob "mo:base/Blob"; +import Result "mo:base/Result"; +import Array "mo:base/Array"; + +persistent actor class (keyName : Text) { + let encryptedMapsState = IcVetkeys.EncryptedMaps.newEncryptedMapsState({ curve = #bls12_381_g2; name = keyName }, "password_manager_example_dapp"); + transient let encryptedMaps = IcVetkeys.EncryptedMaps.EncryptedMaps(encryptedMapsState, Types.accessRightsOperations()); + + /// In this canister, we use the `ByteBuf` type to represent blobs. The reason is that we want to be consistent with the Rust canister implementation. + /// Unfortunately, the `Blob` type cannot be serialized/deserialized in the current Rust implementation efficiently without nesting it in another type. + public type ByteBuf = { inner : Blob }; + + public type EncryptedMapData = { + map_owner : Principal; + map_name : ByteBuf; + keyvals : [(ByteBuf, ByteBuf)]; + access_control : [(Principal, Types.AccessRights)]; + }; + + /// The result type compatible with Rust's `Result`. + public type Result = { + #Ok : Ok; + #Err : Err; + }; + + public query (msg) func get_accessible_shared_map_names() : async [(Principal, ByteBuf)] { + Array.map<(Principal, Blob), (Principal, ByteBuf)>( + encryptedMaps.getAccessibleSharedMapNames(msg.caller), + + func((principal, blob) : (Principal, Blob)) { + (principal, { inner = blob }); + }, + ); + }; + + public query (msg) func get_shared_user_access_for_map( + map_owner : Principal, + map_name : ByteBuf, + ) : async Result<[(Principal, Types.AccessRights)], Text> { + convertResult(encryptedMaps.getSharedUserAccessForMap(msg.caller, (map_owner, map_name.inner))); + }; + + public query (msg) func get_encrypted_values_for_map( + map_owner : Principal, + map_name : ByteBuf, + ) : async Result<[(ByteBuf, ByteBuf)], Text> { + let result = encryptedMaps.getEncryptedValuesForMap(msg.caller, (map_owner, map_name.inner)); + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(values)) { + #Ok( + Array.map<(Blob, Blob), (ByteBuf, ByteBuf)>( + values, + func((blob1, blob2) : (Blob, Blob)) { + ({ inner = blob1 }, { inner = blob2 }); + }, + ) + ); + }; + }; + }; + + public query (msg) func get_all_accessible_encrypted_values() : async [((Principal, ByteBuf), [(ByteBuf, ByteBuf)])] { + Array.map<((Principal, Blob), [(Blob, Blob)]), ((Principal, ByteBuf), [(ByteBuf, ByteBuf)])>( + encryptedMaps.getAllAccessibleEncryptedValues(msg.caller), + func(((owner, map_name), values) : ((Principal, Blob), [(Blob, Blob)])) { + ( + (owner, { inner = map_name }), + Array.map<(Blob, Blob), (ByteBuf, ByteBuf)>( + values, + func((blob1, blob2) : (Blob, Blob)) { + ({ inner = blob1 }, { inner = blob2 }); + }, + ), + ); + }, + ); + }; + + public query (msg) func get_all_accessible_encrypted_maps() : async [EncryptedMapData] { + Array.map, EncryptedMapData>( + encryptedMaps.getAllAccessibleEncryptedMaps(msg.caller), + func(map : IcVetkeys.EncryptedMaps.EncryptedMapData) : EncryptedMapData { + { + map_owner = map.map_owner; + map_name = { inner = map.map_name }; + keyvals = Array.map<(Blob, Blob), (ByteBuf, ByteBuf)>( + map.keyvals, + func((blob1, blob2) : (Blob, Blob)) { + ({ inner = blob1 }, { inner = blob2 }); + }, + ); + access_control = map.access_control; + }; + }, + ); + }; + + public query (msg) func get_encrypted_value( + map_owner : Principal, + map_name : ByteBuf, + map_key : ByteBuf, + ) : async Result { + let result = encryptedMaps.getEncryptedValue(msg.caller, (map_owner, map_name.inner), map_key.inner); + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(null)) { #Ok(null) }; + case (#ok(?blob)) { #Ok(?{ inner = blob }) }; + }; + }; + + public shared (msg) func remove_map_values( + map_owner : Principal, + map_name : ByteBuf, + ) : async Result<[ByteBuf], Text> { + let result = encryptedMaps.removeMapValues(msg.caller, (map_owner, map_name.inner)); + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(values)) { + #Ok( + Array.map( + values, + func(blob : Blob) : ByteBuf { + { inner = blob }; + }, + ) + ); + }; + }; + }; + + public query (msg) func get_owned_non_empty_map_names() : async [ByteBuf] { + Array.map( + encryptedMaps.getOwnedNonEmptyMapNames(msg.caller), + func(blob : Blob) : ByteBuf { + { inner = blob }; + }, + ); + }; + + public shared (msg) func insert_encrypted_value( + map_owner : Principal, + map_name : ByteBuf, + map_key : ByteBuf, + value : ByteBuf, + ) : async Result { + let result = encryptedMaps.insertEncryptedValue(msg.caller, (map_owner, map_name.inner), map_key.inner, value.inner); + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(null)) { #Ok(null) }; + case (#ok(?blob)) { #Ok(?{ inner = blob }) }; + }; + }; + + public shared (msg) func remove_encrypted_value( + map_owner : Principal, + map_name : ByteBuf, + map_key : ByteBuf, + ) : async Result { + let result = encryptedMaps.removeEncryptedValue(msg.caller, (map_owner, map_name.inner), map_key.inner); + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(null)) { #Ok(null) }; + case (#ok(?blob)) { #Ok(?{ inner = blob }) }; + }; + }; + + public shared func get_vetkey_verification_key() : async ByteBuf { + let inner = await encryptedMaps.getVetkeyVerificationKey(); + { inner }; + }; + + public shared (msg) func get_encrypted_vetkey( + map_owner : Principal, + map_name : ByteBuf, + transport_key : ByteBuf, + ) : async Result { + let result = await encryptedMaps.getEncryptedVetkey(msg.caller, (map_owner, map_name.inner), transport_key.inner); + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(vetkey)) { #Ok({ inner = vetkey }) }; + }; + }; + + public query (msg) func get_user_rights( + map_owner : Principal, + map_name : ByteBuf, + user : Principal, + ) : async Result { + convertResult(encryptedMaps.getUserRights(msg.caller, (map_owner, map_name.inner), user)); + }; + + public shared (msg) func set_user_rights( + map_owner : Principal, + map_name : ByteBuf, + user : Principal, + access_rights : Types.AccessRights, + ) : async Result { + convertResult(encryptedMaps.setUserRights(msg.caller, (map_owner, map_name.inner), user, access_rights)); + }; + + public shared (msg) func remove_user( + map_owner : Principal, + map_name : ByteBuf, + user : Principal, + ) : async Result { + convertResult(encryptedMaps.removeUser(msg.caller, (map_owner, map_name.inner), user)); + }; + + /// Convert to the result type compatible with Rust's `Result` + private func convertResult(result : Result.Result) : Result { + switch (result) { + case (#err(e)) { #Err(e) }; + case (#ok(o)) { #Ok(o) }; + }; + }; +}; diff --git a/rust/vetkeys/password_manager/rust/backend/Cargo.toml b/rust/vetkeys/password_manager/rust/backend/Cargo.toml new file mode 100644 index 000000000..cb662f05f --- /dev/null +++ b/rust/vetkeys/password_manager/rust/backend/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ic-vetkeys-encrypted-maps-canister" +authors = ["DFINITY Stiftung"] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +description = "# Basic Identity Based Encryption" +repository = "https://github.com/dfinity/vetkeys" +rust-version = "1.85.0" + +[lib] +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10.2" +ic-cdk = { workspace = true } +ic-dummy-getrandom-for-wasm = "0.1.0" +ic-stable-structures = { workspace = true } +ic-vetkeys = { workspace = true } +serde = "1.0.217" + +[dev-dependencies] +assert_matches = "1.5.0" +pocket-ic = "9.0.0" +rand = "0.8.5" +rand_chacha = "0.3.1" +reqwest = "0.12.12" +strum = "0.27.1" diff --git a/rust/vetkeys/password_manager/rust/backend/Makefile b/rust/vetkeys/password_manager/rust/backend/Makefile new file mode 100644 index 000000000..b6dc7595e --- /dev/null +++ b/rust/vetkeys/password_manager/rust/backend/Makefile @@ -0,0 +1,22 @@ +ROOT_DIR := $(shell git rev-parse --show-toplevel) + +.PHONY: compile-wasm +.SILENT: compile-wasm +compile-wasm: + cargo build --release --target wasm32-unknown-unknown + +.PHONY: test +.SILENT: test +test: compile-wasm + cargo test -p ic-vetkeys-encrypted-maps-canister + +.PHONY: extract-candid +.SILENT: extract-candid +extract-candid: compile-wasm + candid-extractor $(ROOT_DIR)/target/wasm32-unknown-unknown/release/ic_vetkeys_encrypted_maps_canister.wasm > ic_vetkeys_encrypted_maps_canister.did + +.PHONY: clean +.SILENT: clean +clean: + cargo clean + rm -rf .icp/cache \ No newline at end of file diff --git a/rust/vetkeys/password_manager/rust/backend/README.md b/rust/vetkeys/password_manager/rust/backend/README.md new file mode 100644 index 000000000..71f0ac924 --- /dev/null +++ b/rust/vetkeys/password_manager/rust/backend/README.md @@ -0,0 +1,7 @@ +# ic-vetkeys-encrypted-maps-canister + +The canister implemented in this folder directly exposes the methods of the encrypted maps. +This is useful for: + +1. running canister tests +2. implementing dapps that only require encrypted maps \ No newline at end of file diff --git a/rust/vetkeys/password_manager/rust/backend/ic_vetkeys_encrypted_maps_canister.did b/rust/vetkeys/password_manager/rust/backend/ic_vetkeys_encrypted_maps_canister.did new file mode 100644 index 000000000..f30499fd1 --- /dev/null +++ b/rust/vetkeys/password_manager/rust/backend/ic_vetkeys_encrypted_maps_canister.did @@ -0,0 +1,41 @@ +type AccessRights = variant { Read; ReadWrite; ReadWriteManage }; +type ByteBuf = record { inner : blob }; +type EncryptedMapData = record { + access_control : vec record { principal; AccessRights }; + keyvals : vec record { ByteBuf; ByteBuf }; + map_name : ByteBuf; + map_owner : principal; +}; +type Result = variant { Ok : opt ByteBuf; Err : text }; +type Result_1 = variant { Ok : vec record { ByteBuf; ByteBuf }; Err : text }; +type Result_2 = variant { Ok : ByteBuf; Err : text }; +type Result_3 = variant { + Ok : vec record { principal; AccessRights }; + Err : text; +}; +type Result_4 = variant { Ok : opt AccessRights; Err : text }; +type Result_5 = variant { Ok : vec ByteBuf; Err : text }; +service : (text) -> { + get_accessible_shared_map_names : () -> ( + vec record { principal; ByteBuf }, + ) query; + get_all_accessible_encrypted_maps : () -> (vec EncryptedMapData) query; + get_all_accessible_encrypted_values : () -> ( + vec record { + record { principal; ByteBuf }; + vec record { ByteBuf; ByteBuf }; + }, + ) query; + get_encrypted_value : (principal, ByteBuf, ByteBuf) -> (Result) query; + get_encrypted_values_for_map : (principal, ByteBuf) -> (Result_1) query; + get_encrypted_vetkey : (principal, ByteBuf, ByteBuf) -> (Result_2); + get_owned_non_empty_map_names : () -> (vec ByteBuf) query; + get_shared_user_access_for_map : (principal, ByteBuf) -> (Result_3) query; + get_user_rights : (principal, ByteBuf, principal) -> (Result_4) query; + get_vetkey_verification_key : () -> (ByteBuf); + insert_encrypted_value : (principal, ByteBuf, ByteBuf, ByteBuf) -> (Result); + remove_encrypted_value : (principal, ByteBuf, ByteBuf) -> (Result); + remove_map_values : (principal, ByteBuf) -> (Result_5); + remove_user : (principal, ByteBuf, principal) -> (Result_4); + set_user_rights : (principal, ByteBuf, principal, AccessRights) -> (Result_4); +} diff --git a/rust/vetkeys/password_manager/rust/backend/src/lib.rs b/rust/vetkeys/password_manager/rust/backend/src/lib.rs new file mode 100644 index 000000000..ee4034817 --- /dev/null +++ b/rust/vetkeys/password_manager/rust/backend/src/lib.rs @@ -0,0 +1,298 @@ +use std::cell::RefCell; + +use candid::Principal; +use ic_cdk::management_canister::{VetKDCurve, VetKDKeyId}; +use ic_cdk::{init, query, update}; +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory}; +use ic_stable_structures::storable::Blob; +use ic_stable_structures::DefaultMemoryImpl; +use ic_vetkeys::encrypted_maps::{EncryptedMapData, EncryptedMaps, VetKey, VetKeyVerificationKey}; +use ic_vetkeys::types::{AccessRights, ByteBuf, EncryptedMapValue, TransportKey}; + +type Memory = VirtualMemory; +type MapId = (Principal, ByteBuf); + +thread_local! { + static MEMORY_MANAGER: RefCell> = + RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); + static ENCRYPTED_MAPS: RefCell>> = + const { RefCell::new(None) }; +} + +#[init] +fn init(key_name: String) { + let key_id = VetKDKeyId { + curve: VetKDCurve::Bls12_381_G2, + name: key_name, + }; + ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { + encrypted_maps.replace(EncryptedMaps::init( + "encrypted_maps_dapp", + key_id, + id_to_memory(0), + id_to_memory(1), + id_to_memory(2), + id_to_memory(3), + )) + }); +} + +#[query] +fn get_accessible_shared_map_names() -> Vec<(Principal, ByteBuf)> { + ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_accessible_shared_map_names(ic_cdk::api::msg_caller()) + .into_iter() + .map(|map_id| (map_id.0, ByteBuf::from(map_id.1.as_ref().to_vec()))) + .collect() + }) +} + +#[query] +fn get_shared_user_access_for_map( + key_owner: Principal, + key_name: ByteBuf, +) -> Result, String> { + let key_name = bytebuf_to_blob(key_name)?; + let key_id = (key_owner, key_name); + ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_shared_user_access_for_map(ic_cdk::api::msg_caller(), key_id) + }) +} + +#[query] +fn get_encrypted_values_for_map( + map_owner: Principal, + map_name: ByteBuf, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + let result = ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_encrypted_values_for_map(ic_cdk::api::msg_caller(), map_id) + }); + result.map(|map_values| { + map_values + .into_iter() + .map(|(key, value)| (ByteBuf::from(key.as_slice().to_vec()), value)) + .collect() + }) +} + +#[query] +fn get_all_accessible_encrypted_values() -> Vec<(MapId, Vec<(ByteBuf, EncryptedMapValue)>)> { + ENCRYPTED_MAPS + .with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_all_accessible_encrypted_values(ic_cdk::api::msg_caller()) + }) + .into_iter() + .map(|((owner, map_name), encrypted_values)| { + ( + (owner, ByteBuf::from(map_name.as_ref().to_vec())), + encrypted_values + .into_iter() + .map(|(key, value)| (ByteBuf::from(key.as_ref().to_vec()), value)) + .collect(), + ) + }) + .collect() +} + +#[query] +fn get_all_accessible_encrypted_maps() -> Vec> { + ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_all_accessible_encrypted_maps(ic_cdk::api::msg_caller()) + }) +} + +#[query] +fn get_encrypted_value( + map_owner: Principal, + map_name: ByteBuf, + map_key: ByteBuf, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps.as_ref().unwrap().get_encrypted_value( + ic_cdk::api::msg_caller(), + map_id, + bytebuf_to_blob(map_key)?, + ) + }) +} + +#[update] +fn remove_map_values( + map_owner: Principal, + map_name: ByteBuf, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + let result = ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { + encrypted_maps + .as_mut() + .unwrap() + .remove_map_values(ic_cdk::api::msg_caller(), map_id) + }); + result.map(|removed| { + removed + .into_iter() + .map(|key| ByteBuf::from(key.as_ref().to_vec())) + .collect() + }) +} + +#[query] +fn get_owned_non_empty_map_names() -> Vec { + ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_owned_non_empty_map_names(ic_cdk::api::msg_caller()) + .into_iter() + .map(|map_name| ByteBuf::from(map_name.as_slice().to_vec())) + .collect() + }) +} + +#[update] +fn insert_encrypted_value( + map_owner: Principal, + map_name: ByteBuf, + map_key: ByteBuf, + value: EncryptedMapValue, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { + encrypted_maps.as_mut().unwrap().insert_encrypted_value( + ic_cdk::api::msg_caller(), + map_id, + bytebuf_to_blob(map_key)?, + value, + ) + }) +} + +#[update] +fn remove_encrypted_value( + map_owner: Principal, + map_name: ByteBuf, + map_key: ByteBuf, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { + encrypted_maps.as_mut().unwrap().remove_encrypted_value( + ic_cdk::api::msg_caller(), + map_id, + bytebuf_to_blob(map_key)?, + ) + }) +} + +#[update] +async fn get_vetkey_verification_key() -> VetKeyVerificationKey { + ENCRYPTED_MAPS + .with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_vetkey_verification_key() + }) + .await +} + +#[update] +async fn get_encrypted_vetkey( + map_owner: Principal, + map_name: ByteBuf, + transport_key: TransportKey, +) -> Result { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + Ok(ENCRYPTED_MAPS + .with_borrow(|encrypted_maps| { + encrypted_maps.as_ref().unwrap().get_encrypted_vetkey( + ic_cdk::api::msg_caller(), + map_id, + transport_key, + ) + })? + .await) +} + +#[query] +fn get_user_rights( + map_owner: Principal, + map_name: ByteBuf, + user: Principal, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + ENCRYPTED_MAPS.with_borrow(|encrypted_maps| { + encrypted_maps + .as_ref() + .unwrap() + .get_user_rights(ic_cdk::api::msg_caller(), map_id, user) + }) +} + +#[update] +fn set_user_rights( + map_owner: Principal, + map_name: ByteBuf, + user: Principal, + access_rights: AccessRights, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { + encrypted_maps.as_mut().unwrap().set_user_rights( + ic_cdk::api::msg_caller(), + map_id, + user, + access_rights, + ) + }) +} + +#[update] +fn remove_user( + map_owner: Principal, + map_name: ByteBuf, + user: Principal, +) -> Result, String> { + let map_name = bytebuf_to_blob(map_name)?; + let map_id = (map_owner, map_name); + ENCRYPTED_MAPS.with_borrow_mut(|encrypted_maps| { + encrypted_maps + .as_mut() + .unwrap() + .remove_user(ic_cdk::api::msg_caller(), map_id, user) + }) +} + +fn bytebuf_to_blob(buf: ByteBuf) -> Result, String> { + Blob::try_from(buf.as_ref()).map_err(|_| "too large input".to_string()) +} + +fn id_to_memory(id: u8) -> Memory { + MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(id))) +} + +ic_cdk::export_candid!(); diff --git a/rust/vetkeys/password_manager/rust/rust-toolchain.toml b/rust/vetkeys/password_manager/rust/rust-toolchain.toml new file mode 100644 index 000000000..2a2058b04 --- /dev/null +++ b/rust/vetkeys/password_manager/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 From 83fb0c260314fcea836b895bb802cce2e476ac94 Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 17:16:25 +0200 Subject: [PATCH 6/7] fix: restore missing README, dfx.json, Cargo.toml, and symlinks for password_manager --- rust/vetkeys/password_manager/README.md | 59 +++++++++++++++++++ rust/vetkeys/password_manager/motoko/dfx.json | 37 ++++++++++++ rust/vetkeys/password_manager/motoko/frontend | 1 + .../vetkeys/password_manager/motoko/mops.toml | 13 ++++ rust/vetkeys/password_manager/rust/Cargo.toml | 13 ++++ rust/vetkeys/password_manager/rust/dfx.json | 31 ++++++++++ rust/vetkeys/password_manager/rust/frontend | 1 + 7 files changed, 155 insertions(+) create mode 100644 rust/vetkeys/password_manager/README.md create mode 100644 rust/vetkeys/password_manager/motoko/dfx.json create mode 120000 rust/vetkeys/password_manager/motoko/frontend create mode 100644 rust/vetkeys/password_manager/motoko/mops.toml create mode 100644 rust/vetkeys/password_manager/rust/Cargo.toml create mode 100644 rust/vetkeys/password_manager/rust/dfx.json create mode 120000 rust/vetkeys/password_manager/rust/frontend diff --git a/rust/vetkeys/password_manager/README.md b/rust/vetkeys/password_manager/README.md new file mode 100644 index 000000000..d44ee8663 --- /dev/null +++ b/rust/vetkeys/password_manager/README.md @@ -0,0 +1,59 @@ +# VetKey Password Manager + +| Motoko backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager/motoko)| +| --- | --- | +| Rust backend | [![](https://icp.ninja/assets/open.svg)](http://icp.ninja/editor?g=https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager/rust) | + +The **VetKey Password Manager** is an example application demonstrating how to use **VetKeys** and **Encrypted Maps** to build a secure, decentralized password manager on the **Internet Computer (IC)**. This application allows users to create password vaults, store encrypted passwords, and share vaults with other users via their **Internet Identity Principal**. + +## Features + +- **Secure Password Storage**: Uses VetKey to encrypt passwords before storing them in Encrypted Maps. +- **Vault-Based Organization**: Users can create multiple vaults, each containing multiple passwords. +- **Access Control**: Vaults can be shared with other users via their **Internet Identity Principal**. + +## Setup + +### Prerequisites + +- [Local Internet Computer dev environment](https://internetcomputer.org/docs/building-apps/getting-started/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. + +## Running the Project + +### Backend + +The backend consists of an **Encrypted Maps**-enabled canister that securely stores passwords. It is automatically deployed with `dfx deploy`. + +### Frontend + +The frontend is a **Svelte** application providing a user-friendly interface for managing vaults and passwords. + +To run the frontend in development mode with hot reloading: + +```bash +npm run dev +``` + +## Limitations + +This example dapp does not implement key rotation, which is strongly recommended in a production environment. +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. + +## Additional Resources + +- **[Password Manager with Metadata](../password_manager_with_metadata/)** - If you need to store additional metadata alongside passwords. diff --git a/rust/vetkeys/password_manager/motoko/dfx.json b/rust/vetkeys/password_manager/motoko/dfx.json new file mode 100644 index 000000000..3118676e7 --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/dfx.json @@ -0,0 +1,37 @@ +{ + "canisters": { + "ic_vetkeys_encrypted_maps_canister": { + "main": "backend/src/Main.mo", + "type": "motoko", + "args": "--enhanced-orthogonal-persistence", + "init_arg": "(\"test_key_1\")" + }, + "internet-identity": { + "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did", + "type": "custom", + "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", + "remote": { + "id": { + "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" + } + }, + "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz" + }, + "www": { + "dependencies": ["ic_vetkeys_encrypted_maps_canister", "internet-identity"], + "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"], + "frontend": { + "entrypoint": "dist/index.html" + }, + "source": ["dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + }, + "defaults": { + "build": { + "packtool": "npx ic-mops sources", + "args": "" + } + } +} diff --git a/rust/vetkeys/password_manager/motoko/frontend b/rust/vetkeys/password_manager/motoko/frontend new file mode 120000 index 000000000..af288785f --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/frontend @@ -0,0 +1 @@ +../frontend \ No newline at end of file diff --git a/rust/vetkeys/password_manager/motoko/mops.toml b/rust/vetkeys/password_manager/motoko/mops.toml new file mode 100644 index 000000000..593456675 --- /dev/null +++ b/rust/vetkeys/password_manager/motoko/mops.toml @@ -0,0 +1,13 @@ +[package] +name = "ic-vetkeys-encrypted-maps-canister" +version = "0.1.0" +repository = "https://github.com/dfinity/vetkeys/examples/password_manager/motoko" +keywords = [ + "vetkeys,vetkd,encryption,privacy,signature,BLS,key ", + "derivation,IBE" +] +license = "Apache-2.0" + +[dependencies] +base = "0.14.6" +ic-vetkeys = "0.4.0" diff --git a/rust/vetkeys/password_manager/rust/Cargo.toml b/rust/vetkeys/password_manager/rust/Cargo.toml new file mode 100644 index 000000000..0169acf9f --- /dev/null +++ b/rust/vetkeys/password_manager/rust/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = ["backend"] +resolver = "2" + +[workspace.dependencies] +ic-cdk = "0.19.0" +ic-stable-structures = "0.7.0" +ic-vetkeys = "0.6.0" + +[profile.release] +lto = true +opt-level = 'z' +panic = 'abort' \ No newline at end of file diff --git a/rust/vetkeys/password_manager/rust/dfx.json b/rust/vetkeys/password_manager/rust/dfx.json new file mode 100644 index 000000000..356bdb3f5 --- /dev/null +++ b/rust/vetkeys/password_manager/rust/dfx.json @@ -0,0 +1,31 @@ +{ + "canisters": { + "ic_vetkeys_encrypted_maps_canister": { + "candid": "backend/ic_vetkeys_encrypted_maps_canister.did", + "package": "ic-vetkeys-encrypted-maps-canister", + "type": "rust", + "init_arg": "(\"test_key_1\")" + }, + "internet-identity": { + "candid": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity.did", + "type": "custom", + "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", + "remote": { + "id": { + "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" + } + }, + "wasm": "https://github.com/dfinity/internet-identity/releases/download/release-2026-03-16/internet_identity_dev.wasm.gz" + }, + "www": { + "dependencies": ["ic_vetkeys_encrypted_maps_canister", "internet-identity"], + "build": ["cd frontend && npm i --include=dev && npm run build && cd - && rm -r dist > /dev/null 2>&1; mv frontend/dist ./"], + "frontend": { + "entrypoint": "dist/index.html" + }, + "source": ["dist/"], + "type": "assets", + "output_env_file": "frontend/.env" + } + } +} diff --git a/rust/vetkeys/password_manager/rust/frontend b/rust/vetkeys/password_manager/rust/frontend new file mode 120000 index 000000000..af288785f --- /dev/null +++ b/rust/vetkeys/password_manager/rust/frontend @@ -0,0 +1 @@ +../frontend \ No newline at end of file From dfb06717ccb43ed7497816893cafc28d4997cb1c Mon Sep 17 00:00:00 2001 From: Andrea Cerulli Date: Mon, 20 Apr 2026 17:30:28 +0200 Subject: [PATCH 7/7] fix: add missing frontend directory and Cargo.lock for password_manager --- .../password_manager/frontend/.prettierignore | 7 + .../password_manager/frontend/.prettierrc | 5 + .../password_manager/frontend/README.md | 17 + .../frontend/eslint.config.mjs | 56 ++++ .../password_manager/frontend/index.html | 14 + .../password_manager/frontend/package.json | 47 +++ .../frontend/public/.ic-assets.json5 | 10 + ...wered-by-crypto_label-stripe-dark-text.png | Bin 0 -> 6265 bytes ...ered-by-crypto_label-stripe-white-text.png | Bin 0 -> 7672 bytes ...owered-by-crypto_transparent-dark-text.png | Bin 0 -> 12190 bytes ...wered-by-crypto_transparent-white-text.png | Bin 0 -> 11664 bytes .../password_manager/frontend/public/vite.svg | 1 + .../password_manager/frontend/src/App.svelte | 13 + .../password_manager/frontend/src/app.css | 3 + .../frontend/src/assets/svelte.svg | 1 + .../frontend/src/components/Disclaimer.svelte | 26 ++ .../src/components/DisclaimerCopy.svelte | 5 + .../src/components/EditPassword.svelte | 309 ++++++++++++++++++ .../frontend/src/components/EditVault.svelte | 86 +++++ .../frontend/src/components/Header.svelte | 27 ++ .../frontend/src/components/Hero.svelte | 60 ++++ .../src/components/LayoutAuthenticated.svelte | 29 ++ .../src/components/NewPassword.svelte | 108 ++++++ .../src/components/Notifications.svelte | 22 ++ .../frontend/src/components/Password.svelte | 106 ++++++ .../src/components/PasswordEditor.svelte | 68 ++++ .../frontend/src/components/Passwords.svelte | 79 +++++ .../src/components/SharingEditor.svelte | 218 ++++++++++++ .../src/components/SidebarLayout.svelte | 81 +++++ .../frontend/src/components/Spinner.svelte | 5 + .../frontend/src/components/Vault.svelte | 143 ++++++++ .../frontend/src/components/Vaults.svelte | 90 +++++ .../frontend/src/lib/encrypted_maps.ts | 43 +++ .../password_manager/frontend/src/lib/init.ts | 1 + .../frontend/src/lib/password.ts | 56 ++++ .../frontend/src/lib/sleep.ts | 3 + .../frontend/src/lib/vault.ts | 60 ++++ .../password_manager/frontend/src/main.ts | 8 + .../frontend/src/store/auth.ts | 116 +++++++ .../frontend/src/store/draft.ts | 36 ++ .../frontend/src/store/notifications.ts | 30 ++ .../frontend/src/store/vaults.ts | 166 ++++++++++ .../frontend/src/vite-env.d.ts | 2 + .../frontend/svelte.config.js | 7 + .../frontend/tailwind.config.cjs | 6 + .../password_manager/frontend/tsconfig.json | 25 ++ .../password_manager/frontend/vite.config.ts | 44 +++ 47 files changed, 2239 insertions(+) create mode 100644 rust/vetkeys/password_manager/frontend/.prettierignore create mode 100644 rust/vetkeys/password_manager/frontend/.prettierrc create mode 100644 rust/vetkeys/password_manager/frontend/README.md create mode 100644 rust/vetkeys/password_manager/frontend/eslint.config.mjs create mode 100644 rust/vetkeys/password_manager/frontend/index.html create mode 100644 rust/vetkeys/password_manager/frontend/package.json create mode 100644 rust/vetkeys/password_manager/frontend/public/.ic-assets.json5 create mode 100644 rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png create mode 100644 rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-white-text.png create mode 100644 rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png create mode 100644 rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-white-text.png create mode 100644 rust/vetkeys/password_manager/frontend/public/vite.svg create mode 100644 rust/vetkeys/password_manager/frontend/src/App.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/app.css create mode 100644 rust/vetkeys/password_manager/frontend/src/assets/svelte.svg create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Header.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Hero.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Notifications.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Password.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Passwords.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Spinner.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Vault.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/components/Vaults.svelte create mode 100644 rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/lib/init.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/lib/password.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/lib/sleep.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/lib/vault.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/main.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/store/auth.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/store/draft.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/store/notifications.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/store/vaults.ts create mode 100644 rust/vetkeys/password_manager/frontend/src/vite-env.d.ts create mode 100644 rust/vetkeys/password_manager/frontend/svelte.config.js create mode 100644 rust/vetkeys/password_manager/frontend/tailwind.config.cjs create mode 100644 rust/vetkeys/password_manager/frontend/tsconfig.json create mode 100644 rust/vetkeys/password_manager/frontend/vite.config.ts diff --git a/rust/vetkeys/password_manager/frontend/.prettierignore b/rust/vetkeys/password_manager/frontend/.prettierignore new file mode 100644 index 000000000..987c28939 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/.prettierignore @@ -0,0 +1,7 @@ +# Ignore artifacts: +build +coverage +dist +README.md +**/declarations/ + diff --git a/rust/vetkeys/password_manager/frontend/.prettierrc b/rust/vetkeys/password_manager/frontend/.prettierrc new file mode 100644 index 000000000..2b9bd83ee --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/.prettierrc @@ -0,0 +1,5 @@ +{ + "plugins": ["prettier-plugin-svelte"], + "tabWidth": 4, + "overrides": [{ "files": ["*.svelte"], "options": { "parser": "svelte" } }] +} diff --git a/rust/vetkeys/password_manager/frontend/README.md b/rust/vetkeys/password_manager/frontend/README.md new file mode 100644 index 000000000..134a1abb0 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/README.md @@ -0,0 +1,17 @@ +# VetKD Password Manager frontend +Uses the defaults provided by the devkit to implement a VetKD-based password +manager. This utilizes the encrypted maps canister example to realize the +password manager, i.e., there is no dedicated canister implementation, only the +frontend implementation that uses all defaults from the SDK. + +## Step 1: Deploy `encrypted_maps_example` canister and the internet identity canister. + +## Step 2: Tell `frontend` what canisters to communicate with, so the following environment variables must be defined. For a local deployment, one can run `deploy_locally.sh` from that folder. +* `CANISTER_ID_IC_VETKEYS_ENCRYPTED_MAPS_CANISTER` + +## Step 3: Deploy frontend. This returns a link that can be used to access the frontend from the asset canister. +```shell +dfx deploy www +``` +Note: if this returns a URL with the IP `0.0.0.0` and the fronetned does not +work, a potential fix is to replace it with `localhost`. \ No newline at end of file diff --git a/rust/vetkeys/password_manager/frontend/eslint.config.mjs b/rust/vetkeys/password_manager/frontend/eslint.config.mjs new file mode 100644 index 000000000..96aee8e5c --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/eslint.config.mjs @@ -0,0 +1,56 @@ +// @ts-check + +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import svelteConfig from "./svelte.config.js"; +import svelte from "eslint-plugin-svelte"; + +export default tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + ...svelte.configs.recommended, + eslintPluginPrettierRecommended, + { + languageOptions: { + parserOptions: { + projectService: { + defaultProject: "./tsconfig.json", + }, + tsconfigRootDir: import.meta.dirname, + }, + globals: { + ...globals.browser, + ...globals.es2020, + }, + }, + }, + { + files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: [".svelte"], + parser: tseslint.parser, + svelteConfig, + }, + }, + }, + { + ignores: [ + "dist/", + "src/declarations", + "*.config.js", + "*.config.cjs", + "*.config.mjs", + "*.config.ts", + ], + }, + { + rules: { + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + }, + }, +); diff --git a/rust/vetkeys/password_manager/frontend/index.html b/rust/vetkeys/password_manager/frontend/index.html new file mode 100644 index 000000000..89d2ddc2a --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + VetKeys Password Manager + + +
+ + + + diff --git a/rust/vetkeys/password_manager/frontend/package.json b/rust/vetkeys/password_manager/frontend/package.json new file mode 100644 index 000000000..2c2bd3812 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/package.json @@ -0,0 +1,47 @@ +{ + "name": "password-manager-frontend", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint", + "prettier": "prettier --write .", + "prettier-check": "prettier --check .", + "preview": "vite preview" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^12.1.2", + "@tsconfig/svelte": "^5.0.4", + "@typewriter/delta": "^1.2.4", + "daisyui": "^4.12.23", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.2.6", + "eslint-plugin-svelte": "^3.5.1", + "globals": "^16.0.0", + "prettier-plugin-svelte": "^3.4.0", + "svelte": "^4.2.19", + "tslib": "^2.8.1", + "typescript-eslint": "^8.35.1", + "vite": "^5.4.21", + "vite-plugin-environment": "^1.1.3" + }, + "dependencies": { + "@dfinity/agent": "^2.3.0", + "@dfinity/auth-client": "^2.3.0", + "@dfinity/candid": "^2.3.0", + "@dfinity/identity": "^2.3.0", + "@dfinity/principal": "^2.3.0", + "@dfinity/vetkeys": "^0.3.0", + "@popperjs/core": "^2.11.8", + "@sveltejs/vite-plugin-svelte": "^3.0.2", + "@tailwindcss/postcss": "^4.0.6", + "@tailwindcss/vite": "^4.0.0", + "autoprefixer": "^10.4.20", + "rollup-plugin-css-only": "^4.5.2", + "svelte-icons": "^2.1.0", + "svelte-spa-router": "^4.0.1", + "tailwindcss": "^3.0.17", + "typewriter-editor": "^0.9.4" + } +} diff --git a/rust/vetkeys/password_manager/frontend/public/.ic-assets.json5 b/rust/vetkeys/password_manager/frontend/public/.ic-assets.json5 new file mode 100644 index 000000000..a57140a5e --- /dev/null +++ b/rust/vetkeys/password_manager/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' data:;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/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png b/rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_label-stripe-dark-text.png new file mode 100644 index 0000000000000000000000000000000000000000..1a227a2b059107873415a8fa6d61c45908d52756 GIT binary patch literal 6265 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 k5>+ca(Es=%(OZCZ_*S7t&NA}<0giOF*Z=?k literal 0 HcmV?d00001 diff --git a/rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png b/rust/vetkeys/password_manager/frontend/public/img/ic-badge-powered-by-crypto_transparent-dark-text.png new file mode 100644 index 0000000000000000000000000000000000000000..90029c464bf6080f78d19b48c0aea12073448600 GIT binary patch literal 12190 zcmX9^WmFu^5(N_6-5nMW?z;FA++BjZyL)hVmmtC2J!o)uf@E=bf8={Vrl+Q>`c79( zpPoMV+z3T^DP#l!1PBNSWRSGDG6V$V$cOz64(6ksR}x$NxWPL}YdJ$e;86d&AVJDh z7ayIF&dO4v5Y>}JhaV50&4F@22#C5k#1|uI2nc3&kT_7)1M)P-H=bzNb=hF*43bPd z5;5Wd?KcIORyio$$-x051%tOwzfIZkO3I^Fe8{BOuqII^yEu`b_AiEMLOX8+N5TvF zx*El(6Mc8^>30keHVQV4qw1H<*;ut|Y1UZz$PgiW5jFs%^pMgY*np0lN$*$IId=YM z&u2%nP1N+Y$AiPev%^E~^V?$*Nc=BPsA&?7tmGo=QCUJ9KsZQc8Ewe?ExE9$ga=g2 zX|nou9@;nB#H0B3c(l>BNw>TB&C(*-L52xC1rw-4S3*WiHH>lHGOgB5Mot8cG$DsE z)uWvL>yUBrY}`^_vOe|<>v%l)!}^4bS~h1C+bB57JK28Kx1`U-XZ}Qag2FjlH_h

B-^J2-2d7W%HLj zUiX<9yDsx)vrKUhKF_k$!^)3IfMJTP;k}o6p(L{L8tE_zyya5j;Ap?Xoo}lTsTliI}TC9lF7wmg93&n1remsmY?Oa;iTbP`TDZF z6m2{0+S%ZtTLM8>X_4$|X-VNct}&{5EuP%e@#x8ppKO?Oy)31a*7T>i#80zT{275%$nEIjf1eUL4e<$9uF7i($@*~t$Tb>eUcjCNFU$xJ z3}Z$aWKIc>2G2|G=QLcJLOEs2Echkh1JTINq5G;^pp7zEpY z8DMk15xrw+dH?gIf978iMgJEP7EuQ^`{|!yq?>$AZyo;;i?oZDv7)0T@^4&pFb8I4 zL%@GF2eZ*O3*jki(Dh>e@}L_@ku#)Jh*pcGT)M zC2`T>GA2l)(~aG(|BN(n18u3j#dOj8?jUZ3}3FRH=( zyvGa;GB8ZZ)K=~RWg`oe=QM92m1gjoL*Ou*fXZ!{tV%WGQu2h~1VIH%*HkPoQu5yY)vrR^ zc)?(Gs`Tn5nf1%Sk^_X1Tpv&%$Rq-k?oq@l-SoO)zUQ=OBD1YS&~~PhDrf5rIAa<( zXa|29aDjLApONL;tcCShzut>NOizXggRrc)7kIrA6>c2;n$j`cnL}x=Xd>6DU36xj zd|5aL*?;P!s@;X{A%iCRO<$s@b&*8Sd7bB66O!QkKBy)3eH{uZZw}mogv5@w{CUC# z;RlaQCM^Wvu90Ot6;T~~Bb3FlCG+Ok+BqJB0frmXxmVjfz#mPoA?3v41En? z*1!mGa4K7@WC!Aku(O^w)m;8=qE5z4cu*|!7QZ2T41PD)z-^Kj`c||%adZHQV^P;e z;J5q(f=e7~`#9~FvENX|HF|8G>2?@WDlAjQ523NAdas^g7kCr9-|kizCsy>#>2UF))G8{QUD_!BF>&>UEgVB zjMwUn*G7Gq(+g)R%*D*eVA)P~a5nwCw@d1TBlnbXJV!1Acoi#_;3ha

kGJ?@15| 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 literal 0 HcmV?d00001 diff --git a/rust/vetkeys/password_manager/frontend/public/vite.svg b/rust/vetkeys/password_manager/frontend/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rust/vetkeys/password_manager/frontend/src/App.svelte b/rust/vetkeys/password_manager/frontend/src/App.svelte new file mode 100644 index 000000000..ed34e5e20 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/App.svelte @@ -0,0 +1,13 @@ + + +{#if $auth.state === "initialized"} + +{:else} + +{/if} + diff --git a/rust/vetkeys/password_manager/frontend/src/app.css b/rust/vetkeys/password_manager/frontend/src/app.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/rust/vetkeys/password_manager/frontend/src/assets/svelte.svg b/rust/vetkeys/password_manager/frontend/src/assets/svelte.svg new file mode 100644 index 000000000..c5e08481f --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rust/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte b/rust/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte new file mode 100644 index 000000000..2ded84f8f --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Disclaimer.svelte @@ -0,0 +1,26 @@ + + +{#if !isDismissed} +

+

+ +

+ + +
+{/if} diff --git a/rust/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte b/rust/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte new file mode 100644 index 000000000..336ffde7d --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/DisclaimerCopy.svelte @@ -0,0 +1,5 @@ + + +Disclaimer: This sample dapp is intended exclusively for experimental +purpose. You are advised not to use this dapp for storing your critical data such +as keys or passwords. diff --git a/rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte b/rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte new file mode 100644 index 000000000..73cc65f46 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/EditPassword.svelte @@ -0,0 +1,309 @@ + + +{#if editedPassword.parentVaultName.length > 0} +
+ Edit password + +
+
+ {#if $vaultsStore.state === "loaded"} +
+ + + +
+ + + + Back + + + +
+ {:else if $vaultsStore.state === "loading"} + Loading password... + {/if} +
+{:else} +
+ Edit password +
+
+ {#if $vaultsStore.state === "loading"} + + Loading password... + {:else if $vaultsStore.state === "loaded"} +
Could not find password.
+ {/if} +
+{/if} diff --git a/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte b/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte new file mode 100644 index 000000000..5c156fcc8 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/EditVault.svelte @@ -0,0 +1,86 @@ + + +{#if editedVault} +
+ Edit vault + +
+
+ {#if $vaultsStore.state === "loaded"} +
+ + {:else if $vaultsStore.state === "loading"} + Loading vaults... + {/if} +
+{:else} +
+ Edit vault +
+
+ {#if $vaultsStore.state === "loading"} + + Loading vault... + {:else if $vaultsStore.state === "loaded"} +
Could not find vault.
+ {/if} +
+{/if} diff --git a/rust/vetkeys/password_manager/frontend/src/components/Header.svelte b/rust/vetkeys/password_manager/frontend/src/components/Header.svelte new file mode 100644 index 000000000..9245e48b6 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Header.svelte @@ -0,0 +1,27 @@ + + + diff --git a/rust/vetkeys/password_manager/frontend/src/components/Hero.svelte b/rust/vetkeys/password_manager/frontend/src/components/Hero.svelte new file mode 100644 index 000000000..3f7ba7b22 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Hero.svelte @@ -0,0 +1,60 @@ + + +
+
+
+

+ Password Manager +

+

+ Your private passwords on the Internet Computer. +

+

+ A safe place to store your personal lists, thoughts, ideas or + passphrases and much more... +

+ + {#if auth.state === "initializing-auth"} +
+ + Initializing... +
+ {:else if auth.state === "anonymous"} + + {:else if auth.state === "error"} +
An error occurred.
+ {/if} + +
+ +
+
+
+
+ + Powered by the Internet Computer +
+
diff --git a/rust/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte b/rust/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte new file mode 100644 index 000000000..090735f21 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/LayoutAuthenticated.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte b/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte new file mode 100644 index 000000000..7270bc0bb --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/NewPassword.svelte @@ -0,0 +1,108 @@ + + + + +
+ New password +
+ +
+ +
+ + + +
+ + +
diff --git a/rust/vetkeys/password_manager/frontend/src/components/Notifications.svelte b/rust/vetkeys/password_manager/frontend/src/components/Notifications.svelte new file mode 100644 index 000000000..af1b01eb5 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Notifications.svelte @@ -0,0 +1,22 @@ + + +
+ {#each $notifications as n (n.id)} +
+

{n.message}

+
+ {/each} +
diff --git a/rust/vetkeys/password_manager/frontend/src/components/Password.svelte b/rust/vetkeys/password_manager/frontend/src/components/Password.svelte new file mode 100644 index 000000000..9b30c30a2 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Password.svelte @@ -0,0 +1,106 @@ + + +
+ + Password: {password.passwordName} + + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New password + {/if} + +
+ +
+ {#if $vaultsStore.state === "loading"} + + Loading password... + {:else if $vaultsStore.state === "loaded"} + {#if password.parentVaultName === ""} +
+ There is no such password in this vault. +
+ + {:else} +
+
+

+ {password.passwordName}: "{password.content}" +

+
+
+ {/if} +
+ + {/if} +
diff --git a/rust/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte b/rust/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte new file mode 100644 index 000000000..a27c1ba12 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/PasswordEditor.svelte @@ -0,0 +1,68 @@ + + + +
+ + + + +
+
+ +
+ + diff --git a/rust/vetkeys/password_manager/frontend/src/components/Passwords.svelte b/rust/vetkeys/password_manager/frontend/src/components/Passwords.svelte new file mode 100644 index 000000000..70e7f30b3 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Passwords.svelte @@ -0,0 +1,79 @@ + + +
+ Your passwords + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New Password + {/if} + +
+
+ {#if $vaultsStore.state === "loading"} + + Loading passwords... + {:else if $vaultsStore.state === "loaded"} + {#if $vaultsStore.list.length > 0} +
+ +
+ +
+ {#each filteredVaults as vault (vault.name)} + {#each Array.from(vault.passwords.map(([, password]) => password)) as password (password.passwordName)} + + {/each} + {/each} +
+ {:else} +
You don't have any notes.
+ + {/if} + {:else if $vaultsStore.state === "error"} +
Could not load passwords.
+ {/if} +
diff --git a/rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte b/rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte new file mode 100644 index 000000000..53d6b644f --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/SharingEditor.svelte @@ -0,0 +1,218 @@ + + +

Users

+{#if canManage} +

+ Add users by their principal to allow them viewing or editing the vault. +

+{:else} +

+ This vault is shared with you. It is + owned by {editedVault.owner}. +

+

Users with whom the vault is shared:

+{/if} +
+ {#each editedVault.users as sharing (sharing[0].toText())} + + {/each} + + + +
diff --git a/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte b/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte new file mode 100644 index 000000000..94959e666 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/SidebarLayout.svelte @@ -0,0 +1,81 @@ + + +
+ +
+
+ +
+ +
+
+
+
diff --git a/rust/vetkeys/password_manager/frontend/src/components/Spinner.svelte b/rust/vetkeys/password_manager/frontend/src/components/Spinner.svelte new file mode 100644 index 000000000..cc26ba251 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Spinner.svelte @@ -0,0 +1,5 @@ + + + diff --git a/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte b/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte new file mode 100644 index 000000000..5c578a5fa --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Vault.svelte @@ -0,0 +1,143 @@ + + +
+ + + + + Vault: {vault.name} + + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New password + {/if} + +
+ +
+ {#if $vaultsStore.state === "loading"} + + Loading vault... + {:else if $vaultsStore.state === "loaded"} +
+

+ {vaultSummary} +

+
+
+ + +
+ +
+

Passwords

+
+ {#if vault.passwords.length === 0} +
+ You don't have any passwords in this vault. +
+ + {:else} +
+ {#each vault.passwords as password ((password[1].owner, password[1].parentVaultName, password[1].passwordName))} + +
+

+ {password[1].passwordName}: "{password[1] + .content}" +

+
+
+ {/each} +
+ {/if} +
+ + {/if} +
diff --git a/rust/vetkeys/password_manager/frontend/src/components/Vaults.svelte b/rust/vetkeys/password_manager/frontend/src/components/Vaults.svelte new file mode 100644 index 000000000..fc99e9b3f --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/components/Vaults.svelte @@ -0,0 +1,90 @@ + + +
+ Your vaults + + {#if $vaultsStore.state === "loaded" && $vaultsStore.list.length > 0} + New password + {/if} + +
+
+ {#if $vaultsStore.state === "loading"} + + Loading vaults... + {:else if $vaultsStore.state === "loaded"} + {#if $vaultsStore.list.length > 0} +
+ +
+ +
+ {#each filteredVaults as vault ([vault.owner, vault.name])} + +
+

+ "{vault.name}" owned by {vault.owner.toText()} +

+
+
+ {/each} +
+ {:else} +
+ You don't have any vaults. +
+ + {/if} + {:else if $vaultsStore.state === "error"} +
Could not load vaults.
+ {/if} +
diff --git a/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts new file mode 100644 index 000000000..2b2c7e071 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/lib/encrypted_maps.ts @@ -0,0 +1,43 @@ +import "./init.ts"; +import { HttpAgent, type HttpAgentOptions } from "@dfinity/agent"; +import { + DefaultEncryptedMapsClient, + EncryptedMaps, +} from "@dfinity/vetkeys/encrypted_maps"; + +export async function createEncryptedMaps( + agentOptions?: HttpAgentOptions, +): Promise { + const host = + process.env.DFX_NETWORK === "ic" + ? `https://${process.env.CANISTER_ID_IC_VETKEYS_ENCRYPTED_MAPS_CANISTER}.ic0.app` + : "http://localhost:4943"; + const hostOptions = { host }; + + if (!agentOptions) { + agentOptions = hostOptions; + } else { + agentOptions.host = hostOptions.host; + } + + const agent = await HttpAgent.create({ ...agentOptions }); + // Fetch root key for certificate validation during development + if (process.env.NODE_ENV !== "production") { + console.log(`Dev environment - fetching root key...`); + + agent.fetchRootKey().catch((err) => { + console.warn( + "Unable to fetch root key. Check to ensure that your local replica is running", + ); + console.error(err); + }); + } + + // Creates an actor with using the candid interface and the HttpAgent + return new EncryptedMaps( + new DefaultEncryptedMapsClient( + agent, + process.env.CANISTER_ID_IC_VETKEYS_ENCRYPTED_MAPS_CANISTER, + ), + ); +} diff --git a/rust/vetkeys/password_manager/frontend/src/lib/init.ts b/rust/vetkeys/password_manager/frontend/src/lib/init.ts new file mode 100644 index 000000000..062c8af94 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/lib/init.ts @@ -0,0 +1 @@ +window.global ||= window; diff --git a/rust/vetkeys/password_manager/frontend/src/lib/password.ts b/rust/vetkeys/password_manager/frontend/src/lib/password.ts new file mode 100644 index 000000000..e37c46204 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/lib/password.ts @@ -0,0 +1,56 @@ +import type { Principal } from "@dfinity/principal"; + +export interface PasswordModel { + owner: Principal; + parentVaultName: string; + passwordName: string; + content: string; +} + +export function passwordFromContent( + owner: Principal, + parentVaultName: string, + passwordName: string, + content: string, +): PasswordModel { + return { + owner, + parentVaultName, + passwordName, + content, + }; +} + +export function summarize(note: PasswordModel, 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): string { + const title = el.querySelector("h1"); + if (title) { + return title.innerText; + } + + const blockElements = el.querySelectorAll("h1,h2,p,li"); + for (const el of blockElements) { + if (el.textContent && el.textContent.trim().length > 0) { + return el.textContent.trim(); + } + } + return ""; +} + +export function extractTitle(html: string) { + const div = document.createElement("div"); + div.innerHTML = html; + return extractTitleFromDomEl(div); +} diff --git a/rust/vetkeys/password_manager/frontend/src/lib/sleep.ts b/rust/vetkeys/password_manager/frontend/src/lib/sleep.ts new file mode 100644 index 000000000..38caca0c1 --- /dev/null +++ b/rust/vetkeys/password_manager/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/password_manager/frontend/src/lib/vault.ts b/rust/vetkeys/password_manager/frontend/src/lib/vault.ts new file mode 100644 index 000000000..02dc5b712 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/lib/vault.ts @@ -0,0 +1,60 @@ +import type { Principal } from "@dfinity/principal"; +import type { PasswordModel } from "./password"; +import type { AccessRights } from "@dfinity/vetkeys/encrypted_maps"; + +export interface VaultModel { + owner: Principal; + name: string; + passwords: Array<[string, PasswordModel]>; + users: Array<[Principal, AccessRights]>; +} + +export function vaultFromContent( + owner: Principal, + name: string, + passwords: Array<[string, PasswordModel]>, + users: Array<[Principal, AccessRights]>, +): VaultModel { + return { owner, name, passwords, users }; +} + +export function summarize(vault: VaultModel, maxLength = 1500) { + const div = document.createElement("div"); + + div.innerHTML += + "Owner: " + + vault.owner.toString() + + ", " + + vault.users.length + + " users"; + div.innerHTML += ", " + vault.passwords.length + " passwords.\n"; + + 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): string { + const title = el.querySelector("h1"); + if (title) { + return title.innerText; + } + + const blockElements = el.querySelectorAll("h1,h2,p,li"); + for (const el of blockElements) { + if (el.textContent && el.textContent?.trim().length > 0) { + return el.textContent.trim(); + } + } + return ""; +} + +export function extractTitle(html: string) { + const div = document.createElement("div"); + div.innerHTML = html; + return extractTitleFromDomEl(div); +} diff --git a/rust/vetkeys/password_manager/frontend/src/main.ts b/rust/vetkeys/password_manager/frontend/src/main.ts new file mode 100644 index 000000000..ff634fcc2 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/main.ts @@ -0,0 +1,8 @@ +import "./app.css"; +import App from "./App.svelte"; + +const app = new App({ + target: document.body, +}); + +export default app; diff --git a/rust/vetkeys/password_manager/frontend/src/store/auth.ts b/rust/vetkeys/password_manager/frontend/src/store/auth.ts new file mode 100644 index 000000000..ec6547c8b --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/store/auth.ts @@ -0,0 +1,116 @@ +import "../lib/init.ts"; +import { get, writable } from "svelte/store"; +import { AuthClient } from "@dfinity/auth-client"; +import type { JsonnableDelegationChain } from "@dfinity/identity/lib/cjs/identity/delegation"; +import { replace } from "svelte-spa-router"; +import { createEncryptedMaps } from "../lib/encrypted_maps.js"; +import { EncryptedMaps } from "@dfinity/vetkeys/encrypted_maps"; + +export type AuthState = + | { + state: "initializing-auth"; + } + | { + state: "anonymous"; + client: AuthClient; + } + | { + state: "initialized"; + encryptedMaps: EncryptedMaps; + client: AuthClient; + } + | { + state: "error"; + error: string; + }; + +export const auth = writable({ + state: "initializing-auth", +}); + +async function initAuth() { + const client = await AuthClient.create(); + if (await client.isAuthenticated()) { + void authenticate(client); + } else { + auth.update(() => ({ + state: "anonymous", + client, + })); + } +} + +void initAuth(); + +export function login() { + const currentAuth = get(auth); + + if (currentAuth.state === "anonymous") { + void currentAuth.client.login({ + maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000), + identityProvider: + process.env.DFX_NETWORK === "ic" + ? "https://identity.ic0.app/#authorize" + : `http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943/#authorize`, + onSuccess: () => authenticate(currentAuth.client), + }); + } +} + +export async function logout() { + const currentAuth = get(auth); + + if (currentAuth.state === "initialized") { + await currentAuth.client.logout(); + auth.update(() => ({ + state: "anonymous", + client: currentAuth.client, + })); + void replace("/"); + } +} + +export async function authenticate(client: AuthClient) { + handleSessionTimeout(); + + try { + const encryptedMaps = await createEncryptedMaps({ + identity: client.getIdentity(), + }); + + auth.update(() => ({ + state: "initialized", + encryptedMaps, + client, + })); + } catch (e) { + auth.update(() => ({ + state: "error", + error: (e as Error).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(() => { + void logout(); + }, expirationTimeMs - Date.now()); + } catch { + console.error("Could not handle delegation expiry."); + } + }); +} diff --git a/rust/vetkeys/password_manager/frontend/src/store/draft.ts b/rust/vetkeys/password_manager/frontend/src/store/draft.ts new file mode 100644 index 000000000..05835819e --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/store/draft.ts @@ -0,0 +1,36 @@ +import { writable } from "svelte/store"; +import { auth } from "./auth"; + +interface DraftModel { + content: string; +} + +let initialDraft: DraftModel = { + content: "", +}; + +try { + const getDraft = localStorage.getItem("draft"); + if (getDraft) { + const savedDraft: DraftModel = JSON.parse(getDraft) as DraftModel; + if ("content" in savedDraft && "tags" in savedDraft) { + initialDraft = savedDraft; + } + } else { + throw new Error("Draft not found"); + } +} catch { + // ignore error +} + +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/password_manager/frontend/src/store/notifications.ts b/rust/vetkeys/password_manager/frontend/src/store/notifications.ts new file mode 100644 index 000000000..d87630153 --- /dev/null +++ b/rust/vetkeys/password_manager/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: Error, message: string): never { + addNotification({ type: "error", message }); + console.error(e); + console.error(e.stack); + throw e; +} diff --git a/rust/vetkeys/password_manager/frontend/src/store/vaults.ts b/rust/vetkeys/password_manager/frontend/src/store/vaults.ts new file mode 100644 index 000000000..0abfadf65 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/store/vaults.ts @@ -0,0 +1,166 @@ +import { writable } from "svelte/store"; +import { passwordFromContent, type PasswordModel } from "../lib/password"; +import { vaultFromContent, type VaultModel } from "../lib/vault"; +import { auth } from "./auth"; +import { showError } from "./notifications"; +import { + type AccessRights, + EncryptedMaps, +} from "@dfinity/vetkeys/encrypted_maps"; +import type { Principal } from "@dfinity/principal"; + +export const vaultsStore = writable< + | { + state: "uninitialized"; + } + | { + state: "loading"; + } + | { + state: "loaded"; + list: VaultModel[]; + } + | { + state: "error"; + } +>({ state: "uninitialized" }); + +let vaultPollerHandle: ReturnType | null; + +function updateVaults(vaults: VaultModel[]) { + vaultsStore.set({ + state: "loaded", + list: vaults, + }); +} + +export async function refreshVaults(encryptedMaps: EncryptedMaps) { + const allMaps = await encryptedMaps.getAllAccessibleMaps(); + const vaults = allMaps.map((mapData) => { + const vaultName = new TextDecoder().decode(mapData.mapName); + const passwords = new Array<[string, PasswordModel]>(); + for (const [passwordNameBytes, data] of mapData.keyvals) { + const passwordName = new TextDecoder().decode(passwordNameBytes); + const passwordContent = new TextDecoder().decode( + Uint8Array.from(data), + ); + const password = passwordFromContent( + mapData.mapOwner, + vaultName, + passwordName, + passwordContent, + ); + passwords.push([passwordName, password]); + } + return vaultFromContent( + mapData.mapOwner, + vaultName, + passwords, + mapData.accessControl, + ); + }); + + updateVaults(vaults); +} + +export async function addPassword( + password: PasswordModel, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.setValue( + password.owner, + new TextEncoder().encode(password.parentVaultName), + new TextEncoder().encode(password.passwordName), + new TextEncoder().encode(password.content), + ); +} + +export async function removePassword( + password: PasswordModel, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.removeEncryptedValue( + password.owner, + new TextEncoder().encode(password.parentVaultName), + new TextEncoder().encode(password.passwordName), + ); +} + +export async function updatePassword( + password: PasswordModel, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.setValue( + password.owner, + new TextEncoder().encode(password.parentVaultName), + new TextEncoder().encode(password.passwordName), + new TextEncoder().encode(password.content), + ); +} + +export async function addUser( + owner: Principal, + vaultName: string, + user: Principal, + userRights: AccessRights, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.setUserRights( + owner, + new TextEncoder().encode(vaultName), + user, + userRights, + ); +} + +export async function removeUser( + owner: Principal, + vaultName: string, + user: Principal, + encryptedMaps: EncryptedMaps, +) { + await encryptedMaps.removeUser( + owner, + new TextEncoder().encode(vaultName), + user, + ); +} + +auth.subscribe((auth) => { + void (async () => { + if (auth && auth.state === "initialized") { + if (vaultPollerHandle !== null) { + clearInterval(vaultPollerHandle); + vaultPollerHandle = null; + } + + vaultsStore.set({ + state: "loading", + }); + try { + await refreshVaults(auth.encryptedMaps).catch((e: Error) => + showError(e, "Could not poll vaults."), + ); + + vaultPollerHandle = setInterval(() => { + void (async () => { + await refreshVaults(auth.encryptedMaps).catch( + (e: Error) => + showError(e, "Could not poll vaults."), + ); + }); + }, 3000); + } catch { + vaultsStore.set({ + state: "error", + }); + } + } else if (auth.state === "anonymous" && vaultPollerHandle !== null) { + clearInterval(vaultPollerHandle); + vaultPollerHandle = null; + vaultsStore.set({ + state: "uninitialized", + }); + } + })(); +}); diff --git a/rust/vetkeys/password_manager/frontend/src/vite-env.d.ts b/rust/vetkeys/password_manager/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..4078e7476 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/rust/vetkeys/password_manager/frontend/svelte.config.js b/rust/vetkeys/password_manager/frontend/svelte.config.js new file mode 100644 index 000000000..b0683fd24 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/rust/vetkeys/password_manager/frontend/tailwind.config.cjs b/rust/vetkeys/password_manager/frontend/tailwind.config.cjs new file mode 100644 index 000000000..4bcec2567 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/tailwind.config.cjs @@ -0,0 +1,6 @@ +import daisyui from "daisyui"; + +export default { + content: ["./index.html", "./src/**/*.{svelte,js,ts,jsx,tsx}"], + plugins: [daisyui], +}; diff --git a/rust/vetkeys/password_manager/frontend/tsconfig.json b/rust/vetkeys/password_manager/frontend/tsconfig.json new file mode 100644 index 000000000..2f9865d46 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ES2020", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "src" + ] + } + \ No newline at end of file diff --git a/rust/vetkeys/password_manager/frontend/vite.config.ts b/rust/vetkeys/password_manager/frontend/vite.config.ts new file mode 100644 index 000000000..811003329 --- /dev/null +++ b/rust/vetkeys/password_manager/frontend/vite.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' +import tailwindcss from 'tailwindcss' +import autoprefixer from "autoprefixer"; +import css from 'rollup-plugin-css-only'; +import typescript from '@rollup/plugin-typescript'; +import environment from 'vite-plugin-environment'; +import path from 'path'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + svelte(), + css({ output: "bundle.css" }), + typescript({ + inlineSources: true, + }), + environment("all", { prefix: "CANISTER_" }), + environment("all", { prefix: "DFX_" }), + ], + css: { + postcss: { + plugins: [autoprefixer(), tailwindcss()], + } + }, + build: { + sourcemap: true, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, + resolve: { + alias: { + 'ic_vetkeys': path.resolve(__dirname, '../../../frontend/ic_vetkeys/src'), + 'ic_vetkeys/encrypted_maps': path.resolve(__dirname, '../../../frontend/ic_vetkeys/src/encrypted_maps'), + } + }, + root: "./", + server: { + hmr: false + } +}) \ No newline at end of file