Skip to content

Commit c73823d

Browse files
marc0oloclaude
andauthored
feat(vetkeys): adopt EncryptedMaps mixins in the Motoko examples (ic-vetkeys 0.6.0) (#1475)
* feat(vetkeys): adopt EncryptedMaps mixins in the Motoko password managers mops ic-vetkeys 0.6.0 adds the Motoko counterpart to the Rust macro, closing the Motoko half of dfinity/vetkeys#423: - `EncryptedMapsCanister` (mo:ic-vetkeys/encrypted_maps/Canister) contributes the complete Encrypted Maps endpoint set. `password_manager` drops from 228 lines of hand-written delegation to ~15. - `EncryptedMapsControlPlaneCanister` (.../ControlPlaneCanister) contributes the control-plane endpoints and the in-scope `encryptedMaps` object but none of the value read/write endpoints. `password_manager_with_metadata` uses it, so the plain insert/remove mutators — which would write a value with no metadata row and desync the two stores — are never exposed, while the canister keeps its own `*_with_metadata` endpoints. Neither mixin declares stable state: the actor declares the `EncryptedMapsState` and passes it in, keeping the persistent state a plain, visible stable variable the canister owns and can migrate. Both password managers become plain persistent actors that resolve the vetKD key name from the `VETKD_KEY_NAME` canister environment variable (defaulting to test_key_1 so init stays total), replacing the `actor class (keyName : Text)` and its `init_args`. This follows the upstream reference canisters and removes the actor class from the main canister. `icp.yaml` sets the variable under `settings.environment_variables`; the generated Candid loses its `(text)` init argument but exposes an identical 11-method interface. Domain separators now name their own example: both Motoko password managers shared `password_manager_example_app`, and are now `password_manager_app` and `password_manager_with_metadata_app`, matching the Rust examples. 0.6.0 requires moc 1.13.0 and mo:core 2.6.1 (raised from 1.11.0 / 2.5.0), so all five Motoko examples pinning ic-vetkeys move together. The new toolchain surfaces warnings in basic_ibe, basic_bls_signing and basic_vetkd: dot notation (auto-fixed), the deprecated Nat64.fromNat / Nat8.fromNat / Blob.fromArray, and never-reassigned `var`s. Changing those to `let` needs no migration — matching stable fields may differ in mutability. Verified: mops check clean for all five; all backends build; local icp deploy e2e on both password managers — insert/update/read with metadata, vault sharing via the mixin-generated control plane, values and metadata surviving an upgrade, and `insert_encrypted_value` correctly absent from the metadata canister (IC0536). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(vetkeys): resolve the vetKD key name from VETKD_KEY_NAME everywhere The two password managers moved to the `VETKD_KEY_NAME` canister environment variable when they adopted the Encrypted Maps mixins, which left the other Motoko vetKeys examples on `actor class (keyName : Text)` + `init_args` — two conventions side by side in one directory. This converts the rest: - basic_ibe, basic_bls_signing: actor class parameter -> env var - basic_vetkd: the key name was hardcoded to `test_key_1`; it is now configurable through the same variable - encrypted_notes_app_vetkd: also drops the actor class. Its `shared ({ caller = initializer })` binding was never used, so nothing needed replacing. Each canister is now a plain actor reading the key at install with a `?? "test_key_1"` default, so init stays total. `icp.yaml` sets the variable under `settings.environment_variables`, and the READMEs document it — including that the key cannot be changed once a canister holds data, since it feeds vetKD key derivation. Regenerated the committed Candid: every service loses its `(text)` init argument, and the method sets are unchanged. basic_vetkd's file was previously hand-formatted, so it also picks up the generator's parameter names and ordering. Verified: mops check clean; local icp deploy for all four — IBE public key, BLS sign + list, symmetric key verification, and a note round trip. Confirmed the variable is genuinely delivered to the canister rather than silently falling back to the default, by deploying basic_ibe with VETKD_KEY_NAME=no_such_key and watching vetKD reject that key name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(vetkeys): align key-name docs and comments with actual behaviour The `icp.yaml` comment claimed the key is read at install time, which is only true for the password managers: they bake it into the stable EncryptedMapsState, so a later change is ignored. The other four hold it in a `transient`, re-read on every upgrade, so a changed variable does take effect — and orphans everything encrypted under the old key. The code comments and READMEs of those four said only "do not change it" without saying what happens; they now state it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(vetkeys): capture the vetKD key name in stable state, not a transient basic_ibe, basic_bls_signing, basic_vetkd and encrypted_notes_app_vetkd held the key name in a `transient`, which is re-read on every upgrade — so editing VETKD_KEY_NAME and redeploying silently re-keyed the canister and orphaned everything encrypted under the old key, with a green deploy and no error. Dropping `transient` makes it a stable variable captured at the first install, matching the password managers, where the key is baked into the EncryptedMapsState. A later change to the variable is now ignored instead of destructive; only a reinstall, which drops all data, switches keys. This takes away no capability. Changing the key already destroyed the ability to read the data — a migration could rewrite the field, but the canister only ever sees ciphertext and cannot re-encrypt, so it would just yield a canister that cannot read its own store. Real re-keying needs application-level key rotation, which these examples do not implement and which their READMEs already note. All six examples now behave identically, so the code comments, the icp.yaml comments and the README paragraph are one shared wording that states when the key is read, that later changes are ignored, and why reinstall is the only escape. Verified: mops check and icp project show clean for all six; deployed basic_ibe, then upgraded it with VETKD_KEY_NAME=no_such_key and confirmed the IBE public key is unchanged — the misconfiguration is ignored rather than silently orphaning data. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(vetkeys): make the Motoko canister headers doc comments Mirrors the review feedback on #1474 for the Motoko side. The header block describing each canister becomes `///`, matching how cert-var, random_maze and canister_factory already document their `main.mo`. Only the two headers change. Inline notes on individual declarations stay `//`: they are implementation notes, not interface documentation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(vetkeys): finish the password_manager doc comment block Review catch on #1475: the last two lines of the header stayed `//`, truncating the doc comment mid-sentence. The script that converted the block ended it at the first "actor " it found, which matched the words "this actor declares" inside the prose rather than the declaration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 19dea9d commit c73823d

28 files changed

Lines changed: 354 additions & 502 deletions

File tree

‎motoko/vetkeys/basic_bls_signing/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ To confirm the canister can only produce signatures in the intended way, users n
2121

2222
### (Optionally) choose a different master key
2323

24-
This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` before deploying.
24+
This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `VETKD_KEY_NAME` environment variable in `icp.yaml` before the first deploy.
25+
26+
The key name is read once at the first install and captured in stable state: it feeds vetKD key derivation, so a different key cannot decrypt what the old one encrypted — and since the canister only ever sees ciphertext, it cannot re-encrypt either. Changing the variable on a later upgrade is therefore silently ignored; only `icp deploy --mode reinstall`, which drops all data, switches keys. Re-keying live data would need application-level key rotation, which these examples do not implement.
2527

2628
### Install
2729

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
type _anon_class_13_1 =
2-
service {
3-
getMySignatures: () -> (vec Signature) query;
4-
getMyVerificationKey: () -> (blob);
5-
signMessage: (message: text) -> (blob);
6-
};
71
type Signature =
82
record {
93
message: text;
104
signature: blob;
115
timestamp: nat64;
126
};
13-
service : (keyName: text) -> _anon_class_13_1
7+
service : {
8+
getMySignatures: () -> (vec Signature) query;
9+
getMyVerificationKey: () -> (blob);
10+
signMessage: (message: text) -> (blob);
11+
}

‎motoko/vetkeys/basic_bls_signing/backend/main.mo‎

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,26 @@ import Time "mo:core/Time";
66
import Map "mo:core/Map";
77
import Array "mo:core/Array";
88
import List "mo:core/List";
9-
import Nat8 "mo:core/Nat8";
9+
import Nat "mo:core/Nat";
1010
import VetKeys "mo:ic-vetkeys";
1111
import Order "mo:core/Order";
12+
import Runtime "mo:core/Runtime";
13+
14+
actor {
15+
// The vetKD key name this canister derives from. Set the `VETKD_KEY_NAME`
16+
// canister environment variable (see `icp.yaml`) to pick a different key; it
17+
// defaults to `test_key_1` so a deploy can never leave the canister
18+
// half-initialized.
19+
//
20+
// This is a stable variable on purpose, so it is captured at the first install
21+
// and never re-read: changing the environment variable later is silently
22+
// ignored, and only a reinstall — which drops all data — switches keys. The key
23+
// feeds vetKD derivation, so a different key cannot decrypt what the old one
24+
// encrypted, and this canister only ever sees ciphertext and so cannot
25+
// re-encrypt. Were it `transient` instead, a changed variable would take effect
26+
// on the next upgrade and silently orphan the stored data.
27+
let keyName = Runtime.envVar<system>("VETKD_KEY_NAME") ?? "test_key_1";
1228

13-
actor class (keyName : Text) = {
1429
// Types
1530
type Signature = {
1631
message : Text;
@@ -37,7 +52,7 @@ actor class (keyName : Text) = {
3752
};
3853

3954
// Signatures are retained across upgrades: this actor field is not declared `transient`.
40-
private var signatures = Map.empty<SignatureKey, Signature>();
55+
private let signatures = Map.empty<SignatureKey, Signature>();
4156

4257
// Helper function to get current timestamp
4358
private func getTimestamp() : Nat64 {
@@ -47,19 +62,18 @@ actor class (keyName : Text) = {
4762
// Helper function to create context for vetKD
4863
private func context(signer : Principal) : Blob {
4964
// Domain separator for this app
50-
let domainSeparator : [Nat8] = Blob.toArray(Text.encodeUtf8("basic_bls_signing_app"));
51-
let domainSeparatorLength : [Nat8] = [Nat8.fromNat(domainSeparator.size())]; // Length of domain separator
65+
let domainSeparator : [Nat8] = Text.encodeUtf8("basic_bls_signing_app").toArray();
66+
let domainSeparatorLength : [Nat8] = [domainSeparator.size().toNat8()]; // Length of domain separator
5267

5368
// Combine domain separator length, domain separator, and signer principal
54-
let signerBytes = Principal.toBlob(signer);
55-
let signerArray = Blob.toArray(signerBytes);
69+
let signerBytes = signer.toBlob();
70+
let signerArray = signerBytes.toArray();
5671

57-
let contextArray = Array.concat<Nat8>(
58-
Array.concat<Nat8>(domainSeparatorLength, domainSeparator),
72+
let contextArray = domainSeparatorLength.concat(domainSeparator).concat(
5973
signerArray,
6074
);
6175

62-
Blob.fromArray(contextArray);
76+
contextArray.toBlob();
6377
};
6478

6579
// Helper function to get key ID
@@ -73,7 +87,7 @@ actor class (keyName : Text) = {
7387
// Sign a message using BLS
7488
public shared ({ caller }) func signMessage(message : Text) : async Blob {
7589
let signatureBytes = await VetKeys.ManagementCanister.signWithBls(
76-
Text.encodeUtf8(message),
90+
message.encodeUtf8(),
7791
context(caller),
7892
keyId(),
7993
);
@@ -87,26 +101,26 @@ actor class (keyName : Text) = {
87101

88102
// Handle potential timestamp collisions by incrementing until we find a free slot
89103
var timestampForMapKey = timestamp;
90-
while (Map.get(signatures, signatureKeyCompare, { signer = caller; timestamp = timestampForMapKey }) != null) {
104+
while (signatures.get(signatureKeyCompare, { signer = caller; timestamp = timestampForMapKey }) != null) {
91105
timestampForMapKey += 1;
92106
};
93107

94-
ignore Map.insert(signatures, signatureKeyCompare, { signer = caller; timestamp = timestampForMapKey }, signature);
108+
ignore signatures.insert(signatureKeyCompare, { signer = caller; timestamp = timestampForMapKey }, signature);
95109

96110
signatureBytes;
97111
};
98112

99113
// Get all signatures for the current caller
100114
public shared query ({ caller }) func getMySignatures() : async [Signature] {
101-
var callerSignatures = List.empty<Signature>();
115+
let callerSignatures = List.empty<Signature>();
102116

103-
for ((key, value) in Map.entries(signatures)) {
117+
for ((key, value) in signatures.entries()) {
104118
if (Principal.equal(key.signer, caller)) {
105-
List.add(callerSignatures, value);
119+
callerSignatures.add(value);
106120
};
107121
};
108122

109-
List.toArray(callerSignatures);
123+
callerSignatures.toArray();
110124
};
111125

112126
// Get verification key for the current caller

‎motoko/vetkeys/basic_bls_signing/icp.yaml‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ canisters:
22
- name: backend
33
recipe:
44
type: "@dfinity/motoko@v5.0.0"
5-
init_args:
6-
type: text
7-
value: "(\"test_key_1\")"
5+
settings:
6+
# The vetKD master key this canister derives from, read by the canister via
7+
# `Runtime.envVar("VETKD_KEY_NAME")` and captured in stable state at the first
8+
# install. Set it before deploying: a later change is silently ignored, and
9+
# only a reinstall — which drops all data — switches keys.
10+
environment_variables:
11+
VETKD_KEY_NAME: "test_key_1"
812

913
- name: frontend
1014
recipe:

‎motoko/vetkeys/basic_bls_signing/mops.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[toolchain]
2-
moc = "1.11.0"
2+
moc = "1.13.0"
33

44
[dependencies]
5-
core = "2.5.0"
6-
ic-vetkeys = "0.5.0"
5+
core = "2.6.1"
6+
ic-vetkeys = "0.6.0"
77

88
[moc]
99
# M0236: use context dot notation

‎motoko/vetkeys/basic_ibe/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ Note that generally it is possible for a canister to request a decryption key to
2222

2323
### (Optionally) choose a different master key
2424

25-
This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `init_args` value in `icp.yaml` before deploying.
25+
This example uses `test_key_1` by default. To use a different [available master key](https://docs.internetcomputer.org/concepts/vetkeys/#api-overview), change the `VETKD_KEY_NAME` environment variable in `icp.yaml` before the first deploy.
26+
27+
The key name is read once at the first install and captured in stable state: it feeds vetKD key derivation, so a different key cannot decrypt what the old one encrypted — and since the canister only ever sees ciphertext, it cannot re-encrypt either. Changing the variable on a later upgrade is therefore silently ignored; only `icp deploy --mode reinstall`, which drops all data, switches keys. Re-keying live data would need application-level key rotation, which these examples do not implement.
2628

2729
### Install
2830

‎motoko/vetkeys/basic_ibe/backend/backend.did‎

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
type _anon_class_13_1 =
2-
service {
3-
getIbePublicKey: () -> (blob);
4-
getMyEncryptedIbeKey: (transportKey: blob) -> (blob);
5-
getMyMessages: () -> (Inbox) query;
6-
removeMyMessageByIndex: (messageIndex: nat64) -> (Result);
7-
sendMessage: (request: SendMessageRequest) -> (Result);
8-
};
91
type SendMessageRequest =
102
record {
113
encryptedMessage: blob;
@@ -23,4 +15,10 @@ type Message =
2315
timestamp: nat64;
2416
};
2517
type Inbox = record {messages: vec Message;};
26-
service : (keyNameString: text) -> _anon_class_13_1
18+
service : {
19+
getIbePublicKey: () -> (blob);
20+
getMyEncryptedIbeKey: (transportKey: blob) -> (blob);
21+
getMyMessages: () -> (Inbox) query;
22+
removeMyMessageByIndex: (messageIndex: nat64) -> (Result);
23+
sendMessage: (request: SendMessageRequest) -> (Result);
24+
}

‎motoko/vetkeys/basic_ibe/backend/main.mo‎

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,23 @@ import Nat64 "mo:core/Nat64";
99
import Nat "mo:core/Nat";
1010
import Result "mo:core/Result";
1111
import Int "mo:core/Int";
12+
import Runtime "mo:core/Runtime";
13+
14+
actor {
15+
// The vetKD key name this canister derives from. Set the `VETKD_KEY_NAME`
16+
// canister environment variable (see `icp.yaml`) to pick a different key; it
17+
// defaults to `test_key_1` so a deploy can never leave the canister
18+
// half-initialized.
19+
//
20+
// This is a stable variable on purpose, so it is captured at the first install
21+
// and never re-read: changing the environment variable later is silently
22+
// ignored, and only a reinstall — which drops all data — switches keys. The key
23+
// feeds vetKD derivation, so a different key cannot decrypt what the old one
24+
// encrypted, and this canister only ever sees ciphertext and so cannot
25+
// re-encrypt. Were it `transient` instead, a changed variable would take effect
26+
// on the next upgrade and silently orphan the stored data.
27+
let keyNameString = Runtime.envVar<system>("VETKD_KEY_NAME") ?? "test_key_1";
1228

13-
actor class (keyNameString : Text) {
1429
// Types
1530
type Message = {
1631
sender : Principal;
@@ -64,7 +79,7 @@ actor class (keyNameString : Text) {
6479
let DOMAIN_SEPARATOR : Text = "basic_ibe_example_dapp";
6580

6681
// State
67-
var inboxes = Map.empty<Principal, Inbox>();
82+
let inboxes = Map.empty<Principal, Inbox>();
6883

6984
// Management canister actor
7085
let vetKdSystemApi : VetKdSystemApi = actor ("aaaaa-aa");
@@ -74,22 +89,22 @@ actor class (keyNameString : Text) {
7489
let message : Message = {
7590
sender = caller;
7691
encryptedMessage = request.encryptedMessage;
77-
timestamp = Nat64.fromNat(Int.abs(Time.now()));
92+
timestamp = Int.abs(Time.now()).toNat64();
7893
};
7994

8095
let receiver = request.receiver;
81-
let currentInbox = switch (Map.get(inboxes, Principal.compare, receiver)) {
96+
let currentInbox = switch (inboxes.get(receiver)) {
8297
case (?inbox) { inbox };
8398
case null { { messages = [] } };
8499
};
85100

86101
if (currentInbox.messages.size() >= MAX_MESSAGES_PER_INBOX) {
87-
return #Err("Inbox for " # Principal.toText(receiver) # " is full");
102+
return #Err("Inbox for " # receiver.toText() # " is full");
88103
};
89104

90-
let newMessages = Array.concat(currentInbox.messages, [message]);
105+
let newMessages = currentInbox.messages.concat([message]);
91106
let newInbox : Inbox = { messages = newMessages };
92-
ignore Map.insert(inboxes, Principal.compare, receiver, newInbox);
107+
ignore inboxes.insert(receiver, newInbox);
93108

94109
#Ok();
95110
};
@@ -101,7 +116,7 @@ actor class (keyNameString : Text) {
101116
name = keyNameString;
102117
};
103118

104-
let context = Text.encodeUtf8(DOMAIN_SEPARATOR);
119+
let context = DOMAIN_SEPARATOR.encodeUtf8();
105120
let request : VetKdPublicKeyArgs = {
106121
canister_id = null;
107122
context = context;
@@ -119,8 +134,8 @@ actor class (keyNameString : Text) {
119134
name = keyNameString;
120135
};
121136

122-
let context = Text.encodeUtf8(DOMAIN_SEPARATOR);
123-
let input = Principal.toBlob(caller);
137+
let context = DOMAIN_SEPARATOR.encodeUtf8();
138+
let input = caller.toBlob();
124139
let request : VetKdDeriveKeyArgs = {
125140
context = context;
126141
input = input;
@@ -134,20 +149,20 @@ actor class (keyNameString : Text) {
134149

135150
// Get the caller's messages
136151
public shared query ({ caller }) func getMyMessages() : async Inbox {
137-
switch (Map.get(inboxes, Principal.compare, caller)) {
152+
switch (inboxes.get(caller)) {
138153
case (?inbox) { inbox };
139154
case null { { messages = [] } };
140155
};
141156
};
142157

143158
// Remove a message by index
144159
public shared ({ caller }) func removeMyMessageByIndex(messageIndex : Nat64) : async Result<(), Text> {
145-
let currentInbox = switch (Map.get(inboxes, Principal.compare, caller)) {
160+
let currentInbox = switch (inboxes.get(caller)) {
146161
case (?inbox) { inbox };
147162
case null { { messages = [] } };
148163
};
149164

150-
let index = Nat64.toNat(messageIndex);
165+
let index = messageIndex.toNat();
151166
if (index >= currentInbox.messages.size()) {
152167
return #Err("Message index out of bounds");
153168
};
@@ -158,13 +173,13 @@ actor class (keyNameString : Text) {
158173

159174
for (i in messages.keys()) {
160175
if (i != index) {
161-
List.add(newMessagesList, messages[i]);
176+
newMessagesList.add(messages[i]);
162177
};
163178
};
164179

165-
let newMessages = List.toArray(newMessagesList);
180+
let newMessages = newMessagesList.toArray();
166181
let newInbox : Inbox = { messages = newMessages };
167-
ignore Map.insert(inboxes, Principal.compare, caller, newInbox);
182+
ignore inboxes.insert(caller, newInbox);
168183

169184
#Ok();
170185
};

‎motoko/vetkeys/basic_ibe/icp.yaml‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ canisters:
22
- name: backend
33
recipe:
44
type: "@dfinity/motoko@v5.0.0"
5-
init_args:
6-
type: text
7-
value: "(\"test_key_1\")"
5+
settings:
6+
# The vetKD master key this canister derives from, read by the canister via
7+
# `Runtime.envVar("VETKD_KEY_NAME")` and captured in stable state at the first
8+
# install. Set it before deploying: a later change is silently ignored, and
9+
# only a reinstall — which drops all data — switches keys.
10+
environment_variables:
11+
VETKD_KEY_NAME: "test_key_1"
812

913
- name: frontend
1014
recipe:

‎motoko/vetkeys/basic_ibe/mops.toml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[toolchain]
2-
moc = "1.11.0"
2+
moc = "1.13.0"
33

44
[dependencies]
5-
core = "2.5.0"
6-
ic-vetkeys = "0.5.0"
5+
core = "2.6.1"
6+
ic-vetkeys = "0.6.0"
77

88
[moc]
99
# M0236: use context dot notation

0 commit comments

Comments
 (0)