mcpField
E
encrypted-maps

encrypted-maps

github.com/dfinity安全3976 次安装今天更新
代码仓库

关于 encrypted-maps

Encrypted Maps 是用于 Internet Computer 的 vetKeys 加密存储库,适合构建密码管理器、加密笔记和保险库。它在客户端加密键值数据,由 Canister 执行访问控制并仅保存密文,支持三档共享权限。

主要功能

  1. 01客户端加密键值数据
  2. 02Canister 仅存储密文
  3. 03支持三档地图访问权限
  4. 04提供 Rust、Motoko 后端生成器
  5. 05提供 TypeScript 前端客户端

适用场景

  1. 01构建密码管理器
  2. 02保存加密笔记
  3. 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 技能。

层RustMotoko前端
软件包ic-vetkeys 0.9ic-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)。

注意事项

  1. 使用生成器,不要手写端点。 Rust 使用 export_encrypted_maps_canister!,Motoko 使用 include EncryptedMapsCanister(state)。手写委托会偏离前端客户端所期望的 Candid,并导致静默故障。

  2. 一旦已有数据,域分隔符和 vetKD 密钥名称就不可更改。 两者都会参与密钥派生;更改任何一个都会导致所有已存储值无法解密。在 Motoko 中,VETKD_KEY_NAME 环境变量会在首次安装时写入稳定状态——之后升级时修改它会被静默忽略(只有 reinstall 才能切换密钥,但会丢弃所有数据)。由于 test_key_1 也是有效的主网密钥,生产部署若忘记设置 VETKD_KEY_NAME,就会静默地使用它——请在部署时断言密钥符合预期。

  3. 自 0.5.0 起,派生密钥材料默认存储在内存中(此前存储在 IndexedDB 中)。传入 IndexedDbDerivedKeyMaterialCache 可将其持久化,以便在重新加载后保留;并在注销或身份变更时调用 clearCache()。升级后,旧版 @dfinity/vetkeys 0.1–0.4 在 IndexedDB 中的条目仍会留存——请清除一次。

  4. 客户端构造方式已更改。 DefaultEncryptedMapsClient 接收已就绪的 HttpAgent(await HttpAgent.create({ identity, host, rootKey })),而不是 HttpAgentOptions。Agent 和身份来自 @icp-sdk/core。

  5. AccessRights 是 Candid 变体,不是字符串——应使用 { ReadWrite: null },而不是 "ReadWrite"。共有三个级别:Read、ReadWrite、ReadWriteManage。

  6. mapName 和 mapKey 是字节数组,长度各不超过 32 字节——请使用 TextEncoder 编码字符串。

  7. 通过控制平面变体保持每个值的应用状态一致。 如果要在每个值旁存储元数据,请使用 custom_value_endpoints(Rust)/ EncryptedMapsControlPlaneCanister(Motoko)并自行实现值端点——请参阅 references/metadata.md。不要同时公开库的原始值变更接口,否则两个存储会失去同步。

  8. 升级时不要重新初始化状态。 宏/混入会生成生命周期钩子;稳定内存会在升级时保留。自行添加 post_upgrade 并重建状态会导致状态损坏。

  9. Cycles。 vetkd_derive_key(内部使用)会消耗 cycles——test_key_1 和 key_1 在本地与主网的成本相同,且库会附加正确数量的 cycles(多余部分会退还)。请确保 canister 资金充足。(成本表请参阅 vetkeys skill。)

其他参考资料

  • 元数据/自定义值端点:references/metadata.md
  • 更底层的 vetKeys(IBE、BLS、timelock、对称加密、离线派生):vetkeys skill
  • 权威示例: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.

评论

支持 Markdown