Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions .github/workflows/vetkeys-basic-ibe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- master
pull_request:
paths:
- motoko/vetkeys/basic_ibe/**
- rust/vetkeys/basic_ibe/**
- .github/workflows/vetkeys-basic-ibe.yml

Expand All @@ -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
87 changes: 87 additions & 0 deletions motoko/vetkeys/basic_ibe/README.md
Original file line number Diff line number Diff line change
@@ -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/)
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand All @@ -24,14 +24,16 @@ persistent actor class (keyNameString : Text) {

type SendMessageRequest = {
receiver : Principal;
encrypted_message : Blob;
encryptedMessage : Blob;
};

type Result<T, E> = {
#Ok : T;
#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;
Expand Down Expand Up @@ -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;
};
Expand All @@ -103,16 +105,16 @@ 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);
result.public_key;
};

// Get the caller's encrypted IBE key
public shared ({ caller }) func get_my_encrypted_ibe_key(transport_key : Blob) : async Blob {
let key_id : VetKdKeyId = {
public shared ({ caller }) func getMyEncryptedIbeKey(transportKey : Blob) : async Blob {
let keyId : VetKdKeyId = {
curve = #bls12_381_g2;
name = keyNameString;
};
Expand All @@ -122,47 +124,47 @@ 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);
result.encrypted_key;
};

// 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 = [] } };
};
};

// 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<Message>();
let messages = currentInbox.messages;
let newMessagesList = List.empty<Message>();

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();
};
Expand Down
26 changes: 26 additions & 0 deletions motoko/vetkeys/basic_ibe/backend/backend.did
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions motoko/vetkeys/basic_ibe/frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
src/bindings/
13 changes: 13 additions & 0 deletions motoko/vetkeys/basic_ibe/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VetKeys: Basic IBE</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions motoko/vetkeys/basic_ibe/frontend/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
10 changes: 10 additions & 0 deletions motoko/vetkeys/basic_ibe/frontend/public/.ic-assets.json5
Original file line number Diff line number Diff line change
@@ -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
},
]
1 change: 1 addition & 0 deletions motoko/vetkeys/basic_ibe/frontend/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading