Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Every SKILL.md has YAML frontmatter followed by a markdown body. See `skills/ski
`name`, `description`, plus `title`, `category` under `metadata:`

### Recommended frontmatter fields
`license`, `compatibility` (environment requirements — tools, system packages, network access; library deps go in `## Prerequisites`) — validator warns if missing but does not block
`license`, `compatibility` (environment requirements — tools, system packages, network access; document library deps in the body, either as a `## Prerequisites` section or a versions table — whichever reads better for the skill) — validator warns if missing but does not block

### Body sections
No rigid structure — organize content to best serve agents.
Expand Down
54 changes: 54 additions & 0 deletions evaluations/encrypted-maps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"output_evals": [
{
"name": "scaffold_backend_with_macro",
"prompt": "Scaffold just the Rust `lib.rs` for an encrypted password-manager canister on the IC using the vetKeys EncryptedMaps library. No frontend, no Cargo.toml, no explanation.",
"expected_behaviors": [
"Uses the `ic_vetkeys::export_encrypted_maps_canister!(...)` macro with a domain-separator string and four `Memory` instances",
"Sets up a `MemoryManager` and a `memory(id)` helper",
"Calls `ic_cdk::export_candid!();`",
"Does NOT hand-write `vetkd_derive_key` calls or the individual EncryptedMaps endpoints"
]
},
{
"name": "share_map_access_rights_variant",
"prompt": "Using @icp-sdk/vetkeys in the frontend, give me just the call that shares an encrypted map with another principal at read-write access. No setup, no explanation.",
"expected_behaviors": [
"Calls `setUserRights(owner, mapName, user, rights)` (map owner, map name, target principal, and rights)",
"Passes access rights as a Candid variant `{ ReadWrite: null }` — NOT the string \"ReadWrite\""
]
},
{
"name": "scaffold_motoko_backend_mixin",
"prompt": "Scaffold just the Motoko main.mo for an encrypted password-manager backend canister using the vetKeys EncryptedMaps library. No frontend, no explanation.",
"expected_behaviors": [
"Uses `include EncryptedMapsCanister(encryptedMapsState)` from `mo:ic-vetkeys/encrypted_maps/Canister`",
"Builds state with `EncryptedMaps.newEncryptedMapsState<Types.AccessRights>({ curve = #bls12_381_g2; name = keyName }, \"<domain separator>\")`",
"Reads the key name from the `VETKD_KEY_NAME` env var via `Runtime.envVar`",
"Declares the canister as `persistent actor` (so state persists without relying on a compiler flag)",
"Does NOT hand-write the individual EncryptedMaps endpoints (vetKD key, value, or access-control methods)"
]
},
{
"name": "derived_key_material_caching",
"prompt": "In my encrypted notes app the users re-derive their vetKey on every page reload. Should I just save the derived key material in localStorage to avoid that? Answer in a short paragraph.",
"expected_behaviors": [
"Warns against persisting raw key material in localStorage",
"Explains that since @icp-sdk/vetkeys 0.5.0 derived key material is cached in memory only by default (no longer persisted automatically)",
"Recommends `IndexedDbDerivedKeyMaterialCache` to persist across reloads and calling `clearCache()` on logout / identity change"
]
}
],
"trigger_evals": {
"description": "Queries testing whether the encrypted-maps skill activates. should_trigger should load it; should_not_trigger should route elsewhere (bidirectional routing with the sibling skill).",
"should_trigger": [
"Build a password manager on the IC with encrypted vaults users can share with each other",
"Store encrypted key-value data on-chain with per-user read/write access rights",
"Encrypt notes client-side and store them in my canister, shareable between users"
],
"should_not_trigger": [
"I need my canister to produce threshold BLS signatures verifiable by anyone",
"Encrypt a message to a specific principal using identity-based encryption"
]
}
}
64 changes: 64 additions & 0 deletions evaluations/vetkeys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"output_evals": [
{
"name": "ibe_encrypt_to_principal",
"prompt": "Using the IC vetKeys frontend library, write just the TypeScript function that encrypts a string message to a recipient's principal with identity-based encryption (IBE). Assume you already have the canister's derived IBE public key bytes. No canister/backend code, no explanation.",
"expected_behaviors": [
"Imports from `@icp-sdk/vetkeys` (NOT `@dfinity/vetkeys`)",
"Uses `IbeCiphertext.encrypt(...)` with `IbeIdentity.fromPrincipal(recipient)` and `IbeSeed.random()`",
"Deserializes the public key via `DerivedPublicKey.deserialize(...)` and serializes the ciphertext with `.serialize()`",
"Does not invent a static per-user key pair — encrypts to the identity using the derived public key"
]
},
{
"name": "symmetric_key_material_api",
"prompt": "In a frontend, I already decrypted and verified a vetKey into a `VetKey` object. Give me just the snippet that derives an AES key from it and encrypts a string. No backend, no explanation.",
"expected_behaviors": [
"Calls `await vetKey.asDerivedKeyMaterial()` (async) — does NOT call a non-existent `toDerivedKeyMaterial()`",
"Encrypts via `await keyMaterial.encryptMessage(message, domainSep, associatedData)` (the correct 3-argument signature)",
"Does NOT use the raw decrypted vetKey bytes directly as an AES key"
]
},
{
"name": "motoko_has_no_low_level_primitives",
"prompt": "I'm writing a Motoko canister that uses vetKeys for IBE. Show me the Motoko code that generates the transport key pair, calls vetkd_derive_key, and then decrypts the encrypted vetKey with the transport secret key inside the canister. Just the Motoko.",
"expected_behaviors": [
"States that the Motoko `ic-vetkeys` library has no low-level primitives (no transport keys, no IBE, no vetKey decryption)",
"Explains the canister returns the ENCRYPTED vetKey and the frontend (`@icp-sdk/vetkeys`) does transport-key generation, `decryptAndVerify`, and IBE decryption",
"Shows the backend deriving via `mo:ic-vetkeys/ManagementCanister` (`vetKdDeriveKey`) rather than hand-rolling `actor \"aaaaa-aa\"`",
"Does not fabricate a Motoko `TransportSecretKey` / `decryptAndVerify` API"
]
},
{
"name": "cycle_cost_and_helpers",
"prompt": "How many cycles does a vetkd_derive_key call with test_key_1 cost on the local replica versus mainnet, and do I attach them manually? One short paragraph.",
"expected_behaviors": [
"States test_key_1 and key_1 cost the same locally and on mainnet — does NOT claim the local replica charges a different or higher amount",
"Gives the amounts: test_key_1 = 10_000_000_000, key_1 = 26_153_846_153, and notes vetkd_public_key is free",
"Notes the library helpers attach the cycles automatically (the Rust `ic-cdk-management-canister` binding or the Motoko `ManagementCanister`), so you just keep the canister funded"
]
},
{
"name": "bls_verify_uses_derived_public_key",
"prompt": "Frontend: verify a threshold BLS signature returned by my canister using the vetKeys library. Just the verification snippet, no setup.",
"expected_behaviors": [
"Calls `verifyBlsSignature(...)` imported from `@icp-sdk/vetkeys`",
"Passes a DerivedPublicKey object as the first argument via `DerivedPublicKey.deserialize(publicKeyBytes)` — NOT raw public-key bytes",
"Passes the message and signature as the remaining arguments"
]
}
],
"trigger_evals": {
"description": "Queries testing whether the vetkeys skill activates. should_trigger should load it; should_not_trigger should route elsewhere (bidirectional routing with the sibling skill).",
"should_trigger": [
"Implement identity-based encryption on the IC so a user can send an encrypted message to another user's principal",
"Add threshold BLS signing to my canister so anyone can verify signatures it produces",
"Build a sealed-bid auction where bids stay encrypted until the deadline passes",
"Derive a per-user symmetric AES key on my canister so each user can encrypt their own data"
],
"should_not_trigger": [
"Add an encrypted password manager with shareable vaults to my dapp",
"Set up Internet Identity login for my web app"
]
}
}
176 changes: 176 additions & 0 deletions skills/encrypted-maps/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
---
name: encrypted-maps
description: "Add access-controlled, end-to-end encrypted key-value storage to a canister with the vetKeys EncryptedMaps library (ic-vetkeys for Rust and Motoko backends, @icp-sdk/vetkeys for the frontend). Values are encrypted client-side under vetKeys and shared between principals with per-user access rights (Read, ReadWrite, ReadWriteManage). Use when building a password manager, encrypted notes, a secure vault, or any app that stores and shares encrypted data on-chain. Start here for encrypted storage; escalate to the vetkeys skill only for BLS signatures, custom IBE, or timelock encryption."
license: Apache-2.0
compatibility: "icp-cli >= 0.2.2"
metadata:
title: Encrypted Maps
category: Security
---

# Encrypted Maps (vetKeys)

`EncryptedMaps` is a ready-made vetKeys library for **access-controlled, end-to-end encrypted key-value storage**. Each map is owned by a principal and holds `mapKey → value` entries; **values are encrypted on the client** under a vetKey and the canister only ever stores ciphertext. Owners share maps with other principals at three access levels. This is the default starting point for any encrypted-storage app (password manager, encrypted notes, vault).

Use the **`vetkeys` skill** instead when you need lower-level primitives: identity-based encryption (IBE), threshold BLS signatures, timelock encryption, or your own symmetric-key scheme.

| Layer | Rust | Motoko | Frontend |
|-------|------|--------|----------|
| Package | `ic-vetkeys` **0.9** | `ic-vetkeys` **0.6** (moc ≥ 1.13.0, core ≥ 2.6.1) | `@icp-sdk/vetkeys` **0.5** |
| Backend | `export_encrypted_maps_canister!` macro | `EncryptedMapsCanister` mixin | `@icp-sdk/vetkeys/encrypted_maps` |

> Use `@icp-sdk/vetkeys` (≥0.5), not the legacy `@dfinity/vetkeys` (frozen at 0.4). Frontend agent/identity come from `@icp-sdk/core`, not `@dfinity/agent`.

## Concepts

- **Map** — identified by `(mapOwner: Principal, mapName: bytes)`. Contains `mapKey → encryptedValue` entries. `mapName` and `mapKey` are byte arrays, **max 32 bytes** each.
- **Access rights** — `Read`, `ReadWrite`, `ReadWriteManage` (manage = may grant/revoke others). The owner always has full rights.
- **Client-side encryption** — the frontend fetches a per-map vetKey and encrypts/decrypts locally; the canister enforces access control and stores ciphertext. Sharing a map re-encrypts the map key for the new user automatically.
- **Key name & domain separator are immutable** once any value is encrypted — they feed key derivation, so changing them makes stored values undecryptable. See pitfalls.

## Backend — the whole canister in a few lines

The generator emits the `#[init]`/`#[post_upgrade]`, the stable state, and every endpoint the `@icp-sdk/vetkeys` frontend expects — so the Candid matches the client by construction. Do not hand-write the ~200 lines of delegation.

### Rust — `export_encrypted_maps_canister!`

```rust
use ic_stable_structures::memory_manager::{MemoryId, MemoryManager, VirtualMemory};
use ic_stable_structures::DefaultMemoryImpl;
use std::cell::RefCell;

type Memory = VirtualMemory<DefaultMemoryImpl>;

thread_local! {
static MEMORY_MANAGER: RefCell<MemoryManager<DefaultMemoryImpl>> =
RefCell::new(MemoryManager::init(DefaultMemoryImpl::default()));
}

fn memory(id: u8) -> Memory {
MEMORY_MANAGER.with(|m| m.borrow().get(MemoryId::new(id)))
}

// Arg 1: the domain separator that isolates this app's derived keys (keep it
// stable forever). Then four Memory instances, in order: domain-separator config,
// access control, shared keys, encrypted values.
ic_vetkeys::export_encrypted_maps_canister!(
"password_manager_app",
[memory(0), memory(1), memory(2), memory(3)],
);

ic_cdk::export_candid!();
```

The generated `#[init]` takes the vetKD key name (`test_key_1` / `key_1`) as a `String` argument — pass it via `init_args` in `icp.yaml`.

### Motoko — `EncryptedMapsCanister` mixin

```motoko
import EncryptedMapsCanister "mo:ic-vetkeys/encrypted_maps/Canister";
import EncryptedMaps "mo:ic-vetkeys/encrypted_maps/EncryptedMaps";
import Types "mo:ic-vetkeys/Types";
import Runtime "mo:core/Runtime";

persistent actor PasswordManager {
// `transient`: the key name is baked into `encryptedMapsState` at install and never re-read.
transient let keyName = Runtime.envVar<system>("VETKD_KEY_NAME") ?? "test_key_1";

// Arg 2 is the domain separator; like the key name it must stay stable for the life of the canister.
let encryptedMapsState = EncryptedMaps.newEncryptedMapsState<Types.AccessRights>(
{ curve = #bls12_381_g2; name = keyName },
"password_manager_app",
);

// The mixin contributes the full endpoint set (vetKD key, access control, map-name, value endpoints)
// as snake_case methods, exactly what the frontend client calls.
include EncryptedMapsCanister(encryptedMapsState);
};
```

In a `persistent actor` the `encryptedMapsState` binding is stable and persists across upgrades with no `stable` keyword; the actor owns it, so it stays a plain, migratable variable. Set `VETKD_KEY_NAME` at deploy time via canister settings (see the `icp-cli` skill).

## Frontend (TypeScript)

```typescript
import { HttpAgent, type Identity } from "@icp-sdk/core/agent";
import {
DefaultEncryptedMapsClient,
EncryptedMaps,
IndexedDbDerivedKeyMaterialCache,
type AccessRights,
} from "@icp-sdk/vetkeys/encrypted_maps";

export async function createEncryptedMaps(
identity: Identity, canisterId: string, host: string,
): Promise<EncryptedMaps> {
// rootKey comes from safeGetCanisterEnv() (@icp-sdk/core/agent/canister-env) — never fetchRootKey()
const agent = await HttpAgent.create({ identity, host, rootKey });
Comment thread
marc0olo marked this conversation as resolved.
// Since 0.5.0 derived key material is cached in memory only by default.
// Opt into cross-reload persistence, namespaced by principal; clearCache() on logout.
const cache = new IndexedDbDerivedKeyMaterialCache(`vetkeys-${identity.getPrincipal().toText()}`);
return new EncryptedMaps(new DefaultEncryptedMapsClient(agent, canisterId), { cache });
}
```

```typescript
const owner = myPrincipal; // Principal from @icp-sdk/core/principal
const mapName = new TextEncoder().encode("my-vault"); // ≤ 32 bytes
const mapKey = new TextEncoder().encode("github.com"); // ≤ 32 bytes

// Store / read / remove (encryption happens client-side)
await encryptedMaps.setValue(owner, mapName, mapKey, new TextEncoder().encode("s3cr3t"));
const value = await encryptedMaps.getValue(owner, mapName, mapKey); // Uint8Array (empty if absent)
await encryptedMaps.removeEncryptedValue(owner, mapName, mapKey);

// Share the map with another principal (AccessRights is a Candid variant, not a string)
const rights: AccessRights = { ReadWrite: null }; // or { Read: null } / { ReadWriteManage: null }
await encryptedMaps.setUserRights(owner, mapName, otherPrincipal, rights);
const theirRights = await encryptedMaps.getUserRights(owner, mapName, otherPrincipal);

// Everything the caller can access (owned + shared)
const maps = await encryptedMaps.getAllAccessibleMaps();

await encryptedMaps.clearCache(); // on logout / identity switch
```

## KeyManager — the layer beneath (use only when EncryptedMaps doesn't fit)

`EncryptedMaps` is built on **`KeyManager`**, which derives and shares access-controlled vetKeys keyed by name. Reach for `KeyManager` directly only when you need access-controlled **key derivation** (e.g. handing each client a per-resource symmetric or IBE key to use themselves) rather than encrypted key-value **storage** — most apps want `EncryptedMaps`.

Caveat: there is **no ready-made canister generator for KeyManager yet** ([dfinity/vetkeys#422](https://github.com/dfinity/vetkeys/issues/422)) — unlike EncryptedMaps, you wire the endpoints by hand. In Rust, `ic_vetkeys::key_manager::KeyManager::init` takes the domain separator, the `VetKDKeyId`, and three `Memory` instances (config, access control, shared keys), and exposes `get_vetkey_verification_key`, `get_encrypted_vetkey`, `get_user_rights`, `set_user_rights`, `remove_user`. Motoko mirrors this via `KeyManager.newKeyManagerState` + the `KeyManager` class; the frontend uses `@icp-sdk/vetkeys/key_manager` (`KeyManager` + `DefaultKeyManagerClient`).

## Pitfalls

1. **Use the generator, don't hand-write endpoints.** Rust `export_encrypted_maps_canister!`, Motoko `include EncryptedMapsCanister(state)`. Hand-written delegation drifts from the Candid the frontend client expects and breaks silently.

2. **Domain separator and vetKD key name are immutable once data exists.** Both feed key derivation; changing either makes every stored value undecryptable. In Motoko the `VETKD_KEY_NAME` env var is captured into stable state at first install — editing it on a later upgrade is silently ignored (only a `reinstall`, which drops all data, switches keys). Because `test_key_1` is also a valid mainnet key, a production deploy that forgets to set `VETKD_KEY_NAME` silently runs on it — assert the expected key at deploy time.

3. **Derived key material is in-memory by default since 0.5.0** (was IndexedDB). Pass `IndexedDbDerivedKeyMaterialCache` to persist it across reloads, and call `clearCache()` on logout / identity change. Old `@dfinity/vetkeys` 0.1–0.4 IndexedDB entries remain at rest after upgrading — clear them once.

4. **Client construction changed.** `DefaultEncryptedMapsClient` takes a ready `HttpAgent` (`await HttpAgent.create({ identity, host, rootKey })`), not `HttpAgentOptions`. Agent/identity come from `@icp-sdk/core`.

5. **`AccessRights` is a Candid variant, not a string** — `{ ReadWrite: null }`, not `"ReadWrite"`. Three levels: `Read`, `ReadWrite`, `ReadWriteManage`.

6. **`mapName` and `mapKey` are byte arrays, ≤ 32 bytes each** — encode strings with `TextEncoder`.

7. **Keep per-value app state consistent via the control-plane variant.** If you store metadata alongside each value, use `custom_value_endpoints` (Rust) / `EncryptedMapsControlPlaneCanister` (Motoko) and own the value endpoints — see `references/metadata.md`. Don't also expose the library's raw value mutators, or the two stores desync.

8. **Don't re-init state on upgrade.** The macro/mixin generate the lifecycle hooks; stable memory survives upgrades. Adding your own `post_upgrade` that rebuilds state corrupts it.

9. **Cycles.** `vetkd_derive_key` (used under the hood) costs cycles — `test_key_1` and `key_1` cost the same locally and on mainnet, and the library attaches the right amount (excess refunded). Keep the canister funded. (See the `vetkeys` skill for the cost table.)

## Additional References

- Metadata / custom value endpoints: `references/metadata.md`
- Lower-level vetKeys (IBE, BLS, timelock, symmetric, offline derivation): the **`vetkeys`** skill
- Canonical examples: `motoko/vetkeys/password_manager`, `rust/vetkeys/password_manager` in [dfinity/examples](https://github.com/dfinity/examples/tree/master/rust/vetkeys)

## Deploy & verify

Provisioning and generic deploy steps belong to the **`icp-cli`** skill. EncryptedMaps-specific checks:

```bash
icp deploy backend # local replica provisions test_key_1
# From the frontend: setValue then getValue round-trips the plaintext for the owner;
# a principal without rights gets an access-control error on getValue.
```
Loading
Loading