From 68b59651daf265e000b73ed1e58005faaba78a76 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Jul 2026 16:49:28 +0200 Subject: [PATCH 1/6] chore(vetkeys): split basic_ibe into self-contained motoko + rust examples Previously basic_ibe lived only under rust/vetkeys/basic_ibe with a shared frontend symlinked into motoko/ and rust/ subdirs. Split it into two self-contained examples that match the basic_vetkd layout: motoko/vetkeys/basic_ibe/ backend/app.mo + own frontend/ + icp.yaml rust/vetkeys/basic_ibe/ backend/ (flattened) + own frontend/ + icp.yaml Frontend - Duplicated per language; bindings now generated by the @icp-sdk/bindgen Vite plugin from the committed backend/backend.did (replaces the shared scripts/gen_bindings.sh that always used the Rust .did). - Slimmed to the established shape: dropped eslint/prettier/gen_bindings and the BACKEND-env dev logic; added a root workspace package.json so `npm run dev` / `npm run build` run from the example root. Backend - Motoko: idiomatic camelCase public interface (getMyMessages, .encryptedMessage, ...); the vetKD management-canister interface is kept snake_case (fixed system API). moc 1.11.0 + --default-persistent-actors (dropped the now-redundant `persistent` keyword); dropped unused sha2 dep. Committed backend/backend.did. - Rust: unchanged (snake_case), just relocated to the flattened layout. Docs & CI - README split per language (cross-linked), prerequisites link each tool with its install command inline. - vetkeys-basic-ibe.yml: motoko + rust jobs now target the new paths, watch both motoko/** and rust/**, and run test.sh. CODEOWNERS is unchanged: /motoko/vetkeys/ and /rust/vetkeys/ both already map to @dfinity/core-protocol, so both examples keep that owner. Verified locally: Motoko deploy + test.sh + state survives an upgrade; Rust cargo/wasm build + deploy + test.sh; both frontends build and serve. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/vetkeys-basic-ibe.yml | 28 +- motoko/vetkeys/basic_ibe/README.md | 87 +++++ .../vetkeys/basic_ibe/backend/app.mo | 60 +-- motoko/vetkeys/basic_ibe/backend/backend.did | 26 ++ motoko/vetkeys/basic_ibe/frontend/.gitignore | 3 + motoko/vetkeys/basic_ibe/frontend/index.html | 13 + .../vetkeys/basic_ibe/frontend/package.json | 20 + .../frontend/public/.ic-assets.json5 | 10 + .../basic_ibe/frontend/public/vite.svg | 1 + motoko/vetkeys/basic_ibe/frontend/src/main.ts | 368 ++++++++++++++++++ .../vetkeys/basic_ibe/frontend/src/style.css | 304 +++++++++++++++ .../basic_ibe/frontend/src/vite-env.d.ts | 1 + .../vetkeys/basic_ibe/frontend/tsconfig.json | 24 ++ .../vetkeys/basic_ibe/frontend/vite.config.ts | 49 +++ .../vetkeys/basic_ibe}/icp.yaml | 9 +- motoko/vetkeys/basic_ibe/mops.toml | 16 + motoko/vetkeys/basic_ibe/package.json | 13 + motoko/vetkeys/basic_ibe/test.sh | 14 + motoko/vetkeys/basic_ibe/ui_screenshot.png | Bin 0 -> 84227 bytes rust/vetkeys/basic_ibe/{rust => }/Cargo.toml | 0 rust/vetkeys/basic_ibe/README.md | 96 +++-- .../basic_ibe/{rust => }/backend/Cargo.toml | 0 .../basic_ibe/{rust => }/backend/Makefile | 0 .../basic_ibe/{rust => }/backend/backend.did | 0 .../basic_ibe/{rust => }/backend/src/lib.rs | 0 .../basic_ibe/{rust => }/backend/src/types.rs | 0 rust/vetkeys/basic_ibe/frontend/.gitignore | 3 + rust/vetkeys/basic_ibe/frontend/.prettierrc | 4 - .../basic_ibe/frontend/eslint.config.mjs | 30 -- rust/vetkeys/basic_ibe/frontend/package.json | 48 +-- .../frontend/scripts/gen_bindings.sh | 21 - rust/vetkeys/basic_ibe/frontend/src/main.ts | 6 +- .../vetkeys/basic_ibe/frontend/vite.config.ts | 72 ++-- rust/vetkeys/basic_ibe/{rust => }/icp.yaml | 9 +- rust/vetkeys/basic_ibe/motoko/frontend | 1 - rust/vetkeys/basic_ibe/motoko/mops.toml | 10 - rust/vetkeys/basic_ibe/package.json | 13 + .../basic_ibe/{rust => }/rust-toolchain.toml | 0 rust/vetkeys/basic_ibe/rust/frontend | 1 - rust/vetkeys/basic_ibe/test.sh | 14 + 40 files changed, 1140 insertions(+), 234 deletions(-) create mode 100644 motoko/vetkeys/basic_ibe/README.md rename rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo => motoko/vetkeys/basic_ibe/backend/app.mo (63%) create mode 100644 motoko/vetkeys/basic_ibe/backend/backend.did create mode 100644 motoko/vetkeys/basic_ibe/frontend/.gitignore create mode 100644 motoko/vetkeys/basic_ibe/frontend/index.html create mode 100644 motoko/vetkeys/basic_ibe/frontend/package.json create mode 100644 motoko/vetkeys/basic_ibe/frontend/public/.ic-assets.json5 create mode 100644 motoko/vetkeys/basic_ibe/frontend/public/vite.svg create mode 100644 motoko/vetkeys/basic_ibe/frontend/src/main.ts create mode 100644 motoko/vetkeys/basic_ibe/frontend/src/style.css create mode 100644 motoko/vetkeys/basic_ibe/frontend/src/vite-env.d.ts create mode 100644 motoko/vetkeys/basic_ibe/frontend/tsconfig.json create mode 100644 motoko/vetkeys/basic_ibe/frontend/vite.config.ts rename {rust/vetkeys/basic_ibe/motoko => motoko/vetkeys/basic_ibe}/icp.yaml (63%) create mode 100644 motoko/vetkeys/basic_ibe/mops.toml create mode 100644 motoko/vetkeys/basic_ibe/package.json create mode 100755 motoko/vetkeys/basic_ibe/test.sh create mode 100644 motoko/vetkeys/basic_ibe/ui_screenshot.png rename rust/vetkeys/basic_ibe/{rust => }/Cargo.toml (100%) rename rust/vetkeys/basic_ibe/{rust => }/backend/Cargo.toml (100%) rename rust/vetkeys/basic_ibe/{rust => }/backend/Makefile (100%) rename rust/vetkeys/basic_ibe/{rust => }/backend/backend.did (100%) rename rust/vetkeys/basic_ibe/{rust => }/backend/src/lib.rs (100%) rename rust/vetkeys/basic_ibe/{rust => }/backend/src/types.rs (100%) create mode 100644 rust/vetkeys/basic_ibe/frontend/.gitignore delete mode 100644 rust/vetkeys/basic_ibe/frontend/.prettierrc delete mode 100644 rust/vetkeys/basic_ibe/frontend/eslint.config.mjs delete mode 100755 rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh rename rust/vetkeys/basic_ibe/{rust => }/icp.yaml (70%) delete mode 120000 rust/vetkeys/basic_ibe/motoko/frontend delete mode 100644 rust/vetkeys/basic_ibe/motoko/mops.toml create mode 100644 rust/vetkeys/basic_ibe/package.json rename rust/vetkeys/basic_ibe/{rust => }/rust-toolchain.toml (100%) delete mode 120000 rust/vetkeys/basic_ibe/rust/frontend create mode 100755 rust/vetkeys/basic_ibe/test.sh diff --git a/.github/workflows/vetkeys-basic-ibe.yml b/.github/workflows/vetkeys-basic-ibe.yml index 132c81eb65..8f9d0cb593 100644 --- a/.github/workflows/vetkeys-basic-ibe.yml +++ b/.github/workflows/vetkeys-basic-ibe.yml @@ -6,6 +6,7 @@ on: - master pull_request: paths: + - motoko/vetkeys/basic_ibe/** - rust/vetkeys/basic_ibe/** - .github/workflows/vetkeys-basic-ibe.yml @@ -14,23 +15,30 @@ concurrency: cancel-in-progress: true jobs: - rust: + motoko: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 + container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy Basic Ibe Rust - working-directory: rust/vetkeys/basic_ibe/rust - run: icp network start -d && icp deploy - motoko: + - name: Deploy and test + working-directory: motoko/vetkeys/basic_ibe + run: | + icp network start -d + icp deploy + bash test.sh + + rust: runs-on: ubuntu-24.04 - container: ghcr.io/dfinity/icp-dev-env-motoko:1.0.1 + container: ghcr.io/dfinity/icp-dev-env-rust:1.0.1 env: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy Basic Ibe Motoko - working-directory: rust/vetkeys/basic_ibe/motoko - run: icp network start -d && icp deploy + - name: Deploy and test + working-directory: rust/vetkeys/basic_ibe + run: | + icp network start -d + icp deploy + bash test.sh diff --git a/motoko/vetkeys/basic_ibe/README.md b/motoko/vetkeys/basic_ibe/README.md new file mode 100644 index 0000000000..3fcbc31f56 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/README.md @@ -0,0 +1,87 @@ +# Identity-Based Encryption (Motoko) + +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/vetkeys/basic_ibe) + +Also available in: [Rust](../../../rust/vetkeys/basic_ibe) + +The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. Users send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. The canister (IC smart contract) ensures that only the authorized user can access their private decryption key — even if someone else knows your principal, they cannot decrypt messages intended for you, because neither other users nor this canister can access your private key. + +Note that generally it is possible for a canister to request a decryption key to decrypt secrets as part of its code. However, doing so requires the canister to provide its own transport key instead of requesting a user's transport key, and this inherently makes the secrets public. Such functionality can be detected by inspecting the code, so it is crucial that canisters using VetKeys have their code public to allow verifying that the canister handles secrets in a secure way. + +![UI Screenshot](ui_screenshot.png) + +## Features + +- **Secure Messaging**: Uses the IBE capabilities of IC VetKeys to encrypt messages that can only be decrypted by the intended recipient. +- **Principal-Based Encryption**: Messages are encrypted using the recipient's principal as the public key identifier. +- **Private Key Management**: Each user's private decryption key is generated by the VetKD protocol and encrypted using the user's transport key, making it inaccessible to the canister itself. The canister only sees the keys in encrypted form and forwards them to the authorized users. + +## Build and deploy from the command line + +### Prerequisites + +- Install [Node.js](https://nodejs.org/en/download/) +- Install [icp-cli](https://cli.internetcomputer.org): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Install [ic-mops](https://mops.one): `npm install -g ic-mops` + +### (Optionally) choose a different master key + +This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` before deploying. + +### Install + +```bash +git clone https://github.com/dfinity/examples +cd examples/motoko/vetkeys/basic_ibe +``` + +### Deploy + +```bash +icp network start -d +icp deploy +``` + +Open the frontend URL printed by `icp deploy`. + +To run the frontend in development mode with hot reloading (after `icp deploy`): + +```bash +npm run dev +``` + +When done, stop the local network to free up the port for other projects: + +```bash +icp network stop +``` + +## Example components + +### Backend (`backend/`) + +A single Motoko canister that: +- Stores encrypted messages between users. +- Lets users retrieve their personal encrypted messages. +- Lets users retrieve the decryption key for their messages, for later decryption in the user's browser. + +### Frontend (`frontend/`) + +A vanilla TypeScript application providing a simple interface for sending, receiving, and deleting encrypted messages. Canister bindings are generated from `backend/backend.did` at build time by the `@icp-sdk/bindgen` Vite plugin. + +## Updating the Candid interface + +`backend/backend.did` defines the backend's public interface; the frontend bindings are generated from it during the build. If you change the backend's public API, regenerate it: + +```bash +mops generate candid backend +``` + +## Limitations + +This example dapp does not implement key rotation, which is strongly recommended in a production dapp to limit the impact of a potential key compromise if a malicious party gains access to a user's decryption key. + +## Additional resources + +- **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview) diff --git a/rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo b/motoko/vetkeys/basic_ibe/backend/app.mo similarity index 63% rename from rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo rename to motoko/vetkeys/basic_ibe/backend/app.mo index 71f3bd6ba5..43d16ddb89 100644 --- a/rust/vetkeys/basic_ibe/motoko/backend/src/Main.mo +++ b/motoko/vetkeys/basic_ibe/backend/app.mo @@ -10,11 +10,11 @@ import Nat "mo:core/Nat"; import Result "mo:core/Result"; import Int "mo:core/Int"; -persistent actor class (keyNameString : Text) { +actor class (keyNameString : Text) { // Types type Message = { sender : Principal; - encrypted_message : Blob; + encryptedMessage : Blob; timestamp : Nat64; }; @@ -24,7 +24,7 @@ persistent actor class (keyNameString : Text) { type SendMessageRequest = { receiver : Principal; - encrypted_message : Blob; + encryptedMessage : Blob; }; type Result = { @@ -32,6 +32,8 @@ persistent actor class (keyNameString : Text) { #Err : E; }; + // vetKD management canister interface. These names are the fixed system API + // contract (snake_case), so they are kept as-is rather than camelCased. type VetKdKeyId = { curve : { #bls12_381_g2 }; name : Text; @@ -68,33 +70,33 @@ persistent actor class (keyNameString : Text) { let vetKdSystemApi : VetKdSystemApi = actor ("aaaaa-aa"); // Send a message to a receiver - public shared ({ caller }) func send_message(request : SendMessageRequest) : async Result<(), Text> { + public shared ({ caller }) func sendMessage(request : SendMessageRequest) : async Result<(), Text> { let message : Message = { sender = caller; - encrypted_message = request.encrypted_message; + encryptedMessage = request.encryptedMessage; timestamp = Nat64.fromNat(Int.abs(Time.now())); }; let receiver = request.receiver; - let current_inbox = switch (Map.get(inboxes, Principal.compare, receiver)) { + let currentInbox = switch (Map.get(inboxes, Principal.compare, receiver)) { case (?inbox) { inbox }; case null { { messages = [] } }; }; - if (current_inbox.messages.size() >= MAX_MESSAGES_PER_INBOX) { + if (currentInbox.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); + + let newMessages = Array.concat(currentInbox.messages, [message]); + let newInbox : Inbox = { messages = newMessages }; + ignore Map.insert(inboxes, Principal.compare, receiver, newInbox); #Ok(); }; // Get the IBE public key - public shared func get_ibe_public_key() : async Blob { - let key_id : VetKdKeyId = { + public shared func getIbePublicKey() : async Blob { + let keyId : VetKdKeyId = { curve = #bls12_381_g2; name = keyNameString; }; @@ -103,7 +105,7 @@ persistent actor class (keyNameString : Text) { let request : VetKdPublicKeyArgs = { canister_id = null; context = context; - key_id = key_id; + key_id = keyId; }; let result = await vetKdSystemApi.vetkd_public_key(request); @@ -111,8 +113,8 @@ persistent actor class (keyNameString : Text) { }; // 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 = { + public shared ({ caller }) func getMyEncryptedIbeKey(transportKey : Blob) : async Blob { + let keyId : VetKdKeyId = { curve = #bls12_381_g2; name = keyNameString; }; @@ -122,8 +124,8 @@ persistent actor class (keyNameString : Text) { let request : VetKdDeriveKeyArgs = { context = context; input = input; - key_id = key_id; - transport_public_key = transport_key; + key_id = keyId; + transport_public_key = transportKey; }; let result = await (with cycles = 26_153_846_153) vetKdSystemApi.vetkd_derive_key(request); @@ -131,7 +133,7 @@ persistent actor class (keyNameString : Text) { }; // Get the caller's messages - public shared query ({ caller }) func get_my_messages() : async Inbox { + public shared query ({ caller }) func getMyMessages() : async Inbox { switch (Map.get(inboxes, Principal.compare, caller)) { case (?inbox) { inbox }; case null { { messages = [] } }; @@ -139,30 +141,30 @@ persistent actor class (keyNameString : Text) { }; // 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)) { + public shared ({ caller }) func removeMyMessageByIndex(messageIndex : Nat64) : async Result<(), Text> { + let currentInbox = 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()) { + let index = Nat64.toNat(messageIndex); + if (index >= currentInbox.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(); + let messages = currentInbox.messages; + let newMessagesList = List.empty(); for (i in messages.keys()) { if (i != index) { - List.add(new_messages_list, messages[i]); + List.add(newMessagesList, 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); + let newMessages = List.toArray(newMessagesList); + let newInbox : Inbox = { messages = newMessages }; + ignore Map.insert(inboxes, Principal.compare, caller, newInbox); #Ok(); }; diff --git a/motoko/vetkeys/basic_ibe/backend/backend.did b/motoko/vetkeys/basic_ibe/backend/backend.did new file mode 100644 index 0000000000..07bd94baaf --- /dev/null +++ b/motoko/vetkeys/basic_ibe/backend/backend.did @@ -0,0 +1,26 @@ +type _anon_class_13_1 = + service { + getIbePublicKey: () -> (blob); + getMyEncryptedIbeKey: (transportKey: blob) -> (blob); + getMyMessages: () -> (Inbox) query; + removeMyMessageByIndex: (messageIndex: nat64) -> (Result); + sendMessage: (request: SendMessageRequest) -> (Result); + }; +type SendMessageRequest = + record { + encryptedMessage: blob; + receiver: principal; + }; +type Result = + variant { + Err: text; + Ok; + }; +type Message = + record { + encryptedMessage: blob; + sender: principal; + timestamp: nat64; + }; +type Inbox = record {messages: vec Message;}; +service : (keyNameString: text) -> _anon_class_13_1 diff --git a/motoko/vetkeys/basic_ibe/frontend/.gitignore b/motoko/vetkeys/basic_ibe/frontend/.gitignore new file mode 100644 index 0000000000..061e2e66e1 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +src/bindings/ diff --git a/motoko/vetkeys/basic_ibe/frontend/index.html b/motoko/vetkeys/basic_ibe/frontend/index.html new file mode 100644 index 0000000000..d8a01a7868 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + VetKeys: Basic IBE + + +
+ + + diff --git a/motoko/vetkeys/basic_ibe/frontend/package.json b/motoko/vetkeys/basic_ibe/frontend/package.json new file mode 100644 index 0000000000..0a68d864e4 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/package.json @@ -0,0 +1,20 @@ +{ + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "prebuild": "npm i --include=dev", + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "@icp-sdk/auth": "^7.1.0", + "@icp-sdk/core": "^5.4.0", + "@icp-sdk/vetkeys": "^0.5.0-beta.0" + }, + "devDependencies": { + "@icp-sdk/bindgen": "~0.2.2", + "typescript": "~5.7.2", + "vite": "^6.4.1" + } +} diff --git a/motoko/vetkeys/basic_ibe/frontend/public/.ic-assets.json5 b/motoko/vetkeys/basic_ibe/frontend/public/.ic-assets.json5 new file mode 100644 index 0000000000..2997d66d2b --- /dev/null +++ b/motoko/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/motoko/vetkeys/basic_ibe/frontend/public/vite.svg b/motoko/vetkeys/basic_ibe/frontend/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/motoko/vetkeys/basic_ibe/frontend/src/main.ts b/motoko/vetkeys/basic_ibe/frontend/src/main.ts new file mode 100644 index 0000000000..41100bd8ac --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/src/main.ts @@ -0,0 +1,368 @@ +import "./style.css"; +import { Principal } from "@icp-sdk/core/principal"; +import { + TransportSecretKey, + DerivedPublicKey, + EncryptedVetKey, + VetKey, + IbeCiphertext, + IbeIdentity, + IbeSeed, +} from "@icp-sdk/vetkeys"; +import { createActor, type Backend, type Inbox } from "./bindings/backend"; +import { AuthClient, LocalStorage } from "@icp-sdk/auth/client"; +import { HttpAgent } from "@icp-sdk/core/agent"; +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; + +const canisterEnv = safeGetCanisterEnv<{ + "PUBLIC_CANISTER_ID:backend": string; +}>(); + +let ibePrivateKey: VetKey | undefined = undefined; +let ibePublicKey: DerivedPublicKey | undefined = undefined; +let myPrincipal: Principal | undefined = undefined; +let authClient: AuthClient | undefined; +let basicIbeActor: Backend | undefined; + +async function getBasicIbeActor(): Promise { + if (basicIbeActor) return basicIbeActor; + const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; + if (!canisterId) { + throw Error("Canister ID for basic_ibe is not set"); + } + if (!authClient) { + throw Error("Auth client is not initialized"); + } + + const agent = await HttpAgent.create({ + identity: await authClient.getIdentity(), + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }); + basicIbeActor = createActor(canisterId, { agent }); + + return basicIbeActor; +} + +async function getIbePublicKey(): Promise { + if (ibePublicKey) return ibePublicKey; + const actor = await getBasicIbeActor(); + ibePublicKey = DerivedPublicKey.deserialize( + new Uint8Array(await actor.getIbePublicKey()), + ); + 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 actor = await getBasicIbeActor(); + const encryptedKey = Uint8Array.from( + await actor.getMyEncryptedIbeKey( + 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 actor = await getBasicIbeActor(); + const result = await actor.sendMessage({ + encryptedMessage: encryptedMessage, + receiver: receiverPrincipal, + }); + + if ("Err" in result) { + console.error("Error sending message:", result.Err); + alert("Error sending message: " + result.Err); + } else { + alert("Message sent successfully!"); + } + } catch (error) { + console.error("Error sending message:", error); + alert("Error sending message: " + (error as Error).message); + } +} + +async function showMessages() { + const actor = await getBasicIbeActor(); + const inbox = await actor.getMyMessages(); + 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.encryptedMessage), + ); + + 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 actor = await getBasicIbeActor(); + const result = await actor.removeMyMessageByIndex( + BigInt(index), + ); + if ("Err" in result) { + console.error("Error deleting message:", result.Err); + alert("Error deleting message: " + result.Err); + } else { + // Re-load all messages to refresh message indices + await showMessages(); + } + } catch (error) { + console.error("Error deleting message:", error); + alert( + "Error deleting message: " + (error as Error).message, + ); + } + })(); + }); + }); +} + +export async function login(client: AuthClient): Promise { + try { + const identity = await client.signIn({ + maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000), + }); + myPrincipal = identity.getPrincipal(); + updateUI(true); + } catch (error: unknown) { + console.error("Authentication failed:", error); + alert("Authentication failed: " + error); + } +} + +export function logout() { + void authClient?.signOut(); + const messagesDiv = document.getElementById("messages")!; + messagesDiv.innerHTML = ""; + ibePrivateKey = undefined; + myPrincipal = undefined; + basicIbeActor = undefined; + updateUI(false); +} + +async function initAuth() { + const isLocal = + window.location.hostname === "localhost" || + window.location.hostname.endsWith(".localhost"); + // Workaround for https://github.com/dfinity/icp-js-auth/issues/120 + // IdbStorage has a race condition on localhost dev servers. LocalStorage + // avoids IDB on local but uses plain string storage (less secure), so + // production deployments keep the default secure IdbStorage + ECDSA key. + authClient = new AuthClient({ + identityProvider: isLocal + ? "http://id.ai.localhost:8000/authorize" + : "https://id.ai/authorize", + ...(isLocal ? { storage: new LocalStorage(), keyType: "Ed25519" as const } : {}), + }); + const isAuthenticated = authClient.isAuthenticated(); + + if (isAuthenticated) { + myPrincipal = (await 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) { + console.error("Auth client not initialized"); + alert("Auth client not initialized"); + return; + } + + void 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 () => { + try { + await sendMessage(); + } catch (error: unknown) { + const msg = (error as Error).message ?? String(error); + console.error("Error in sendMessage:", error); + alert(msg); + } + })(); +}); +document.getElementById("showMessages")!.addEventListener("click", () => { + void (async () => { + try { + await showMessages(); + } catch (error: unknown) { + console.error("Error in showMessages:", error); + alert("Error loading messages: " + (error as Error).message); + } + })(); +}); + +// Initialize auth +void initAuth(); diff --git a/motoko/vetkeys/basic_ibe/frontend/src/style.css b/motoko/vetkeys/basic_ibe/frontend/src/style.css new file mode 100644 index 0000000000..cde19eb89c --- /dev/null +++ b/motoko/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/motoko/vetkeys/basic_ibe/frontend/src/vite-env.d.ts b/motoko/vetkeys/basic_ibe/frontend/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/motoko/vetkeys/basic_ibe/frontend/tsconfig.json b/motoko/vetkeys/basic_ibe/frontend/tsconfig.json new file mode 100644 index 0000000000..a4883f28e6 --- /dev/null +++ b/motoko/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/motoko/vetkeys/basic_ibe/frontend/vite.config.ts b/motoko/vetkeys/basic_ibe/frontend/vite.config.ts new file mode 100644 index 0000000000..cd9cb43bc1 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/frontend/vite.config.ts @@ -0,0 +1,49 @@ +import { defineConfig } from "vite"; +import { execSync } from "child_process"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; + +function getDevServerConfig() { + try { + const canisterId = execSync("icp canister status backend -e local -i", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + const networkStatus = JSON.parse( + execSync("icp network status --json", { + encoding: "utf-8", + stdio: "pipe", + }) + ); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }; + } catch {} + + throw new Error( + "No local network running. Start with:\n icp network start -d && icp deploy" + ); +} + +export default defineConfig(({ command }) => ({ + base: "./", + plugins: [ + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/bindings", + }), + ], + optimizeDeps: { + esbuildOptions: { define: { global: "globalThis" } }, + }, + build: { + sourcemap: true, + }, + server: command === "serve" ? getDevServerConfig() : undefined, +})); diff --git a/rust/vetkeys/basic_ibe/motoko/icp.yaml b/motoko/vetkeys/basic_ibe/icp.yaml similarity index 63% rename from rust/vetkeys/basic_ibe/motoko/icp.yaml rename to motoko/vetkeys/basic_ibe/icp.yaml index 425afe996a..ba5979ac3f 100644 --- a/rust/vetkeys/basic_ibe/motoko/icp.yaml +++ b/motoko/vetkeys/basic_ibe/icp.yaml @@ -1,18 +1,19 @@ canisters: - - name: basic_ibe + - name: backend recipe: type: "@dfinity/motoko@v5.0.0" init_args: type: text value: "(\"test_key_1\")" - - name: www + - name: frontend recipe: type: "@dfinity/asset-canister@v2.2.1" configuration: - dir: dist + dir: frontend/dist build: - - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./ + - npm install --prefix frontend + - npm run build --prefix frontend networks: - name: local diff --git a/motoko/vetkeys/basic_ibe/mops.toml b/motoko/vetkeys/basic_ibe/mops.toml new file mode 100644 index 0000000000..5dae136934 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/mops.toml @@ -0,0 +1,16 @@ +[toolchain] +moc = "1.11.0" + +[dependencies] +core = "2.5.0" +ic-vetkeys = "0.5.0" + +[moc] +# M0236: use context dot notation +# M0237: redundant explicit implicit arguments +# M0223: redundant type instantiation +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" +candid = "backend/backend.did" diff --git a/motoko/vetkeys/basic_ibe/package.json b/motoko/vetkeys/basic_ibe/package.json new file mode 100644 index 0000000000..b778b0a68d --- /dev/null +++ b/motoko/vetkeys/basic_ibe/package.json @@ -0,0 +1,13 @@ +{ + "name": "basic_ibe", + "private": true, + "type": "module", + "scripts": { + "build": "npm run build --workspaces --if-present", + "prebuild": "npm run prebuild --workspaces --if-present", + "dev": "npm run dev --workspaces --if-present" + }, + "workspaces": [ + "frontend" + ] +} diff --git a/motoko/vetkeys/basic_ibe/test.sh b/motoko/vetkeys/basic_ibe/test.sh new file mode 100755 index 0000000000..f2060d3803 --- /dev/null +++ b/motoko/vetkeys/basic_ibe/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +echo "--- Testing basic_ibe: IBE public key ---" +result=$(icp canister call backend getIbePublicKey '()') && \ + echo "$result" | head -1 && \ + echo "$result" | grep -q 'blob' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "--- Testing basic_ibe: empty inbox query ---" +result=$(icp canister call backend getMyMessages '()') && \ + echo "$result" && \ + echo "$result" | grep -q 'messages' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/vetkeys/basic_ibe/ui_screenshot.png b/motoko/vetkeys/basic_ibe/ui_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee53804b5e0d95172db7ce1da2fb76d6c48440f GIT binary patch literal 84227 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 +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/rust/vetkeys/basic_ibe) -The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. This application allows users to send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. This canister (IC smart contract) ensures that only the authorized user can access their private decryption key, meaning that even if someone else knows your principal, they cannot decrypt messages intended for you because neither other users nor this canister can access your private key. +Also available in: [Motoko](../../../motoko/vetkeys/basic_ibe) -Note that generally it is possible for a canister to request a decryption key to decrypt secrets as part of its code. -However, doing so requires the canister to provide its own transport key instead of requesting a user's transport key and this inherently makes secrets public. -A canister functionality for decrypting secrets can be detected by inspecting the code and, therefore, it is crucial that canisters using VetKeys have their code public to allow to verify that the canister handles secrets in a secure way. +The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. Users send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. The canister (IC smart contract) ensures that only the authorized user can access their private decryption key — even if someone else knows your principal, they cannot decrypt messages intended for you, because neither other users nor this canister can access your private key. + +Note that generally it is possible for a canister to request a decryption key to decrypt secrets as part of its code. However, doing so requires the canister to provide its own transport key instead of requesting a user's transport key, and this inherently makes the secrets public. Such functionality can be detected by inspecting the code, so it is crucial that canisters using VetKeys have their code public to allow verifying that the canister handles secrets in a secure way. ![UI Screenshot](ui_screenshot.png) ## Features -- **Secure Messaging**: Uses IBE capabilities of IC Vetkeys to encrypt messages that can only be decrypted by the intended recipient. +- **Secure Messaging**: Uses the IBE capabilities of IC VetKeys to encrypt messages that can only be decrypted by the intended recipient. - **Principal-Based Encryption**: Messages are encrypted using the recipient's principal as the public key identifier. - **Private Key Management**: Each user's private decryption key is generated by the VetKD protocol and encrypted using the user's transport key, making it inaccessible to the canister itself. The canister only sees the keys in encrypted form and forwards them to the authorized users. -## Setup +## Build and deploy from the command line ### Prerequisites -- [ICP CLI](https://cli.internetcomputer.org) -- [npm](https://www.npmjs.com/package/npm) - -### (Optionally) Choose a Different Master Key +- Install [Node.js](https://nodejs.org/en/download/) +- Install [icp-cli](https://cli.internetcomputer.org): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` +- Install the [Rust toolchain](https://www.rust-lang.org/tools/install), then add the WASM target: `rustup target add wasm32-unknown-unknown` -This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` to the desired key before running `icp deploy` in the next step. +### (Optionally) choose a different master key -### Folder Structure +This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` before deploying. -This example provides both a **Rust** and a **Motoko** backend, sharing a common `frontend/`: +### Install -``` -basic_ibe/ -├── frontend/ ← shared frontend (symlinked into rust/ and motoko/) -├── motoko/ ← Motoko backend + icp.yaml -└── rust/ ← Rust backend + icp.yaml +```bash +git clone https://github.com/dfinity/examples +cd examples/rust/vetkeys/basic_ibe ``` -### Deploy the Canisters Locally +### Deploy -Deploy with the **Motoko** backend: ```bash -cd motoko -icp network start -d && icp deploy +icp network start -d +icp deploy ``` -Or deploy with the **Rust** backend: -```bash -cd rust -icp network start -d && icp deploy -``` +Open the frontend URL printed by `icp deploy`. + +To run the frontend in development mode with hot reloading (after `icp deploy`): -To run the frontend in development mode with hot reloading (after running `icp deploy`): ```bash -cd frontend -npm run dev:motoko # if you deployed the Motoko backend -# or -npm run dev:rust # if you deployed the Rust backend +npm run dev ``` -When you are done testing, stop the local network to free up resources and unblock the default port for other projects: +When done, stop the local network to free up the port for other projects: + ```bash icp network stop ``` -## Example Components +## Example components + +### Backend (`backend/`) + +A single Rust canister that: +- Stores encrypted messages between users. +- Lets users retrieve their personal encrypted messages. +- Lets users retrieve the decryption key for their messages, for later decryption in the user's browser. -### Backend +### Frontend (`frontend/`) -The backend consists of a canister that: -* Stores encrypted messages between users. -* Allows users to retrieve their personal encrypted messages. -* Allows users to retrieve the decryption key for their messages for later decryption in user's browser. +A vanilla TypeScript application providing a simple interface for sending, receiving, and deleting encrypted messages. Canister bindings are generated from `backend/backend.did` at build time by the `@icp-sdk/bindgen` Vite plugin. -### Frontend +## Updating the Candid interface -The frontend is a vanilla typescript application providing a simple interface for sending, receiving, and deleting encrypted messages. +`backend/backend.did` defines the backend's public interface; the frontend bindings are generated from it during the build. If you change the backend's public API, regenerate it: + +```bash +cd backend && make extract-candid +``` ## Limitations -This example dapp does not implement key rotation, which is strongly recommended in a production dapp to limit the impact of potential key compromise if a malicious party gains access to the user's decryption key. +This example dapp does not implement key rotation, which is strongly recommended in a production dapp to limit the impact of a potential key compromise if a malicious party gains access to a user's decryption key. -## Additional Resources +## Additional resources -- **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** - For more information about VetKeys and VetKD. +- **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview) diff --git a/rust/vetkeys/basic_ibe/rust/backend/Cargo.toml b/rust/vetkeys/basic_ibe/backend/Cargo.toml similarity index 100% rename from rust/vetkeys/basic_ibe/rust/backend/Cargo.toml rename to rust/vetkeys/basic_ibe/backend/Cargo.toml diff --git a/rust/vetkeys/basic_ibe/rust/backend/Makefile b/rust/vetkeys/basic_ibe/backend/Makefile similarity index 100% rename from rust/vetkeys/basic_ibe/rust/backend/Makefile rename to rust/vetkeys/basic_ibe/backend/Makefile diff --git a/rust/vetkeys/basic_ibe/rust/backend/backend.did b/rust/vetkeys/basic_ibe/backend/backend.did similarity index 100% rename from rust/vetkeys/basic_ibe/rust/backend/backend.did rename to rust/vetkeys/basic_ibe/backend/backend.did diff --git a/rust/vetkeys/basic_ibe/rust/backend/src/lib.rs b/rust/vetkeys/basic_ibe/backend/src/lib.rs similarity index 100% rename from rust/vetkeys/basic_ibe/rust/backend/src/lib.rs rename to rust/vetkeys/basic_ibe/backend/src/lib.rs diff --git a/rust/vetkeys/basic_ibe/rust/backend/src/types.rs b/rust/vetkeys/basic_ibe/backend/src/types.rs similarity index 100% rename from rust/vetkeys/basic_ibe/rust/backend/src/types.rs rename to rust/vetkeys/basic_ibe/backend/src/types.rs diff --git a/rust/vetkeys/basic_ibe/frontend/.gitignore b/rust/vetkeys/basic_ibe/frontend/.gitignore new file mode 100644 index 0000000000..061e2e66e1 --- /dev/null +++ b/rust/vetkeys/basic_ibe/frontend/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +src/bindings/ diff --git a/rust/vetkeys/basic_ibe/frontend/.prettierrc b/rust/vetkeys/basic_ibe/frontend/.prettierrc deleted file mode 100644 index fa1d0d324a..0000000000 --- a/rust/vetkeys/basic_ibe/frontend/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "plugins": [], - "tabWidth": 4 -} diff --git a/rust/vetkeys/basic_ibe/frontend/eslint.config.mjs b/rust/vetkeys/basic_ibe/frontend/eslint.config.mjs deleted file mode 100644 index 9755154491..0000000000 --- a/rust/vetkeys/basic_ibe/frontend/eslint.config.mjs +++ /dev/null @@ -1,30 +0,0 @@ -// @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_ibe/frontend/package.json b/rust/vetkeys/basic_ibe/frontend/package.json index 3513a48db3..0a68d864e4 100644 --- a/rust/vetkeys/basic_ibe/frontend/package.json +++ b/rust/vetkeys/basic_ibe/frontend/package.json @@ -1,32 +1,20 @@ { - "name": "basic-ibe-frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "printf '\\nNo backend specified. Use one of:\\n\\n npm run dev:motoko\\n npm run dev:rust\\n\\n' && exit 1", - "dev:motoko": "npm run build:bindings && BACKEND=motoko vite", - "dev:rust": "npm run build:bindings && BACKEND=rust vite", - "build": "npm run build:bindings && 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" - }, - "dependencies": { - "@icp-sdk/auth": "^7.1.0", - "@icp-sdk/core": "^5.4.0", - "@icp-sdk/vetkeys": "^0.5.0-beta.0" - } + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "prebuild": "npm i --include=dev", + "build": "vite build", + "dev": "vite" + }, + "dependencies": { + "@icp-sdk/auth": "^7.1.0", + "@icp-sdk/core": "^5.4.0", + "@icp-sdk/vetkeys": "^0.5.0-beta.0" + }, + "devDependencies": { + "@icp-sdk/bindgen": "~0.2.2", + "typescript": "~5.7.2", + "vite": "^6.4.1" + } } diff --git a/rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh b/rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh deleted file mode 100755 index 6212f6296b..0000000000 --- a/rust/vetkeys/basic_ibe/frontend/scripts/gen_bindings.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -eu - -# Resolve the script's physical location so we work correctly even when the -# icp CLI has symlinked `frontend/` into a backend subdirectory for the build. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -FRONTEND_DIR="$(dirname "$SCRIPT_DIR")" -EXAMPLE_ROOT="$(dirname "$FRONTEND_DIR")" - -# Bindings are always generated from the Rust backend since both backends -# expose the same Candid interface. -if command -v candid-extractor >/dev/null 2>&1; then - (cd "$EXAMPLE_ROOT/rust/backend" && make extract-candid) -fi - -rm -rf "$FRONTEND_DIR/src/declarations/basic_ibe" -mkdir -p "$FRONTEND_DIR/src/declarations/basic_ibe" -npx --yes @icp-sdk/bindgen \ - --did-file "$EXAMPLE_ROOT/rust/backend/backend.did" \ - --out-dir "$FRONTEND_DIR/src/declarations/basic_ibe" \ - --declarations-flat --force diff --git a/rust/vetkeys/basic_ibe/frontend/src/main.ts b/rust/vetkeys/basic_ibe/frontend/src/main.ts index 70c8720da8..5bf70cff53 100644 --- a/rust/vetkeys/basic_ibe/frontend/src/main.ts +++ b/rust/vetkeys/basic_ibe/frontend/src/main.ts @@ -9,13 +9,13 @@ import { IbeIdentity, IbeSeed, } from "@icp-sdk/vetkeys"; -import { createActor, type Backend, type Inbox } from "./declarations/basic_ibe/backend"; +import { createActor, type Backend, type Inbox } from "./bindings/backend"; import { AuthClient, LocalStorage } from "@icp-sdk/auth/client"; import { HttpAgent } from "@icp-sdk/core/agent"; import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; const canisterEnv = safeGetCanisterEnv<{ - "PUBLIC_CANISTER_ID:basic_ibe": string; + "PUBLIC_CANISTER_ID:backend": string; }>(); let ibePrivateKey: VetKey | undefined = undefined; @@ -26,7 +26,7 @@ let basicIbeActor: Backend | undefined; async function getBasicIbeActor(): Promise { if (basicIbeActor) return basicIbeActor; - const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:basic_ibe"]; + const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { throw Error("Canister ID for basic_ibe is not set"); } diff --git a/rust/vetkeys/basic_ibe/frontend/vite.config.ts b/rust/vetkeys/basic_ibe/frontend/vite.config.ts index be565c1638..cd9cb43bc1 100644 --- a/rust/vetkeys/basic_ibe/frontend/vite.config.ts +++ b/rust/vetkeys/basic_ibe/frontend/vite.config.ts @@ -1,51 +1,49 @@ import { defineConfig } from "vite"; import { execSync } from "child_process"; - -const environment = process.env.ICP_ENVIRONMENT || "local"; -const CANISTER_NAMES = ["basic_ibe"]; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; function getDevServerConfig() { - const backend = process.env.BACKEND; - if (!backend) { - throw new Error( - "BACKEND env var is required. Use `npm run dev:motoko` or `npm run dev:rust`.", + try { + const canisterId = execSync("icp canister status backend -e local -i", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + const networkStatus = JSON.parse( + execSync("icp network status --json", { + encoding: "utf-8", + stdio: "pipe", + }) ); - } + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }; + } catch {} - const networkStatus = JSON.parse( - execSync(`icp network status -e ${environment} --json --project-root-override ../${backend}`, { - encoding: "utf-8", - }), + throw new Error( + "No local network running. Start with:\n icp network start -d && icp deploy" ); - const canisterParams = CANISTER_NAMES.map((name) => { - const id = execSync( - `icp canister status ${name} -e ${environment} --id-only --project-root-override ../${backend}`, - { encoding: "utf-8", stdio: "pipe" }, - ).trim(); - return `PUBLIC_CANISTER_ID:${name}=${id}`; - }).join("&"); - return { - headers: { - "Set-Cookie": `ic_env=${encodeURIComponent( - `${canisterParams}&ic_root_key=${networkStatus.root_key}`, - )}; SameSite=Lax;`, - }, - proxy: { - "/api": { target: networkStatus.api_url, changeOrigin: true }, - }, - hmr: false, - }; } export default defineConfig(({ command }) => ({ + base: "./", + plugins: [ + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/bindings", + }), + ], + optimizeDeps: { + esbuildOptions: { define: { global: "globalThis" } }, + }, build: { sourcemap: true, - rollupOptions: { - output: { - inlineDynamicImports: true, - }, - }, }, - root: "./", - ...(command === "serve" ? { server: getDevServerConfig() } : {}), + server: command === "serve" ? getDevServerConfig() : undefined, })); diff --git a/rust/vetkeys/basic_ibe/rust/icp.yaml b/rust/vetkeys/basic_ibe/icp.yaml similarity index 70% rename from rust/vetkeys/basic_ibe/rust/icp.yaml rename to rust/vetkeys/basic_ibe/icp.yaml index a0e57efd86..d51f46a3f1 100644 --- a/rust/vetkeys/basic_ibe/rust/icp.yaml +++ b/rust/vetkeys/basic_ibe/icp.yaml @@ -1,5 +1,5 @@ canisters: - - name: basic_ibe + - name: backend recipe: type: "@dfinity/rust@v3.2.0" configuration: @@ -9,13 +9,14 @@ canisters: type: text value: "(\"test_key_1\")" - - name: www + - name: frontend recipe: type: "@dfinity/asset-canister@v2.2.1" configuration: - dir: dist + dir: frontend/dist build: - - cd frontend && npm i --include=dev && npm run build && cd - && rm -rf dist; mv frontend/dist ./ + - npm install --prefix frontend + - npm run build --prefix frontend networks: - name: local diff --git a/rust/vetkeys/basic_ibe/motoko/frontend b/rust/vetkeys/basic_ibe/motoko/frontend deleted file mode 120000 index af288785f3..0000000000 --- a/rust/vetkeys/basic_ibe/motoko/frontend +++ /dev/null @@ -1 +0,0 @@ -../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 deleted file mode 100644 index c81b0cdc52..0000000000 --- a/rust/vetkeys/basic_ibe/motoko/mops.toml +++ /dev/null @@ -1,10 +0,0 @@ -[toolchain] -moc = "1.9.0" - -[dependencies] -core = "2.5.0" -ic-vetkeys = "0.5.0" -sha2 = "0.1.14" - -[canisters.basic_ibe] -main = "backend/src/Main.mo" diff --git a/rust/vetkeys/basic_ibe/package.json b/rust/vetkeys/basic_ibe/package.json new file mode 100644 index 0000000000..b778b0a68d --- /dev/null +++ b/rust/vetkeys/basic_ibe/package.json @@ -0,0 +1,13 @@ +{ + "name": "basic_ibe", + "private": true, + "type": "module", + "scripts": { + "build": "npm run build --workspaces --if-present", + "prebuild": "npm run prebuild --workspaces --if-present", + "dev": "npm run dev --workspaces --if-present" + }, + "workspaces": [ + "frontend" + ] +} diff --git a/rust/vetkeys/basic_ibe/rust/rust-toolchain.toml b/rust/vetkeys/basic_ibe/rust-toolchain.toml similarity index 100% rename from rust/vetkeys/basic_ibe/rust/rust-toolchain.toml rename to rust/vetkeys/basic_ibe/rust-toolchain.toml diff --git a/rust/vetkeys/basic_ibe/rust/frontend b/rust/vetkeys/basic_ibe/rust/frontend deleted file mode 120000 index af288785f3..0000000000 --- a/rust/vetkeys/basic_ibe/rust/frontend +++ /dev/null @@ -1 +0,0 @@ -../frontend \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/test.sh b/rust/vetkeys/basic_ibe/test.sh new file mode 100755 index 0000000000..898456a29f --- /dev/null +++ b/rust/vetkeys/basic_ibe/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -e + +echo "--- Testing basic_ibe: IBE public key ---" +result=$(icp canister call backend get_ibe_public_key '()') && \ + echo "$result" | head -1 && \ + echo "$result" | grep -q 'blob' && \ + echo "PASS" || (echo "FAIL" && exit 1) + +echo "--- Testing basic_ibe: empty inbox query ---" +result=$(icp canister call backend get_my_messages '()') && \ + echo "$result" && \ + echo "$result" | grep -q 'messages' && \ + echo "PASS" || (echo "FAIL" && exit 1) From ec51f1e7771fdbdcf33ed161e21e6fdaeda80aba Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Jul 2026 17:18:51 +0200 Subject: [PATCH 2/6] fix(vetkeys/basic_ibe): address Copilot review - main.ts: the "Canister ID not set" error referenced the old canister name (basic_ibe); update it to `backend` to match the renamed PUBLIC_CANISTER_ID:backend env key, so a missing env cookie is not confusing to debug. - test.sh: call the query method (getMyMessages / get_my_messages) with `--query` instead of as an update. Applies to both the Motoko and Rust variants. Co-Authored-By: Claude Opus 4.8 (1M context) --- motoko/vetkeys/basic_ibe/frontend/src/main.ts | 2 +- motoko/vetkeys/basic_ibe/test.sh | 2 +- rust/vetkeys/basic_ibe/frontend/src/main.ts | 2 +- rust/vetkeys/basic_ibe/test.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/motoko/vetkeys/basic_ibe/frontend/src/main.ts b/motoko/vetkeys/basic_ibe/frontend/src/main.ts index 41100bd8ac..0396ebcb82 100644 --- a/motoko/vetkeys/basic_ibe/frontend/src/main.ts +++ b/motoko/vetkeys/basic_ibe/frontend/src/main.ts @@ -28,7 +28,7 @@ async function getBasicIbeActor(): Promise { if (basicIbeActor) return basicIbeActor; const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { - throw Error("Canister ID for basic_ibe is not set"); + throw Error("Canister ID for backend is not set"); } if (!authClient) { throw Error("Auth client is not initialized"); diff --git a/motoko/vetkeys/basic_ibe/test.sh b/motoko/vetkeys/basic_ibe/test.sh index f2060d3803..6aa469a3a5 100755 --- a/motoko/vetkeys/basic_ibe/test.sh +++ b/motoko/vetkeys/basic_ibe/test.sh @@ -8,7 +8,7 @@ result=$(icp canister call backend getIbePublicKey '()') && \ echo "PASS" || (echo "FAIL" && exit 1) echo "--- Testing basic_ibe: empty inbox query ---" -result=$(icp canister call backend getMyMessages '()') && \ +result=$(icp canister call backend getMyMessages '()' --query) && \ echo "$result" && \ echo "$result" | grep -q 'messages' && \ echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/vetkeys/basic_ibe/frontend/src/main.ts b/rust/vetkeys/basic_ibe/frontend/src/main.ts index 5bf70cff53..f9c3231898 100644 --- a/rust/vetkeys/basic_ibe/frontend/src/main.ts +++ b/rust/vetkeys/basic_ibe/frontend/src/main.ts @@ -28,7 +28,7 @@ async function getBasicIbeActor(): Promise { if (basicIbeActor) return basicIbeActor; const canisterId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"]; if (!canisterId) { - throw Error("Canister ID for basic_ibe is not set"); + throw Error("Canister ID for backend is not set"); } if (!authClient) { throw Error("Auth client is not initialized"); diff --git a/rust/vetkeys/basic_ibe/test.sh b/rust/vetkeys/basic_ibe/test.sh index 898456a29f..a89ae8a613 100755 --- a/rust/vetkeys/basic_ibe/test.sh +++ b/rust/vetkeys/basic_ibe/test.sh @@ -8,7 +8,7 @@ result=$(icp canister call backend get_ibe_public_key '()') && \ echo "PASS" || (echo "FAIL" && exit 1) echo "--- Testing basic_ibe: empty inbox query ---" -result=$(icp canister call backend get_my_messages '()') && \ +result=$(icp canister call backend get_my_messages '()' --query) && \ echo "$result" && \ echo "$result" | grep -q 'messages' && \ echo "PASS" || (echo "FAIL" && exit 1) From 59f970c1a520e8fcdd970d8ae63d2c13fb294a94 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 10:13:19 +0200 Subject: [PATCH 3/6] chore(vetkeys/basic_ibe): rust v3.3.0, package `backend`, drop Makefile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the Rust recipe to @dfinity/rust@v3.3.0 and rename the Cargo package to `backend` (drops the custom `package:` in the recipe config). Drop backend/Makefile and advertise candid regeneration in the README via `icp build backend && candid-extractor … > backend/backend.did`. Verified: rust deploy + test.sh pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- rust/vetkeys/basic_ibe/README.md | 2 +- rust/vetkeys/basic_ibe/backend/Cargo.toml | 2 +- rust/vetkeys/basic_ibe/backend/Makefile | 15 --------------- rust/vetkeys/basic_ibe/icp.yaml | 3 +-- 4 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 rust/vetkeys/basic_ibe/backend/Makefile diff --git a/rust/vetkeys/basic_ibe/README.md b/rust/vetkeys/basic_ibe/README.md index 367c8adbe2..e9a164ec43 100644 --- a/rust/vetkeys/basic_ibe/README.md +++ b/rust/vetkeys/basic_ibe/README.md @@ -74,7 +74,7 @@ A vanilla TypeScript application providing a simple interface for sending, recei `backend/backend.did` defines the backend's public interface; the frontend bindings are generated from it during the build. If you change the backend's public API, regenerate it: ```bash -cd backend && make extract-candid +icp build backend && candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did ``` ## Limitations diff --git a/rust/vetkeys/basic_ibe/backend/Cargo.toml b/rust/vetkeys/basic_ibe/backend/Cargo.toml index 8d9be7a0f2..8acc73773e 100644 --- a/rust/vetkeys/basic_ibe/backend/Cargo.toml +++ b/rust/vetkeys/basic_ibe/backend/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ic-vetkd-example-basic-ibe-backend" +name = "backend" authors = ["DFINITY Stiftung"] version = "0.1.0" edition = "2021" diff --git a/rust/vetkeys/basic_ibe/backend/Makefile b/rust/vetkeys/basic_ibe/backend/Makefile deleted file mode 100644 index 766c349bf2..0000000000 --- a/rust/vetkeys/basic_ibe/backend/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -.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 ../.icp \ No newline at end of file diff --git a/rust/vetkeys/basic_ibe/icp.yaml b/rust/vetkeys/basic_ibe/icp.yaml index d51f46a3f1..3b7759d854 100644 --- a/rust/vetkeys/basic_ibe/icp.yaml +++ b/rust/vetkeys/basic_ibe/icp.yaml @@ -1,9 +1,8 @@ canisters: - name: backend recipe: - type: "@dfinity/rust@v3.2.0" + type: "@dfinity/rust@v3.3.0" configuration: - package: ic-vetkd-example-basic-ibe-backend candid: backend/backend.did init_args: type: text From ec00026fe037b62584080f682ac7d7035ee2ad4a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 10:46:21 +0200 Subject: [PATCH 4/6] chore(vetkeys/basic_ibe): drop test.sh, deploy-only CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example never had a test.sh on master; drop the ones added during the split and run deploy-only CI (the deploy verifies build + install). Persistence is unchanged — the `--default-persistent-actors` flag is kept (without it and without an explicit `persistent` keyword, moc 1.11.0 errors M0219; the flag is the forward-compatible choice and provably persists this main-canister actor class across upgrades). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/vetkeys-basic-ibe.yml | 6 ++---- motoko/vetkeys/basic_ibe/test.sh | 14 -------------- rust/vetkeys/basic_ibe/test.sh | 14 -------------- 3 files changed, 2 insertions(+), 32 deletions(-) delete mode 100755 motoko/vetkeys/basic_ibe/test.sh delete mode 100755 rust/vetkeys/basic_ibe/test.sh diff --git a/.github/workflows/vetkeys-basic-ibe.yml b/.github/workflows/vetkeys-basic-ibe.yml index 8f9d0cb593..6f93a75815 100644 --- a/.github/workflows/vetkeys-basic-ibe.yml +++ b/.github/workflows/vetkeys-basic-ibe.yml @@ -22,12 +22,11 @@ jobs: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy and test + - name: Deploy working-directory: motoko/vetkeys/basic_ibe run: | icp network start -d icp deploy - bash test.sh rust: runs-on: ubuntu-24.04 @@ -36,9 +35,8 @@ jobs: ICP_CLI_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Deploy and test + - name: Deploy working-directory: rust/vetkeys/basic_ibe run: | icp network start -d icp deploy - bash test.sh diff --git a/motoko/vetkeys/basic_ibe/test.sh b/motoko/vetkeys/basic_ibe/test.sh deleted file mode 100755 index 6aa469a3a5..0000000000 --- a/motoko/vetkeys/basic_ibe/test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "--- Testing basic_ibe: IBE public key ---" -result=$(icp canister call backend getIbePublicKey '()') && \ - echo "$result" | head -1 && \ - echo "$result" | grep -q 'blob' && \ - echo "PASS" || (echo "FAIL" && exit 1) - -echo "--- Testing basic_ibe: empty inbox query ---" -result=$(icp canister call backend getMyMessages '()' --query) && \ - echo "$result" && \ - echo "$result" | grep -q 'messages' && \ - echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/vetkeys/basic_ibe/test.sh b/rust/vetkeys/basic_ibe/test.sh deleted file mode 100755 index a89ae8a613..0000000000 --- a/rust/vetkeys/basic_ibe/test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "--- Testing basic_ibe: IBE public key ---" -result=$(icp canister call backend get_ibe_public_key '()') && \ - echo "$result" | head -1 && \ - echo "$result" | grep -q 'blob' && \ - echo "PASS" || (echo "FAIL" && exit 1) - -echo "--- Testing basic_ibe: empty inbox query ---" -result=$(icp canister call backend get_my_messages '()' --query) && \ - echo "$result" && \ - echo "$result" | grep -q 'messages' && \ - echo "PASS" || (echo "FAIL" && exit 1) From 770ae060d49976641ddfb659ff1bbb18baa203ef Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 12:04:40 +0200 Subject: [PATCH 5/6] docs(vetkeys/basic_ibe): drop "smart contract" web3 jargon "The canister (IC smart contract) ensures ..." -> "The canister ensures ...". Co-Authored-By: Claude Opus 4.8 (1M context) --- motoko/vetkeys/basic_ibe/README.md | 2 +- rust/vetkeys/basic_ibe/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/motoko/vetkeys/basic_ibe/README.md b/motoko/vetkeys/basic_ibe/README.md index 3fcbc31f56..1d48cd813b 100644 --- a/motoko/vetkeys/basic_ibe/README.md +++ b/motoko/vetkeys/basic_ibe/README.md @@ -4,7 +4,7 @@ Also available in: [Rust](../../../rust/vetkeys/basic_ibe) -The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. Users send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. The canister (IC smart contract) ensures that only the authorized user can access their private decryption key — even if someone else knows your principal, they cannot decrypt messages intended for you, because neither other users nor this canister can access your private key. +The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. Users send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. The canister ensures that only the authorized user can access their private decryption key — even if someone else knows your principal, they cannot decrypt messages intended for you, because neither other users nor this canister can access your private key. Note that generally it is possible for a canister to request a decryption key to decrypt secrets as part of its code. However, doing so requires the canister to provide its own transport key instead of requesting a user's transport key, and this inherently makes the secrets public. Such functionality can be detected by inspecting the code, so it is crucial that canisters using VetKeys have their code public to allow verifying that the canister handles secrets in a secure way. diff --git a/rust/vetkeys/basic_ibe/README.md b/rust/vetkeys/basic_ibe/README.md index e9a164ec43..5cc1b29048 100644 --- a/rust/vetkeys/basic_ibe/README.md +++ b/rust/vetkeys/basic_ibe/README.md @@ -4,7 +4,7 @@ Also available in: [Motoko](../../../motoko/vetkeys/basic_ibe) -The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. Users send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. The canister (IC smart contract) ensures that only the authorized user can access their private decryption key — even if someone else knows your principal, they cannot decrypt messages intended for you, because neither other users nor this canister can access your private key. +The **Basic IBE** example demonstrates how to use **[VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** to implement secure messaging between users by means of Identity-Based Encryption (IBE) on the **Internet Computer (IC)**. Users send encrypted messages to other users using their **Internet Identity Principal** as the encryption key identifier. The canister ensures that only the authorized user can access their private decryption key — even if someone else knows your principal, they cannot decrypt messages intended for you, because neither other users nor this canister can access your private key. Note that generally it is possible for a canister to request a decryption key to decrypt secrets as part of its code. However, doing so requires the canister to provide its own transport key instead of requesting a user's transport key, and this inherently makes the secrets public. Such functionality can be detected by inspecting the code, so it is crucial that canisters using VetKeys have their code public to allow verifying that the canister handles secrets in a secure way. From b5f4546daccf7eb5aa2a40c408c888dc6ec59cc3 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 17 Jul 2026 12:32:10 +0200 Subject: [PATCH 6/6] docs(vetkeys/basic_ibe): drop "dapp", fix security link Replace "dapp" with "app" and normalize the security best-practices link to https://docs.internetcomputer.org/guides/security/overview/. Co-Authored-By: Claude Opus 4.8 (1M context) --- motoko/vetkeys/basic_ibe/README.md | 4 ++-- rust/vetkeys/basic_ibe/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/motoko/vetkeys/basic_ibe/README.md b/motoko/vetkeys/basic_ibe/README.md index 1d48cd813b..73983979f4 100644 --- a/motoko/vetkeys/basic_ibe/README.md +++ b/motoko/vetkeys/basic_ibe/README.md @@ -79,9 +79,9 @@ mops generate candid backend ## Limitations -This example dapp does not implement key rotation, which is strongly recommended in a production dapp to limit the impact of a potential key compromise if a malicious party gains access to a user's decryption key. +This example app does not implement key rotation, which is strongly recommended in a production app to limit the impact of a potential key compromise if a malicious party gains access to a user's decryption key. ## Additional resources - **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. -- [Security best practices](https://docs.internetcomputer.org/guides/security/overview) +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview/) diff --git a/rust/vetkeys/basic_ibe/README.md b/rust/vetkeys/basic_ibe/README.md index 5cc1b29048..78b530bbb7 100644 --- a/rust/vetkeys/basic_ibe/README.md +++ b/rust/vetkeys/basic_ibe/README.md @@ -79,9 +79,9 @@ icp build backend && candid-extractor target/wasm32-unknown-unknown/release/back ## Limitations -This example dapp does not implement key rotation, which is strongly recommended in a production dapp to limit the impact of a potential key compromise if a malicious party gains access to a user's decryption key. +This example app does not implement key rotation, which is strongly recommended in a production app to limit the impact of a potential key compromise if a malicious party gains access to a user's decryption key. ## Additional resources - **[What are VetKeys](https://docs.internetcomputer.org/concepts/vetkeys)** — more information about VetKeys and VetKD. -- [Security best practices](https://docs.internetcomputer.org/guides/security/overview) +- [Security best practices](https://docs.internetcomputer.org/guides/security/overview/)