diff --git a/.github/workflows/vetkeys-basic-ibe.yml b/.github/workflows/vetkeys-basic-ibe.yml index 132c81eb65..6f93a75815 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,28 @@ 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 + working-directory: motoko/vetkeys/basic_ibe + run: | + icp network start -d + icp deploy + + 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 + working-directory: rust/vetkeys/basic_ibe + run: | + icp network start -d + icp deploy diff --git a/motoko/vetkeys/basic_ibe/README.md b/motoko/vetkeys/basic_ibe/README.md new file mode 100644 index 0000000000..73983979f4 --- /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 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 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/) 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..0396ebcb82 --- /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 backend 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/ui_screenshot.png b/motoko/vetkeys/basic_ibe/ui_screenshot.png new file mode 100644 index 0000000000..2ee53804b5 Binary files /dev/null and b/motoko/vetkeys/basic_ibe/ui_screenshot.png differ diff --git a/rust/vetkeys/basic_ibe/rust/Cargo.toml b/rust/vetkeys/basic_ibe/Cargo.toml similarity index 100% rename from rust/vetkeys/basic_ibe/rust/Cargo.toml rename to rust/vetkeys/basic_ibe/Cargo.toml diff --git a/rust/vetkeys/basic_ibe/README.md b/rust/vetkeys/basic_ibe/README.md index 1a556253b6..78b530bbb7 100644 --- a/rust/vetkeys/basic_ibe/README.md +++ b/rust/vetkeys/basic_ibe/README.md @@ -1,91 +1,87 @@ -# Identity-Based Encryption +# Identity-Based Encryption (Rust) - +[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 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 +icp build backend && candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did +``` ## 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 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 +## 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 92% rename from rust/vetkeys/basic_ibe/rust/backend/Cargo.toml rename to rust/vetkeys/basic_ibe/backend/Cargo.toml index 8d9be7a0f2..8acc73773e 100644 --- a/rust/vetkeys/basic_ibe/rust/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/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..f9c3231898 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,9 +26,9 @@ 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"); + 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/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 54% rename from rust/vetkeys/basic_ibe/rust/icp.yaml rename to rust/vetkeys/basic_ibe/icp.yaml index a0e57efd86..3b7759d854 100644 --- a/rust/vetkeys/basic_ibe/rust/icp.yaml +++ b/rust/vetkeys/basic_ibe/icp.yaml @@ -1,21 +1,21 @@ canisters: - - name: basic_ibe + - 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 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/backend/Makefile b/rust/vetkeys/basic_ibe/rust/backend/Makefile deleted file mode 100644 index 766c349bf2..0000000000 --- a/rust/vetkeys/basic_ibe/rust/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/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