关于 encrypted-maps
Encrypted Maps 是用于 Internet Computer 的 vetKeys 加密存储库,适合构建密码管理器、加密笔记和保险库。它在客户端加密键值数据,由 Canister 执行访问控制并仅保存密文,支持三档共享权限。
主要功能
- 01客户端加密键值数据
- 02Canister 仅存储密文
- 03支持三档地图访问权限
- 04提供 Rust、Motoko 后端生成器
- 05提供 TypeScript 前端客户端
适用场景
- 01构建密码管理器
- 02保存加密笔记
- 03实现共享访问的加密数据保险库
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dfinity/ icskills --skill encrypted-maps安装后在 Claude Code 中输入/encrypted-maps确认已加载。
附带文件references/metadata.md
详细介绍
加密映射(vetKeys)
EncryptedMaps 是一款现成的 vetKeys 库,用于访问控制式端到端加密键值存储。每个映射都由一个主体拥有,并包含 mapKey → value 条目;值会在客户端使用 vetKey 加密,canister 始终只存储密文。所有者可通过三个访问级别与其他主体共享映射。这是任何加密存储应用(密码管理器、加密笔记、保险库)的默认起点。
如果你需要更底层的原语,例如基于身份的加密(IBE)、阈值 BLS 签名、定时加密或自定义对称密钥方案,请改用 vetkeys 技能。
| 层 | Rust | Motoko | 前端 |
|---|---|---|---|
| 软件包 | ic-vetkeys 0.9 | ic-vetkeys 0.6(moc ≥ 1.13.0,core ≥ 2.6.1) | @icp-sdk/vetkeys 0.7 |
| 后端 | export_encrypted_maps_canister! 宏 | EncryptedMapsCanister 混入 | @icp-sdk/vetkeys/encrypted_maps |
使用
@icp-sdk/vetkeys(≥0.7),不要使用旧版@dfinity/vetkeys(冻结于 0.4)。0.5 和 0.6 将@icp-sdk/core作为普通依赖,因此可能嵌套第二份副本;0.7 则将其声明为^5 || ^6对等依赖,API 完全相同。前端 Agent/身份来自@icp-sdk/core,而非@dfinity/agent。
概念
- 映射 — 由
(mapOwner: Principal, mapName: bytes)标识。包含mapKey → encryptedValue条目。mapName和mapKey是字节数组,长度均最多为 32 字节。 - 访问权限 —
Read、ReadWrite、ReadWriteManage(manage = 可授予/撤销他人的权限)。所有者始终拥有完整权限。 - 客户端加密 — 前端获取每个映射对应的 vetKey,并在本地加密/解密;canister 执行访问控制并存储密文。共享映射时,映射密钥会自动重新加密并提供给新用户。
- 密钥名称和域分隔符一旦有值被加密,就不可更改——它们参与密钥派生,因此更改后会导致已存储的值无法解密。请参阅注意事项。
后端 — 用几行代码搭建整个 canister
生成器会生成 #[init]/#[post_upgrade]、稳定状态以及 @icp-sdk/vetkeys 前端所需的全部端点,因此 Candid 会自动与客户端匹配。不要手写这约 200 行的委托代码。
Rust — export_encrypted_maps_canister!
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!();
生成的 #[init] 会将 vetKD 密钥名称(test_key_1 / key_1)作为 String 参数接收——通过 icp.yaml 中的 init_args 传入。
Motoko — EncryptedMapsCanister 混入
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);
};
在 persistent actor 中,encryptedMapsState 绑定是稳定的,无需使用 stable 关键字即可跨升级持久化;它由 actor 拥有,因此保持为普通的、可迁移的变量。请在部署时通过 canister 设置配置 VETKD_KEY_NAME(请参阅 icp-cli 技能)。
前端(TypeScript)
import { HttpAgent, type Identity } from "@icp-sdk/core/agent";
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
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 from the canister env (never fetchRootKey() in shipped code); undefined on mainnet
const rootKey = safeGetCanisterEnv()?.IC_ROOT_KEY;
const agent = await HttpAgent.create({ identity, host, rootKey });
// 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 });
}
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 — 更底层的一层(仅在 EncryptedMaps 不适用时使用)
EncryptedMaps 构建于 KeyManager 之上,后者会按名称派生 vetKey 并共享访问控制权限。只有在你需要访问控制式的密钥派生(例如,为每个客户端提供一个由其自行使用的、针对每个资源的对称密钥或 IBE 密钥),而不是加密键值存储时,才直接使用 KeyManager——大多数应用都适合使用 EncryptedMaps。
注意:目前还没有现成的 KeyManager canister 生成器(dfinity/vetkeys#422)——与 EncryptedMaps 不同,你需要手动接入端点。在 Rust 中,ic_vetkeys::key_manager::KeyManager::init 接收域分隔符、VetKDKeyId 和三个 Memory 实例(配置、访问控制、共享密钥),并提供 get_vetkey_verification_key、get_encrypted_vetkey、get_user_rights、set_user_rights、remove_user。Motoko 则通过 KeyManager.newKeyManagerState 和 KeyManager 类提供对应功能;前端使用 @icp-sdk/vetkeys/key_manager(KeyManager 和 DefaultKeyManagerClient)。
注意事项
-
使用生成器,不要手写端点。 Rust 使用
export_encrypted_maps_canister!,Motoko 使用include EncryptedMapsCanister(state)。手写委托会偏离前端客户端所期望的 Candid,并导致静默故障。 -
一旦已有数据,域分隔符和 vetKD 密钥名称就不可更改。 两者都会参与密钥派生;更改任何一个都会导致所有已存储值无法解密。在 Motoko 中,
VETKD_KEY_NAME环境变量会在首次安装时写入稳定状态——之后升级时修改它会被静默忽略(只有reinstall才能切换密钥,但会丢弃所有数据)。由于test_key_1也是有效的主网密钥,生产部署若忘记设置VETKD_KEY_NAME,就会静默地使用它——请在部署时断言密钥符合预期。 -
自 0.5.0 起,派生密钥材料默认存储在内存中(此前存储在 IndexedDB 中)。传入
IndexedDbDerivedKeyMaterialCache可将其持久化,以便在重新加载后保留;并在注销或身份变更时调用clearCache()。升级后,旧版@dfinity/vetkeys0.1–0.4 在 IndexedDB 中的条目仍会留存——请清除一次。 -
客户端构造方式已更改。
DefaultEncryptedMapsClient接收已就绪的HttpAgent(await HttpAgent.create({ identity, host, rootKey })),而不是HttpAgentOptions。Agent 和身份来自@icp-sdk/core。 -
AccessRights是 Candid 变体,不是字符串——应使用{ ReadWrite: null },而不是"ReadWrite"。共有三个级别:Read、ReadWrite、ReadWriteManage。 -
mapName和mapKey是字节数组,长度各不超过 32 字节——请使用TextEncoder编码字符串。 -
通过控制平面变体保持每个值的应用状态一致。 如果要在每个值旁存储元数据,请使用
custom_value_endpoints(Rust)/EncryptedMapsControlPlaneCanister(Motoko)并自行实现值端点——请参阅references/metadata.md。不要同时公开库的原始值变更接口,否则两个存储会失去同步。 -
升级时不要重新初始化状态。 宏/混入会生成生命周期钩子;稳定内存会在升级时保留。自行添加
post_upgrade并重建状态会导致状态损坏。 -
Cycles。
vetkd_derive_key(内部使用)会消耗 cycles——test_key_1和key_1在本地与主网的成本相同,且库会附加正确数量的 cycles(多余部分会退还)。请确保 canister 资金充足。(成本表请参阅vetkeysskill。)
其他参考资料
- 元数据/自定义值端点:
references/metadata.md - 更底层的 vetKeys(IBE、BLS、timelock、对称加密、离线派生):
vetkeysskill - 权威示例:dfinity/examples 中的
motoko/vetkeys/password_manager、rust/vetkeys/password_manager
部署与验证
资源配置和通用部署步骤请参阅 icp-cli skill。EncryptedMaps 专属检查:
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.