From bef84f9e4bee45f03fe5aeb3d55107590896ee2d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 28 Apr 2026 15:37:37 +0200 Subject: [PATCH 1/4] docs: add vetKeys concept and encryption guide Add docs/concepts/vetkeys.md explaining the vetKD protocol, the V/E/T/K properties, use cases, and API overview. Add docs/guides/security/encryption.mdx with a full how-to covering the 4-step key derivation flow, EncryptedMaps, and IBE patterns in Motoko/Rust/TypeScript. Closes #112, #113 --- docs/concepts/vetkeys.md | 107 +++++++- docs/guides/security/encryption.md | 23 -- docs/guides/security/encryption.mdx | 390 ++++++++++++++++++++++++++++ 3 files changed, 486 insertions(+), 34 deletions(-) delete mode 100644 docs/guides/security/encryption.md create mode 100644 docs/guides/security/encryption.mdx diff --git a/docs/concepts/vetkeys.md b/docs/concepts/vetkeys.md index 198c7717..3f4499eb 100644 --- a/docs/concepts/vetkeys.md +++ b/docs/concepts/vetkeys.md @@ -5,17 +5,102 @@ sidebar: order: 11 --- -TODO: Write content for this page. +VetKeys (verifiably encrypted threshold keys) give canisters the ability to derive secret key material on demand, without any node or canister ever seeing the raw key. The protocol that underpins this capability is called vetKD: verifiable encrypted threshold key derivation. - -Explain VetKeys (Verifiable Encrypted Threshold Key Derivation). Cover what VetKeys enables: onchain encryption where only authorized parties can decrypt, identity-based encryption (IBE), transport keys for secure communication, and key derivation for access control. Explain the current status (API available, production timeline) and how developers can start building with it. +The core problem vetKeys solve: encrypting data and storing it onchain is easy when the secret key stays on one device. The difficulty arises when a user needs to access that data from another device, share it with someone else, or let a canister participate in encryption workflows. Transmitting key material over public channels or storing it in a canister exposes it. VetKeys eliminate that exposure by making keys derivable from the network itself, encrypted for delivery, and verifiable by the recipient. - -- Portal: VetKeys sections -- icskills: vetkd -- Learn Hub: https://learn.internetcomputer.org (threshold key derivation) +## The vetKD properties - -- guides/security/data-integrity -- practical VetKeys usage -- concepts/chain-key-cryptography -- underlying cryptographic framework -- concepts/security -- VetKeys in the security model +The name describes how keys are derived: + +- **Verifiable.** Recipients can verify that the key they received is correct and has not been tampered with. No trust in any individual node is required. +- **Encrypted.** Each derived key is encrypted under a client-supplied transport public key before it leaves the subnet. No node or canister ever sees the raw derived key. +- **Threshold.** Key derivation requires a quorum of subnet nodes to cooperate. No single node can derive the key on its own. +- **Keys.** The output is raw cryptographic key material that can be used for symmetric encryption, identity-based encryption, BLS signatures, or further key derivation. + +## How the protocol works + +Every canister that uses vetKeys interacts with the subnet's threshold key derivation infrastructure through two management canister methods: `vetkd_public_key` and `vetkd_derive_key`. + +A key is derived from three inputs: + +- **Canister ID.** Keys are scoped per canister. A key derived by one canister cannot be used as a key derived by another. +- **Context.** A developer-chosen domain separator (for example, `b"my_app_v1"`). Context isolates subkeys per feature or use case within the same canister. +- **Input.** An application-defined identifier for the specific key (for example, a user's principal, a document ID, or a room ID). + +The derivation is **deterministic**: the same (canister, context, input) triple always produces the same key. Keys do not need to be stored anywhere; they can be retrieved on demand by any client that can authenticate to the canister. + +When a canister calls `vetkd_derive_key`: + +1. The canister passes the `input`, `context`, `transport_public_key`, and `key_id` to the management canister. +2. A threshold of subnet nodes cooperates to derive the key and encrypt it under the supplied transport public key. +3. The encrypted key is returned to the canister, which forwards it to the client. +4. The client decrypts the key using its transport secret key, obtaining the raw vetKey locally. + +The client's transport key pair is ephemeral: generated fresh for each session and discarded after use. No node, no subnet, and no canister ever holds the client's raw derived key. + +## API overview + +The vetKD API is exposed through two management canister methods: + +```candid +vetkd_public_key : (record { + canister_id : opt canister_id; + context : blob; + key_id : record { curve : vetkd_curve; name : text }; +}) -> (record { public_key : blob }); + +vetkd_derive_key : (record { + input : blob; + context : blob; + transport_public_key : blob; + key_id : record { curve : vetkd_curve; name : text }; +}) -> (record { encrypted_key : blob }); +``` + +The only supported curve is `bls12_381_g2`. Two key names are available: + +| Key name | Environment | Purpose | Cycle cost (approx.) | +|----------|-------------|---------|----------------------| +| `test_key_1` | Local + mainnet | Development and testing | 10,000,000,000 | +| `key_1` | Mainnet only | Production | 26,153,846,153 | + +`vetkd_public_key` carries no cycle cost. `vetkd_derive_key` consumes cycles at the rates above. If a canister may be blackholed or called by other canisters, send more cycles than the advertised cost: unused cycles are refunded, and this ensures calls succeed if the subnet grows in size. + +## Use cases + +### Encrypted onchain storage + +A canister derives a symmetric encryption key for each user or resource using a unique input (a principal or document ID). The client encrypts data with this key before storing it in the canister. Only the client, and anyone the canister grants access to, can later obtain the decryption key. The `EncryptedMaps` library in `ic-vetkeys` and `@dfinity/vetkeys` provides a ready-to-use implementation of this pattern. + +### Distributed key management (DKMS) + +Because key derivation is deterministic, a user can retrieve the same key from any device by authenticating to the canister. Canisters can grant access to other users by updating an access control list, enabling collaborative encrypted storage without peer-to-peer key exchange. + +### Identity-based encryption (IBE) + +Anyone can encrypt a message to a principal without the recipient being online or having pre-registered a key. The sender derives the recipient's public key from the canister's master public key and the recipient's principal. The recipient later authenticates to obtain their corresponding vetKey and decrypts. IBE is an asymmetric scheme: any party can encrypt to an identity, but only the holder of that identity can decrypt. + +### Timelock encryption + +A variant of IBE where the canister controls when a decryption key becomes available. A sender encrypts to a future timestamp or batch identifier; the canister releases the corresponding vetKey only after the specified time or condition is met. Applications include secret-bid auctions, delayed-reveal content, and protection against maximal extractable value (MEV) on decentralized exchanges. + +### Threshold BLS signatures + +VetKeys introduce threshold BLS signatures to canisters. BLS signatures are compact and support efficient aggregation, making them well-suited for multi-chain protocols and applications that need to verify many signatures efficiently. + +### Verifiable randomness + +VetKeys can function as a verifiable random function (VRF): each (canister, context, input) triple produces a unique, unpredictable value that anyone can verify was correctly derived. This is useful for lotteries, games, and NFT trait assignment where outcomes must be demonstrably fair. + +## Current status + +The vetKD management canister API is live on mainnet. The `ic-vetkeys` Rust crate (`v0.6`) and `@dfinity/vetkeys` npm package (`v0.4.0`) provide higher-level abstractions over the raw API. Pin your dependency versions and consult the [DFINITY forum](https://forum.dfinity.org/t/threshold-key-derivation-privacy-on-the-ic/16560) for any migration guides after upgrades. + +## Next steps + +- [Encryption with VetKeys](../guides/security/encryption.md): implement encrypted storage, IBE, and the full end-to-end key derivation flow +- [Chain-Key Cryptography](chain-key-cryptography.md): the threshold cryptographic foundation that vetKeys build on +- [Security](security.md): where vetKeys fit in the broader canister security model + + diff --git a/docs/guides/security/encryption.md b/docs/guides/security/encryption.md deleted file mode 100644 index fa34b18b..00000000 --- a/docs/guides/security/encryption.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: "Encryption with VetKeys" -description: "Encrypt and decrypt data on ICP using VetKeys for privacy, key management, and identity-based encryption" -sidebar: - order: 6 ---- - -TODO: Write content for this page. - - -How to encrypt data on ICP using VetKeys. Cover the end-to-end flow: setting up a canister with VetKeys, encrypting data client-side with a transport key, storing encrypted data onchain, and decrypting with derived keys. Include patterns for: encrypted onchain storage (e.g. encrypted-notes), distributed key management (DKMS), identity-based encryption (IBE), and timelock encryption. Show both backend (Rust/Motoko canister code) and frontend (JS decryption) sides. Reference the encrypted-notes and vetkd examples as real-world implementations. - - -- Portal: building-apps/network-features/vetkeys/ (9 files: intro, API, BLS-signatures, DKMS, encrypted-storage, IBE, timelock, VRF, demos) -- icskills: vetkd -- Examples: vetkd (both), vetkeys (both), encrypted-notes-dapp-vetkd (both), filevault (Motoko) -- Learn Hub: check for VetKeys articles - - -- concepts/vetkeys -- VetKeys conceptual background (what they are, how the protocol works) -- guides/security/data-integrity -- certified variables and signature verification -- guides/authentication/internet-identity -- identity-based patterns -- concepts/chain-key-cryptography -- threshold cryptography foundation diff --git a/docs/guides/security/encryption.mdx b/docs/guides/security/encryption.mdx new file mode 100644 index 00000000..e41b7115 --- /dev/null +++ b/docs/guides/security/encryption.mdx @@ -0,0 +1,390 @@ +--- +title: "Encryption with VetKeys" +description: "Encrypt and decrypt data on ICP using VetKeys for privacy, key management, and identity-based encryption" +sidebar: + order: 6 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +VetKeys enable canisters to derive cryptographic key material on demand so that clients can encrypt and decrypt data without the canister ever seeing the raw key. This guide covers the complete flow: exposing vetKD endpoints in a canister, generating a transport key pair on the frontend, and using the derived key for symmetric encryption. It also covers higher-level patterns: the `EncryptedMaps` abstraction for encrypted key-value storage, and identity-based encryption (IBE) for sending encrypted messages to a principal. + +For background on how the vetKD protocol works, see [VetKeys](../../concepts/vetkeys.md). + +## Prerequisites + + + + +Add the following to `Cargo.toml`: + +```toml +[dependencies] +candid = "0.10" +ic-cdk = "0.19" +ic-vetkeys = "0.6" +ic-stable-structures = "0.7" +serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +``` + + + + +Add `ic-vetkeys` to `mops.toml`: + +```toml +[package] +name = "my-vetkd-app" +version = "0.1.0" + +[dependencies] +core = "2.0.0" +``` + + + + +Frontend (TypeScript): + +```bash +npm install @dfinity/vetkeys@0.4.0 +``` + +## Step 1: Expose vetKD endpoints in the backend canister + +The backend canister wraps the management canister's `vetkd_derive_key` and `vetkd_public_key` methods and enforces access control. In this pattern, each caller gets a vetKey scoped to their principal, so only that caller can retrieve their key. + + + + +```motoko +import Blob "mo:core/Blob"; +import Principal "mo:core/Principal"; +import Text "mo:core/Text"; + +persistent actor { + + type VetKdCurve = { #bls12_381_g2 }; + + type VetKdKeyId = { + curve : VetKdCurve; + name : Text; + }; + + type VetKdPublicKeyRequest = { + canister_id : ?Principal; + context : Blob; + key_id : VetKdKeyId; + }; + + type VetKdPublicKeyResponse = { + public_key : Blob; + }; + + type VetKdDeriveKeyRequest = { + input : Blob; + context : Blob; + transport_public_key : Blob; + key_id : VetKdKeyId; + }; + + type VetKdDeriveKeyResponse = { + encrypted_key : Blob; + }; + + let managementCanister : actor { + vetkd_public_key : VetKdPublicKeyRequest -> async VetKdPublicKeyResponse; + vetkd_derive_key : VetKdDeriveKeyRequest -> async VetKdDeriveKeyResponse; + } = actor "aaaaa-aa"; + + let context : Blob = Text.encodeUtf8("my_app_v1"); + + func keyId() : VetKdKeyId { + { curve = #bls12_381_g2; name = "test_key_1" } + // Use "key_1" for production + }; + + public shared func getPublicKey() : async Blob { + let response = await managementCanister.vetkd_public_key({ + canister_id = null; + context; + key_id = keyId(); + }); + response.public_key + }; + + public shared ({ caller }) func getEncryptedVetKey( + input : Blob, + transportPublicKey : Blob, + ) : async Blob { + // caller is captured before the await + // test_key_1 costs ~10B cycles; key_1 costs ~26B cycles + let response = await (with cycles = 10_000_000_000) managementCanister.vetkd_derive_key({ + input; + context; + transport_public_key = transportPublicKey; + key_id = keyId(); + }); + response.encrypted_key + }; +}; +``` + + + + +```rust +use candid::Principal; +use ic_cdk::update; + +const CONTEXT: &[u8] = b"my_app_v1"; + +fn key_id() -> ic_cdk::management_canister::VetKDKeyId { + ic_cdk::management_canister::VetKDKeyId { + curve: ic_cdk::management_canister::VetKDCurve::Bls12_381_G2, + name: "test_key_1".to_string(), // Use "key_1" for production + } +} + +#[update] +async fn get_public_key() -> Vec { + let request = ic_cdk::management_canister::VetKDPublicKeyArgs { + canister_id: None, + context: CONTEXT.to_vec(), + key_id: key_id(), + }; + let reply = ic_cdk::management_canister::vetkd_public_key(&request) + .await + .expect("vetkd_public_key call failed"); + reply.public_key +} + +#[update] +async fn get_encrypted_vetkey(input: Vec, transport_public_key: Vec) -> Vec { + let caller = ic_cdk::caller(); // capture before await + + // Use input as-is (caller's principal, document ID, etc.) + // test_key_1 costs ~10B cycles; key_1 costs ~26B cycles + let request = ic_cdk::management_canister::VetKDDeriveKeyArgs { + input, + context: CONTEXT.to_vec(), + transport_public_key, + key_id: key_id(), + }; + let reply = ic_cdk::management_canister::vetkd_derive_key(&request) + .await + .expect("vetkd_derive_key call failed"); + reply.encrypted_key +} + +ic_cdk::export_candid!(); +``` + + + + +**Key decisions:** + +- **Context** (`my_app_v1`): a domain separator that scopes all keys to this application. Change it if you need to isolate keys between features of the same canister. +- **Input**: the identifier for the specific key (a principal, a document ID, a room ID). Different inputs yield different keys; the same input always yields the same key. +- **Caller capture before `await`**: always read `caller` before any `await` in an update call. + +## Step 2: Generate a transport key pair on the frontend + +The transport key pair is ephemeral. Generate it fresh for each session or each key request. + +```typescript +import { TransportSecretKey } from "@dfinity/vetkeys"; + +const seed = crypto.getRandomValues(new Uint8Array(32)); +const transportSecretKey = TransportSecretKey.fromSeed(seed); +const transportPublicKey = transportSecretKey.publicKey(); +``` + +Pass `transportPublicKey` to the canister when requesting a derived key. + +## Step 3: Retrieve and decrypt the vetKey + +```typescript +import { + TransportSecretKey, + DerivedPublicKey, + EncryptedVetKey, +} from "@dfinity/vetkeys"; + +// Use the caller's principal bytes (or another unique identifier) as the input +const input = new Uint8Array(0); // simplest possible input; use a real ID in practice + +const [encryptedKeyBytes, verificationKeyBytes] = await Promise.all([ + backendActor.get_encrypted_vetkey(input, transportPublicKey), + backendActor.get_public_key(), +]); + +const verificationKey = DerivedPublicKey.deserialize( + new Uint8Array(verificationKeyBytes), +); +const encryptedVetKey = EncryptedVetKey.deserialize( + new Uint8Array(encryptedKeyBytes), +); + +// Verify and decrypt: throws if the key is malformed or was tampered with +const vetKey = encryptedVetKey.decryptAndVerify( + transportSecretKey, + verificationKey, + input, +); +``` + +## Step 4: Derive a symmetric key and encrypt data + +The raw vetKey is not used directly as an AES key. Use `toDerivedKeyMaterial()` to derive a symmetric key from it. + +```typescript +// Derive a 256-bit AES-GCM key +const aesKeyMaterial = vetKey.toDerivedKeyMaterial(); +const aesKey = await crypto.subtle.importKey( + "raw", + aesKeyMaterial.data.slice(0, 32), + { name: "AES-GCM" }, + false, + ["encrypt", "decrypt"], +); + +// Encrypt +const iv = crypto.getRandomValues(new Uint8Array(12)); +const ciphertext = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + aesKey, + new TextEncoder().encode("secret message"), +); + +// Store ciphertext (and iv) in the canister; never store the key + +// Decrypt +const plaintext = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + aesKey, + ciphertext, +); +``` + +Store only the ciphertext and IV in the canister; the raw key exists only in the client's memory for the duration of the session. + +## Using EncryptedMaps for encrypted key-value storage + +`EncryptedMaps` is a higher-level abstraction that combines `KeyManager` (access-controlled vetKey derivation) with encrypted storage. It manages key derivation, access control, and client-side encryption transparently. Each named map is secured with a single vetKey; all key-value pairs in the map share the same access permissions. + + + + +```typescript +import { EncryptedMaps } from "@dfinity/vetkeys/encrypted_maps"; + +// encryptedMapsClientInstance connects to your backend canister +const encryptedMaps = new EncryptedMaps(encryptedMapsClientInstance); + +const mapOwner = Principal.fromText("aaaaa-aa"); +const mapName = "passwords"; +const mapKey = "email_account"; + +// Store an encrypted value (encryption is automatic) +const value = new TextEncoder().encode("my_secure_password"); +await encryptedMaps.setValue(mapOwner, mapName, mapKey, value); + +// Retrieve and decrypt a stored value +const stored = await encryptedMaps.getValue(mapOwner, mapName, mapKey); + +// Grant another user read-write access to the map +const user = Principal.fromText("bbbbbb-bb"); +await encryptedMaps.setUserRights(mapOwner, mapName, user, { ReadWrite: null }); +``` + + + + +The backend `EncryptedMaps` component stores only ciphertext; all plaintext stays on the frontend. See the [password manager example](https://github.com/dfinity/vetkeys/tree/main/examples/password_manager) for a full implementation. + +For the Rust backend, `EncryptedMaps` lives in `ic_vetkeys::encrypted_maps`; for TypeScript, import from `@dfinity/vetkeys/encrypted_maps`. + +## Identity-based encryption (IBE) + +IBE lets anyone encrypt a message to a principal using only the canister's public key. The recipient authenticates to the canister, obtains their corresponding vetKey, and decrypts. No prior key exchange is needed and the sender does not need the recipient to be online. + +**Encrypt (sender, no canister call needed):** + +```typescript +import { + IbeCiphertext, + IbeIdentity, + IbeSeed, + DerivedPublicKey, +} from "@dfinity/vetkeys"; + +// Derive the canister's IBE public key (fetch once, cache) +const publicKeyBytes = await backendActor.get_public_key(); +const ibePublicKey = DerivedPublicKey.deserialize(new Uint8Array(publicKeyBytes)); + +// Encrypt to the recipient's principal +const recipientIdentity = IbeIdentity.fromBytes(recipientPrincipalBytes); +const seed = IbeSeed.random(); +const plaintext = new TextEncoder().encode("secret message"); + +const ciphertext = IbeCiphertext.encrypt( + ibePublicKey, + recipientIdentity, + plaintext, + seed, +); +const serialized = ciphertext.serialize(); // store in the canister or transmit +``` + +**Decrypt (recipient, after obtaining vetKey):** + +```typescript +import { IbeCiphertext } from "@dfinity/vetkeys"; + +// Obtain the vetKey for the recipient's principal (steps 2-3 above) +const vetKey = /* ... decryptAndVerify as shown in Step 3 ... */; + +const deserialized = IbeCiphertext.deserialize(serialized); +const decrypted = deserialized.decrypt(vetKey); +// decrypted is Uint8Array of the plaintext +``` + +See the [basic IBE example](https://github.com/dfinity/vetkeys/tree/main/examples/basic_ibe) for a complete Motoko and Rust backend implementation. + +## Testing locally + +Start the local network and deploy: + +```bash +icp network start -d +icp deploy backend +``` + +The local network automatically provisions both `test_key_1` and `key_1`. Verify that your canister returns a public key: + +```bash +icp canister call backend get_public_key '()' +# Returns: (blob "...") -- 48+ bytes of BLS public key data +``` + +For `vetkd_derive_key` testing, use the [chain-key testing canister](https://github.com/dfinity/chainkey-testing-canister) (`vrqyr-saaaa-aaaan-qzn4q-cai`) on mainnet as a lower-cost alternative during development. It provides a fake vetKD implementation with no threshold. Use key name `insecure_test_key_1`. Never use it with real data or in production. + +## Common mistakes + +- **Reusing transport keys across sessions.** Each session must generate a fresh transport key pair. +- **Using the raw vetKey as an AES key.** Always call `toDerivedKeyMaterial()` first; do not pass the raw bytes to `importKey`. +- **Putting secret data in the `input` field.** The `input` is sent to the management canister in plaintext. Use it as an identifier (principal, document ID), not for the secret data itself. +- **Mismatched `context` between frontend and backend.** If the canister uses `b"my_app_v1"` but the frontend verification uses a different value, decryption will fail silently. +- **Not attaching enough cycles to `vetkd_derive_key`.** `test_key_1` costs approximately 10 billion cycles; `key_1` costs approximately 26 billion cycles. + +## Next steps + +- [VetKeys concept](../../concepts/vetkeys.md): how the vetKD protocol works and what use cases it enables +- [Data integrity](data-integrity.md): certified variables and response verification +- [Internet Identity](../authentication/internet-identity.md): authenticate users before granting access to vetKeys +- [vetkeys repository](https://github.com/dfinity/vetkeys): full examples including password manager, IBE messaging, and secret-bid auction + +{/* Upstream: informed by dfinity/portal docs/building-apps/network-features/vetkeys/ (introduction.mdx, api.mdx, encrypted-onchain-storage.mdx, identity-based-encryption.mdx, dkms.mdx, timelock-encryption.mdx); dfinity/icskills vetkd */} From 0a5b64a9ddf17fb5562dfde58f75a6cb9ad66b1a Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 28 Apr 2026 15:51:18 +0200 Subject: [PATCH 2/4] chore(examples): bump to d4ea422; update vetkeys example links Bump dfinity/examples submodule from 954d208 to d4ea422. The vetkeys examples (password_manager, encrypted_notes_dapp_vetkd, basic_ibe, basic_timelock_ibe, encrypted_chat, basic_bls_signing, password_manager_with_metadata) are now canonical under rust/vetkeys/ in this repo. Update all docs links that pointed to github.com/dfinity/vetkeys/tree/main/examples/... to their new paths at github.com/dfinity/examples/tree/master/rust/vetkeys/... Also add links to new examples (password_manager_with_metadata, basic_timelock_ibe) in the encryption guide, and separate the library and examples links in Next steps. --- .sources/examples | 2 +- docs/guides/security/data-integrity.md | 10 +++++----- docs/guides/security/encryption.mdx | 9 +++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.sources/examples b/.sources/examples index 954d2080..d4ea4220 160000 --- a/.sources/examples +++ b/.sources/examples @@ -1 +1 @@ -Subproject commit 954d2080d5c2fec71772676b73e41c3211487d49 +Subproject commit d4ea4220c26b46e676721145d21ca21c8d7dfaa6 diff --git a/docs/guides/security/data-integrity.md b/docs/guides/security/data-integrity.md index 3a3c988f..ad7d37db 100644 --- a/docs/guides/security/data-integrity.md +++ b/docs/guides/security/data-integrity.md @@ -345,9 +345,9 @@ const derivedKey: DerivedPublicKey = canisterKey.deriveSubKey( ``` For complete IBE and encrypted storage examples, see: -- [Password manager example](https://github.com/dfinity/vetkeys/tree/main/examples/password_manager): encrypted key-value storage with `EncryptedMaps` -- [Encrypted notes app](https://github.com/dfinity/vetkeys/tree/main/examples/encrypted_notes_dapp_vetkd): per-user encrypted note storage -- [IBE example](https://github.com/dfinity/vetkeys/tree/main/examples/basic_ibe): identity-based encryption with Internet Identity principals +- [Password manager](https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager): encrypted key-value storage with `EncryptedMaps` +- [Encrypted notes app](https://github.com/dfinity/examples/tree/master/rust/vetkeys/encrypted_notes_dapp_vetkd): per-user encrypted note storage +- [IBE example](https://github.com/dfinity/examples/tree/master/rust/vetkeys/basic_ibe): identity-based encryption with Internet Identity principals ## Certified variables for data authenticity @@ -448,8 +448,8 @@ Confirm that: ## Next steps - [vetKeys concept guide](../../concepts/vetkeys.md): how the threshold key derivation protocol works -- [Encryption guide](./encryption.md): vetKeys encryption patterns including EncryptedMaps (coming soon) +- [Encryption guide](./encryption.md): vetKeys encryption patterns including EncryptedMaps - [Certified variables](../backends/certified-variables.md): full certified data implementation - [Security model](../../concepts/security.md): IC security guarantees and threat model - + diff --git a/docs/guides/security/encryption.mdx b/docs/guides/security/encryption.mdx index e41b7115..952c6ee6 100644 --- a/docs/guides/security/encryption.mdx +++ b/docs/guides/security/encryption.mdx @@ -303,7 +303,7 @@ await encryptedMaps.setUserRights(mapOwner, mapName, user, { ReadWrite: null }); -The backend `EncryptedMaps` component stores only ciphertext; all plaintext stays on the frontend. See the [password manager example](https://github.com/dfinity/vetkeys/tree/main/examples/password_manager) for a full implementation. +The backend `EncryptedMaps` component stores only ciphertext; all plaintext stays on the frontend. See the [password manager example](https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager) (Motoko + Rust) for a full implementation, or the [password manager with metadata](https://github.com/dfinity/examples/tree/master/rust/vetkeys/password_manager_with_metadata) variant that adds unencrypted metadata alongside encrypted values. For the Rust backend, `EncryptedMaps` lives in `ic_vetkeys::encrypted_maps`; for TypeScript, import from `@dfinity/vetkeys/encrypted_maps`. @@ -352,7 +352,7 @@ const decrypted = deserialized.decrypt(vetKey); // decrypted is Uint8Array of the plaintext ``` -See the [basic IBE example](https://github.com/dfinity/vetkeys/tree/main/examples/basic_ibe) for a complete Motoko and Rust backend implementation. +See the [basic IBE example](https://github.com/dfinity/examples/tree/master/rust/vetkeys/basic_ibe) (Motoko + Rust) for a complete backend and frontend implementation. For IBE with a time-based release condition (timelock encryption), see the [secret-bid auction example](https://github.com/dfinity/examples/tree/master/rust/vetkeys/basic_timelock_ibe). ## Testing locally @@ -385,6 +385,7 @@ For `vetkd_derive_key` testing, use the [chain-key testing canister](https://git - [VetKeys concept](../../concepts/vetkeys.md): how the vetKD protocol works and what use cases it enables - [Data integrity](data-integrity.md): certified variables and response verification - [Internet Identity](../authentication/internet-identity.md): authenticate users before granting access to vetKeys -- [vetkeys repository](https://github.com/dfinity/vetkeys): full examples including password manager, IBE messaging, and secret-bid auction +- [vetkeys examples](https://github.com/dfinity/examples/tree/master/rust/vetkeys): password manager, encrypted notes, IBE messaging, BLS signing, and secret-bid auction +- [ic-vetkeys library](https://github.com/dfinity/vetkeys): Rust crate and TypeScript package source -{/* Upstream: informed by dfinity/portal docs/building-apps/network-features/vetkeys/ (introduction.mdx, api.mdx, encrypted-onchain-storage.mdx, identity-based-encryption.mdx, dkms.mdx, timelock-encryption.mdx); dfinity/icskills vetkd */} +{/* Upstream: informed by dfinity/portal docs/building-apps/network-features/vetkeys/ (introduction.mdx, api.mdx, encrypted-onchain-storage.mdx, identity-based-encryption.mdx, dkms.mdx, timelock-encryption.mdx); dfinity/icskills vetkd; dfinity/examples rust/vetkeys/password_manager, rust/vetkeys/password_manager_with_metadata, rust/vetkeys/basic_ibe, rust/vetkeys/basic_timelock_ibe, rust/vetkeys/encrypted_notes_dapp_vetkd */} From 6ea007b2d8cb2290ac1dfa3bb25cce507cf08c2b Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 28 Apr 2026 16:14:55 +0200 Subject: [PATCH 3/4] fix(encryption): scope context to caller, fix transport key bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rust: replace unused `use candid::Principal` import with inline `candid::Principal` usage; add `caller_context()` that prefixes the domain separator with the caller's principal bytes; use it in both `get_public_key` and `get_encrypted_vetkey` so each caller's keys are cryptographically isolated - Motoko: same pattern via `callerContext()` using mo:core Array/Blob - Frontend Step 2: replace `TransportSecretKey.fromSeed` + `.publicKey()` with `.random()` + `.publicKeyBytes()` — `.publicKeyBytes()` returns the Uint8Array the canister endpoint expects; `.publicKey()` returns a DerivedPublicKey object and would produce incorrect Candid encoding - Update surrounding prose and Common mistakes to match --- docs/guides/security/encryption.mdx | 59 +++++++++++++++++++---------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/docs/guides/security/encryption.mdx b/docs/guides/security/encryption.mdx index 952c6ee6..3480abbd 100644 --- a/docs/guides/security/encryption.mdx +++ b/docs/guides/security/encryption.mdx @@ -53,13 +53,15 @@ npm install @dfinity/vetkeys@0.4.0 ## Step 1: Expose vetKD endpoints in the backend canister -The backend canister wraps the management canister's `vetkd_derive_key` and `vetkd_public_key` methods and enforces access control. In this pattern, each caller gets a vetKey scoped to their principal, so only that caller can retrieve their key. +The backend canister wraps the management canister's `vetkd_derive_key` and `vetkd_public_key` methods and enforces per-caller key isolation. The context passed to both API methods encodes the domain separator and the caller's principal, so each caller's keys are cryptographically separate and only that caller can retrieve them. ```motoko +import Array "mo:core/Array"; import Blob "mo:core/Blob"; +import Nat8 "mo:core/Nat8"; import Principal "mo:core/Principal"; import Text "mo:core/Text"; @@ -98,17 +100,28 @@ persistent actor { vetkd_derive_key : VetKdDeriveKeyRequest -> async VetKdDeriveKeyResponse; } = actor "aaaaa-aa"; - let context : Blob = Text.encodeUtf8("my_app_v1"); + let domainSeparator : [Nat8] = Blob.toArray(Text.encodeUtf8("my_app_v1")); + + // Encodes domain separator + caller principal so each caller's keys are isolated. + func callerContext(caller : Principal) : Blob { + Blob.fromArray( + Array.flatten([ + [Nat8.fromNat(domainSeparator.size())], + domainSeparator, + Blob.toArray(Principal.toBlob(caller)), + ]) + ) + }; func keyId() : VetKdKeyId { { curve = #bls12_381_g2; name = "test_key_1" } // Use "key_1" for production }; - public shared func getPublicKey() : async Blob { + public shared ({ caller }) func getPublicKey() : async Blob { let response = await managementCanister.vetkd_public_key({ canister_id = null; - context; + context = callerContext(caller); key_id = keyId(); }); response.public_key @@ -118,11 +131,10 @@ persistent actor { input : Blob, transportPublicKey : Blob, ) : async Blob { - // caller is captured before the await // test_key_1 costs ~10B cycles; key_1 costs ~26B cycles let response = await (with cycles = 10_000_000_000) managementCanister.vetkd_derive_key({ input; - context; + context = callerContext(caller); transport_public_key = transportPublicKey; key_id = keyId(); }); @@ -135,10 +147,18 @@ persistent actor { ```rust -use candid::Principal; use ic_cdk::update; -const CONTEXT: &[u8] = b"my_app_v1"; +const DOMAIN_SEPARATOR: &[u8] = b"my_app_v1"; + +/// Encodes domain separator + caller principal so each caller's keys are isolated. +fn caller_context(caller: candid::Principal) -> Vec { + [DOMAIN_SEPARATOR.len() as u8] + .into_iter() + .chain(DOMAIN_SEPARATOR.iter().copied()) + .chain(caller.as_slice().iter().copied()) + .collect() +} fn key_id() -> ic_cdk::management_canister::VetKDKeyId { ic_cdk::management_canister::VetKDKeyId { @@ -149,9 +169,10 @@ fn key_id() -> ic_cdk::management_canister::VetKDKeyId { #[update] async fn get_public_key() -> Vec { + let caller = ic_cdk::caller(); let request = ic_cdk::management_canister::VetKDPublicKeyArgs { canister_id: None, - context: CONTEXT.to_vec(), + context: caller_context(caller), key_id: key_id(), }; let reply = ic_cdk::management_canister::vetkd_public_key(&request) @@ -163,12 +184,10 @@ async fn get_public_key() -> Vec { #[update] async fn get_encrypted_vetkey(input: Vec, transport_public_key: Vec) -> Vec { let caller = ic_cdk::caller(); // capture before await - - // Use input as-is (caller's principal, document ID, etc.) // test_key_1 costs ~10B cycles; key_1 costs ~26B cycles let request = ic_cdk::management_canister::VetKDDeriveKeyArgs { input, - context: CONTEXT.to_vec(), + context: caller_context(caller), transport_public_key, key_id: key_id(), }; @@ -186,8 +205,8 @@ ic_cdk::export_candid!(); **Key decisions:** -- **Context** (`my_app_v1`): a domain separator that scopes all keys to this application. Change it if you need to isolate keys between features of the same canister. -- **Input**: the identifier for the specific key (a principal, a document ID, a room ID). Different inputs yield different keys; the same input always yields the same key. +- **Context**: encodes the domain separator (`my_app_v1`) plus the caller's principal. This makes every caller's keys cryptographically separate; a key derived for one principal cannot be decrypted by another. Both `getPublicKey` and `getEncryptedVetKey` must use the same context so that `decryptAndVerify` succeeds on the frontend. +- **Input**: an additional identifier within the caller's key space (a document ID, a room ID, or an empty vector for a single per-user key). Different inputs yield different keys; the same input always yields the same key. - **Caller capture before `await`**: always read `caller` before any `await` in an update call. ## Step 2: Generate a transport key pair on the frontend @@ -197,9 +216,8 @@ The transport key pair is ephemeral. Generate it fresh for each session or each ```typescript import { TransportSecretKey } from "@dfinity/vetkeys"; -const seed = crypto.getRandomValues(new Uint8Array(32)); -const transportSecretKey = TransportSecretKey.fromSeed(seed); -const transportPublicKey = transportSecretKey.publicKey(); +const transportSecretKey = TransportSecretKey.random(); +const transportPublicKey = transportSecretKey.publicKeyBytes(); ``` Pass `transportPublicKey` to the canister when requesting a derived key. @@ -213,8 +231,9 @@ import { EncryptedVetKey, } from "@dfinity/vetkeys"; -// Use the caller's principal bytes (or another unique identifier) as the input -const input = new Uint8Array(0); // simplest possible input; use a real ID in practice +// An additional identifier within the caller's key space. +// Use an empty vector for a single per-user key, or a document/room ID for multiple. +const input = new Uint8Array(0); const [encryptedKeyBytes, verificationKeyBytes] = await Promise.all([ backendActor.get_encrypted_vetkey(input, transportPublicKey), @@ -377,7 +396,7 @@ For `vetkd_derive_key` testing, use the [chain-key testing canister](https://git - **Reusing transport keys across sessions.** Each session must generate a fresh transport key pair. - **Using the raw vetKey as an AES key.** Always call `toDerivedKeyMaterial()` first; do not pass the raw bytes to `importKey`. - **Putting secret data in the `input` field.** The `input` is sent to the management canister in plaintext. Use it as an identifier (principal, document ID), not for the secret data itself. -- **Mismatched `context` between frontend and backend.** If the canister uses `b"my_app_v1"` but the frontend verification uses a different value, decryption will fail silently. +- **Mismatched `context` between `getPublicKey` and `getEncryptedVetKey`.** Both endpoints must derive context from the same inputs (domain separator + caller principal). If they differ, `decryptAndVerify` will fail silently. - **Not attaching enough cycles to `vetkd_derive_key`.** `test_key_1` costs approximately 10 billion cycles; `key_1` costs approximately 26 billion cycles. ## Next steps From c06e3dfd2ad87fe70772bc9691bb720e4c7c7168 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Tue, 28 Apr 2026 16:18:51 +0200 Subject: [PATCH 4/4] fix(vetkeys): replace onchain/blockchain with network terminology --- docs/concepts/vetkeys.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/concepts/vetkeys.md b/docs/concepts/vetkeys.md index 3f4499eb..e27d660f 100644 --- a/docs/concepts/vetkeys.md +++ b/docs/concepts/vetkeys.md @@ -1,13 +1,13 @@ --- title: "VetKeys" -description: "Verifiable encrypted threshold key derivation for onchain encryption and secret management" +description: "Verifiable encrypted threshold key derivation for encryption and secret management on ICP" sidebar: order: 11 --- VetKeys (verifiably encrypted threshold keys) give canisters the ability to derive secret key material on demand, without any node or canister ever seeing the raw key. The protocol that underpins this capability is called vetKD: verifiable encrypted threshold key derivation. -The core problem vetKeys solve: encrypting data and storing it onchain is easy when the secret key stays on one device. The difficulty arises when a user needs to access that data from another device, share it with someone else, or let a canister participate in encryption workflows. Transmitting key material over public channels or storing it in a canister exposes it. VetKeys eliminate that exposure by making keys derivable from the network itself, encrypted for delivery, and verifiable by the recipient. +The core problem vetKeys solve: encrypting data and storing it on the network is easy when the secret key stays on one device. The difficulty arises when a user needs to access that data from another device, share it with someone else, or let a canister participate in encryption workflows. Transmitting key material over public channels or storing it in a canister exposes it. VetKeys eliminate that exposure by making keys derivable from the network itself, encrypted for delivery, and verifiable by the recipient. ## The vetKD properties @@ -69,7 +69,7 @@ The only supported curve is `bls12_381_g2`. Two key names are available: ## Use cases -### Encrypted onchain storage +### Encrypted storage A canister derives a symmetric encryption key for each user or resource using a unique input (a principal or document ID). The client encrypts data with this key before storing it in the canister. Only the client, and anyone the canister grants access to, can later obtain the decryption key. The `EncryptedMaps` library in `ic-vetkeys` and `@dfinity/vetkeys` provides a ready-to-use implementation of this pattern.