encrypted-maps について
Encrypted Mapsは、Internet Computer上の暗号化ストレージ向けvetKeysライブラリです。パスワードマネージャー、暗号化ノート、保管庫などに利用でき、クライアント側でデータを暗号化し、Canisterはアクセス制御と暗号文の保存を担います。共有権限は3段階です。
主な機能
- 01クライアント側でキーバリューデータを暗号化
- 02Canisterには暗号文のみを保存
- 03マップのアクセス権限を3段階で設定
- 04RustとMotokoのバックエンド生成機能
- 05TypeScriptフロントエンドクライアント
活用シーン
- 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 を使って暗号化され、キャニスターには暗号文のみが保存されます。所有者は、3 段階のアクセスレベルでほかのプリンシパルとマップを共有できます。これは、あらゆる暗号化ストレージアプリ(パスワードマネージャー、暗号化されたメモ、ボールト)の標準的な出発点です。
より低レベルのプリミティブ(ID ベース暗号化(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 |
レガシーの
@dfinity/vetkeys(0.4 で凍結)ではなく、@icp-sdk/vetkeys(≥0.7)を使用してください。0.5 と 0.6 では@icp-sdk/coreが通常の依存関係として含まれていたため、2 つ目のコピーがネストされる可能性がありました。0.7 では^5 || ^6をピア依存関係として指定しており、API は同一です。フロントエンドのエージェント/ID は@dfinity/agentではなく、@icp-sdk/coreから取得します。
概念
- マップ —
(mapOwner: Principal, mapName: bytes)で識別されます。mapKey → encryptedValueのエントリーを含みます。mapNameとmapKeyはバイト配列で、それぞれ最大 32 バイトです。 - アクセス権 —
Read、ReadWrite、ReadWriteManage(manage はほかのユーザーへの権限の付与/取り消しが可能なことを意味します)。所有者は常にすべての権限を持ちます。 - クライアント側の暗号化 — フロントエンドはマップごとの vetKey を取得し、ローカルで暗号化/復号します。キャニスターはアクセス制御を強制し、暗号文を保存します。マップを共有すると、新しいユーザー向けにマップキーが自動的に再暗号化されます。
- キー名とドメインセパレーターは、値を一度でも暗号化すると変更できません。鍵導出に使われるため、変更すると保存済みの値を復号できなくなります。落とし穴を参照してください。
バックエンド — 数行でキャニスター全体を実装
ジェネレーターは #[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 キーワードがなくてもアップグレード後に保持されます。アクターがこれを所有するため、通常の移行可能な変数のままにします。デプロイ時にキャニスター設定から 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 を基盤としており、名前をキーにして、アクセス制御付きの vetKeys を導出・共有します。KeyManager を直接使うのは、暗号化されたキーバリューのストレージではなく、アクセス制御付きの鍵導出(たとえば、各クライアントに自身で使用するリソースごとの共通鍵または IBE 鍵を渡すこと)が必要な場合に限ってください。ほとんどのアプリには EncryptedMaps が適しています。
注意点として、現時点では KeyManager 用のすぐに使えるキャニスタージェネレーターはありません(dfinity/vetkeys#422)。EncryptedMaps とは異なり、エンドポイントは手動で接続する必要があります。Rust では、ic_vetkeys::key_manager::KeyManager::init にドメインセパレーター、VetKDKeyId、および 3 つの 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は有効な mainnet キーでもあるため、VETKD_KEY_NAMEの設定を忘れたまま本番環境にデプロイすると、気づかないうちにそのキーで実行されます。デプロイ時に想定したキーであることをアサートしてください。 -
0.5.0 以降、導出されたキー素材はデフォルトでメモリ内に保存されます(以前は IndexedDB でした)。再読み込み後も保持するには
IndexedDbDerivedKeyMaterialCacheを渡し、ログアウト時または ID の変更時にclearCache()を呼び出してください。アップグレード後も、古い@dfinity/vetkeys0.1~0.4 の IndexedDB エントリーは保存されたままです。一度削除してください。 -
クライアントの構築方法が変更されました。
DefaultEncryptedMapsClientが受け取るのは、HttpAgentOptionsではなく、初期化済みのHttpAgent(await HttpAgent.create({ identity, host, rootKey }))です。エージェントと ID は@icp-sdk/coreから取得します。 -
AccessRightsは文字列ではなく Candid バリアントです。"ReadWrite"ではなく{ ReadWrite: null }を使用します。権限レベルはRead、ReadWrite、ReadWriteManageの 3 つです。 -
mapNameとmapKeyはそれぞれ最大 32 バイトのバイト配列です。TextEncoderで文字列をエンコードしてください。 -
コントロールプレーンのバリアントを使って、値ごとのアプリ状態の一貫性を保ってください。 各値にメタデータを併せて保存する場合は、
custom_value_endpoints(Rust)またはEncryptedMapsControlPlaneCanister(Motoko)を使用して、値のエンドポイントを独自に管理してください。references/metadata.mdを参照してください。ライブラリの値を変更するエンドポイントも公開すると、2 つのストアの同期がずれてしまいます。 -
アップグレード時に状態を再初期化しないでください。 マクロ/ミックスインがライフサイクルフックを生成し、アップグレード後も安定メモリは保持されます。状態を再構築する独自の
post_upgradeを追加すると、状態が破損します。 -
Cycles。(内部で使用される)
vetkd_derive_keyは cycles を消費します。test_key_1とkey_1のコストはローカルでも mainnet でも同じで、ライブラリは適切な量を付与します(余剰分は返金されます)。Canister に十分な cycles を確保してください。コスト表はvetkeysスキルを参照してください。
追加のリファレンス
- メタデータ/カスタム値エンドポイント:
references/metadata.md - 低レベルの vetKeys(IBE、BLS、タイムロック、対称暗号、オフライン導出):
vetkeysスキル - 標準的なサンプル:dfinity/examples の
motoko/vetkeys/password_manager、rust/vetkeys/password_manager
デプロイと検証
プロビジョニングと一般的なデプロイ手順については、icp-cli スキルを参照してください。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.