@didcid/keymaster 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/cjs/db/abstract-base.cjs +25 -0
- package/dist/cjs/db/cache.cjs +27 -0
- package/dist/cjs/db/chrome.cjs +32 -0
- package/dist/cjs/db/json-memory.cjs +24 -0
- package/dist/cjs/db/json.cjs +35 -0
- package/dist/cjs/db/mongo.cjs +57 -0
- package/dist/cjs/db/redis.cjs +55 -0
- package/dist/cjs/db/sqlite.cjs +69 -0
- package/dist/cjs/db/typeGuards.cjs +11 -0
- package/dist/cjs/db/web.cjs +29 -0
- package/dist/cjs/encryption.cjs +59 -0
- package/dist/cjs/index.cjs +32 -0
- package/dist/cjs/keymaster-client.cjs +1139 -0
- package/dist/cjs/keymaster.cjs +3787 -0
- package/dist/cjs/node.cjs +45 -0
- package/dist/cjs/search-client.cjs +87 -0
- package/dist/esm/db/abstract-base.js +22 -0
- package/dist/esm/db/abstract-base.js.map +1 -0
- package/dist/esm/db/cache.js +21 -0
- package/dist/esm/db/cache.js.map +1 -0
- package/dist/esm/db/chrome.js +26 -0
- package/dist/esm/db/chrome.js.map +1 -0
- package/dist/esm/db/json-memory.js +18 -0
- package/dist/esm/db/json-memory.js.map +1 -0
- package/dist/esm/db/json.js +29 -0
- package/dist/esm/db/json.js.map +1 -0
- package/dist/esm/db/mongo.js +51 -0
- package/dist/esm/db/mongo.js.map +1 -0
- package/dist/esm/db/redis.js +49 -0
- package/dist/esm/db/redis.js.map +1 -0
- package/dist/esm/db/sqlite.js +63 -0
- package/dist/esm/db/sqlite.js.map +1 -0
- package/dist/esm/db/typeGuards.js +7 -0
- package/dist/esm/db/typeGuards.js.map +1 -0
- package/dist/esm/db/web.js +23 -0
- package/dist/esm/db/web.js.map +1 -0
- package/dist/esm/encryption.js +55 -0
- package/dist/esm/encryption.js.map +1 -0
- package/dist/esm/index.js +11 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/keymaster-client.js +1133 -0
- package/dist/esm/keymaster-client.js.map +1 -0
- package/dist/esm/keymaster.js +2733 -0
- package/dist/esm/keymaster.js.map +1 -0
- package/dist/esm/node.js +7 -0
- package/dist/esm/node.js.map +1 -0
- package/dist/esm/search-client.js +81 -0
- package/dist/esm/search-client.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/db/abstract-base.d.ts +7 -0
- package/dist/types/db/cache.d.ts +9 -0
- package/dist/types/db/chrome.d.ts +8 -0
- package/dist/types/db/json-memory.d.ts +7 -0
- package/dist/types/db/json.d.ts +9 -0
- package/dist/types/db/mongo.d.ts +15 -0
- package/dist/types/db/redis.d.ts +13 -0
- package/dist/types/db/sqlite.d.ts +12 -0
- package/dist/types/db/typeGuards.d.ts +3 -0
- package/dist/types/db/web.d.ts +8 -0
- package/dist/types/encryption.d.ts +10 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/keymaster-client.d.ts +134 -0
- package/dist/types/keymaster.d.ts +211 -0
- package/dist/types/node.d.ts +6 -0
- package/dist/types/search-client.d.ts +9 -0
- package/dist/types/types.d.ts +373 -0
- package/package.json +171 -0
|
@@ -0,0 +1,2733 @@
|
|
|
1
|
+
import { imageSize } from 'image-size';
|
|
2
|
+
import { fileTypeFromBuffer } from 'file-type';
|
|
3
|
+
import { InvalidDIDError, InvalidParameterError, KeymasterError, UnknownIDError } from '@didcid/common/errors';
|
|
4
|
+
import { isWalletEncFile, isWalletFile } from './db/typeGuards.js';
|
|
5
|
+
import { isValidDID } from '@didcid/ipfs/utils';
|
|
6
|
+
import { decMnemonic, encMnemonic } from "./encryption.js";
|
|
7
|
+
const DefaultSchema = {
|
|
8
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
9
|
+
"type": "object",
|
|
10
|
+
"properties": {
|
|
11
|
+
"propertyName": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"required": [
|
|
16
|
+
"propertyName"
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
export var DmailTags;
|
|
20
|
+
(function (DmailTags) {
|
|
21
|
+
DmailTags["DMAIL"] = "dmail";
|
|
22
|
+
DmailTags["INBOX"] = "inbox";
|
|
23
|
+
DmailTags["DRAFT"] = "draft";
|
|
24
|
+
DmailTags["SENT"] = "sent";
|
|
25
|
+
DmailTags["ARCHIVED"] = "archived";
|
|
26
|
+
DmailTags["DELETED"] = "deleted";
|
|
27
|
+
DmailTags["UNREAD"] = "unread";
|
|
28
|
+
})(DmailTags || (DmailTags = {}));
|
|
29
|
+
export var NoticeTags;
|
|
30
|
+
(function (NoticeTags) {
|
|
31
|
+
NoticeTags["DMAIL"] = "dmail";
|
|
32
|
+
NoticeTags["BALLOT"] = "ballot";
|
|
33
|
+
NoticeTags["POLL"] = "poll";
|
|
34
|
+
NoticeTags["CREDENTIAL"] = "credential";
|
|
35
|
+
})(NoticeTags || (NoticeTags = {}));
|
|
36
|
+
export default class Keymaster {
|
|
37
|
+
passphrase;
|
|
38
|
+
gatekeeper;
|
|
39
|
+
db;
|
|
40
|
+
cipher;
|
|
41
|
+
searchEngine;
|
|
42
|
+
defaultRegistry;
|
|
43
|
+
ephemeralRegistry;
|
|
44
|
+
maxNameLength;
|
|
45
|
+
maxDataLength;
|
|
46
|
+
_walletCache;
|
|
47
|
+
_hdkeyCache;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
if (!options || !options.gatekeeper || !options.gatekeeper.createDID) {
|
|
50
|
+
throw new InvalidParameterError('options.gatekeeper');
|
|
51
|
+
}
|
|
52
|
+
if (!options.wallet || !options.wallet.loadWallet || !options.wallet.saveWallet) {
|
|
53
|
+
throw new InvalidParameterError('options.wallet');
|
|
54
|
+
}
|
|
55
|
+
if (!options.cipher || !options.cipher.verifySig) {
|
|
56
|
+
throw new InvalidParameterError('options.cipher');
|
|
57
|
+
}
|
|
58
|
+
if (options.search && !options.search.search) {
|
|
59
|
+
throw new InvalidParameterError('options.search');
|
|
60
|
+
}
|
|
61
|
+
if (!options.passphrase) {
|
|
62
|
+
throw new InvalidParameterError('options.passphrase');
|
|
63
|
+
}
|
|
64
|
+
this.passphrase = options.passphrase;
|
|
65
|
+
this.gatekeeper = options.gatekeeper;
|
|
66
|
+
this.db = options.wallet;
|
|
67
|
+
this.cipher = options.cipher;
|
|
68
|
+
this.searchEngine = options.search;
|
|
69
|
+
this.defaultRegistry = options.defaultRegistry || 'hyperswarm';
|
|
70
|
+
this.ephemeralRegistry = 'hyperswarm';
|
|
71
|
+
this.maxNameLength = options.maxNameLength || 32;
|
|
72
|
+
this.maxDataLength = 8 * 1024; // 8 KB max data to store in a JSON object
|
|
73
|
+
}
|
|
74
|
+
async listRegistries() {
|
|
75
|
+
return this.gatekeeper.listRegistries();
|
|
76
|
+
}
|
|
77
|
+
async mutateWallet(mutator) {
|
|
78
|
+
// Create wallet if none and make sure _walletCache is set
|
|
79
|
+
if (!this._walletCache) {
|
|
80
|
+
await this.loadWallet();
|
|
81
|
+
}
|
|
82
|
+
await this.db.updateWallet(async (stored) => {
|
|
83
|
+
const decrypted = this._walletCache;
|
|
84
|
+
const before = JSON.stringify(decrypted);
|
|
85
|
+
await mutator(decrypted);
|
|
86
|
+
const after = JSON.stringify(decrypted);
|
|
87
|
+
if (before === after) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const reenc = await this.encryptWalletForStorage(decrypted);
|
|
91
|
+
Object.assign(stored, reenc);
|
|
92
|
+
this._walletCache = decrypted;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async loadWallet() {
|
|
96
|
+
if (this._walletCache) {
|
|
97
|
+
return this._walletCache;
|
|
98
|
+
}
|
|
99
|
+
let stored = await this.db.loadWallet();
|
|
100
|
+
if (!stored) {
|
|
101
|
+
stored = await this.newWallet();
|
|
102
|
+
}
|
|
103
|
+
const upgraded = await this.upgradeWallet(stored);
|
|
104
|
+
this._walletCache = await this.decryptWallet(upgraded);
|
|
105
|
+
return this._walletCache;
|
|
106
|
+
}
|
|
107
|
+
async saveWallet(wallet, overwrite = true) {
|
|
108
|
+
let upgraded = await this.upgradeWallet(wallet);
|
|
109
|
+
// Decrypt if encrypted to verify passphrase and get decrypted form
|
|
110
|
+
const decrypted = await this.decryptWallet(upgraded);
|
|
111
|
+
let toStore = await this.encryptWalletForStorage(decrypted);
|
|
112
|
+
const ok = await this.db.saveWallet(toStore, overwrite);
|
|
113
|
+
if (ok) {
|
|
114
|
+
this._walletCache = decrypted;
|
|
115
|
+
}
|
|
116
|
+
return ok;
|
|
117
|
+
}
|
|
118
|
+
async newWallet(mnemonic, overwrite = false) {
|
|
119
|
+
try {
|
|
120
|
+
if (!mnemonic) {
|
|
121
|
+
mnemonic = this.cipher.generateMnemonic();
|
|
122
|
+
}
|
|
123
|
+
this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw new InvalidParameterError('mnemonic');
|
|
127
|
+
}
|
|
128
|
+
const mnemonicEnc = await encMnemonic(mnemonic, this.passphrase);
|
|
129
|
+
const wallet = {
|
|
130
|
+
version: 1,
|
|
131
|
+
seed: { mnemonicEnc },
|
|
132
|
+
counter: 0,
|
|
133
|
+
ids: {}
|
|
134
|
+
};
|
|
135
|
+
const ok = await this.saveWallet(wallet, overwrite);
|
|
136
|
+
if (!ok) {
|
|
137
|
+
throw new KeymasterError('save wallet failed');
|
|
138
|
+
}
|
|
139
|
+
return wallet;
|
|
140
|
+
}
|
|
141
|
+
async decryptMnemonic() {
|
|
142
|
+
const wallet = await this.loadWallet();
|
|
143
|
+
return this.getMnemonicForDerivation(wallet);
|
|
144
|
+
}
|
|
145
|
+
async getMnemonicForDerivation(wallet) {
|
|
146
|
+
return decMnemonic(wallet.seed.mnemonicEnc, this.passphrase);
|
|
147
|
+
}
|
|
148
|
+
async checkWallet() {
|
|
149
|
+
const wallet = await this.loadWallet();
|
|
150
|
+
let checked = 0;
|
|
151
|
+
let invalid = 0;
|
|
152
|
+
let deleted = 0;
|
|
153
|
+
// Validate keys
|
|
154
|
+
await this.resolveSeedBank();
|
|
155
|
+
for (const name of Object.keys(wallet.ids)) {
|
|
156
|
+
try {
|
|
157
|
+
const doc = await this.resolveDID(wallet.ids[name].did);
|
|
158
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
159
|
+
deleted += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
invalid += 1;
|
|
164
|
+
}
|
|
165
|
+
checked += 1;
|
|
166
|
+
}
|
|
167
|
+
for (const id of Object.values(wallet.ids)) {
|
|
168
|
+
if (id.owned) {
|
|
169
|
+
for (const did of id.owned) {
|
|
170
|
+
try {
|
|
171
|
+
const doc = await this.resolveDID(did);
|
|
172
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
173
|
+
deleted += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
invalid += 1;
|
|
178
|
+
}
|
|
179
|
+
checked += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (id.held) {
|
|
183
|
+
for (const did of id.held) {
|
|
184
|
+
try {
|
|
185
|
+
const doc = await this.resolveDID(did);
|
|
186
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
187
|
+
deleted += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
invalid += 1;
|
|
192
|
+
}
|
|
193
|
+
checked += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (wallet.names) {
|
|
198
|
+
for (const name of Object.keys(wallet.names)) {
|
|
199
|
+
try {
|
|
200
|
+
const doc = await this.resolveDID(wallet.names[name]);
|
|
201
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
202
|
+
deleted += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
invalid += 1;
|
|
207
|
+
}
|
|
208
|
+
checked += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { checked, invalid, deleted };
|
|
212
|
+
}
|
|
213
|
+
async fixWallet() {
|
|
214
|
+
let idsRemoved = 0;
|
|
215
|
+
let ownedRemoved = 0;
|
|
216
|
+
let heldRemoved = 0;
|
|
217
|
+
let namesRemoved = 0;
|
|
218
|
+
await this.mutateWallet(async (wallet) => {
|
|
219
|
+
for (const name of Object.keys(wallet.ids)) {
|
|
220
|
+
let remove = false;
|
|
221
|
+
try {
|
|
222
|
+
const doc = await this.resolveDID(wallet.ids[name].did);
|
|
223
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
224
|
+
remove = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
remove = true;
|
|
229
|
+
}
|
|
230
|
+
if (remove) {
|
|
231
|
+
delete wallet.ids[name];
|
|
232
|
+
idsRemoved++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const id of Object.values(wallet.ids)) {
|
|
236
|
+
if (id.owned) {
|
|
237
|
+
for (let i = 0; i < id.owned.length; i++) {
|
|
238
|
+
let remove = false;
|
|
239
|
+
try {
|
|
240
|
+
const doc = await this.resolveDID(id.owned[i]);
|
|
241
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
242
|
+
remove = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
remove = true;
|
|
247
|
+
}
|
|
248
|
+
if (remove) {
|
|
249
|
+
id.owned.splice(i, 1);
|
|
250
|
+
i--;
|
|
251
|
+
ownedRemoved++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (id.held) {
|
|
256
|
+
for (let i = 0; i < id.held.length; i++) {
|
|
257
|
+
let remove = false;
|
|
258
|
+
try {
|
|
259
|
+
const doc = await this.resolveDID(id.held[i]);
|
|
260
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
261
|
+
remove = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
remove = true;
|
|
266
|
+
}
|
|
267
|
+
if (remove) {
|
|
268
|
+
id.held.splice(i, 1);
|
|
269
|
+
i--;
|
|
270
|
+
heldRemoved++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (wallet.names) {
|
|
276
|
+
for (const name of Object.keys(wallet.names)) {
|
|
277
|
+
let remove = false;
|
|
278
|
+
try {
|
|
279
|
+
const doc = await this.resolveDID(wallet.names[name]);
|
|
280
|
+
if (doc.didDocumentMetadata?.deactivated) {
|
|
281
|
+
remove = true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
remove = true;
|
|
286
|
+
}
|
|
287
|
+
if (remove) {
|
|
288
|
+
delete wallet.names[name];
|
|
289
|
+
namesRemoved++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
return { idsRemoved, ownedRemoved, heldRemoved, namesRemoved };
|
|
295
|
+
}
|
|
296
|
+
async resolveSeedBank() {
|
|
297
|
+
const keypair = await this.hdKeyPair();
|
|
298
|
+
const operation = {
|
|
299
|
+
type: "create",
|
|
300
|
+
created: new Date(0).toISOString(),
|
|
301
|
+
registration: {
|
|
302
|
+
version: 1,
|
|
303
|
+
type: "agent",
|
|
304
|
+
registry: this.defaultRegistry,
|
|
305
|
+
},
|
|
306
|
+
publicJwk: keypair.publicJwk,
|
|
307
|
+
};
|
|
308
|
+
const msgHash = this.cipher.hashJSON(operation);
|
|
309
|
+
const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
|
|
310
|
+
const signed = {
|
|
311
|
+
...operation,
|
|
312
|
+
signature: {
|
|
313
|
+
signed: new Date(0).toISOString(),
|
|
314
|
+
hash: msgHash,
|
|
315
|
+
value: signature
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const did = await this.gatekeeper.createDID(signed);
|
|
319
|
+
return this.gatekeeper.resolveDID(did);
|
|
320
|
+
}
|
|
321
|
+
async updateSeedBank(doc) {
|
|
322
|
+
const keypair = await this.hdKeyPair();
|
|
323
|
+
const did = doc.didDocument?.id;
|
|
324
|
+
if (!did) {
|
|
325
|
+
throw new InvalidParameterError('seed bank missing DID');
|
|
326
|
+
}
|
|
327
|
+
const current = await this.gatekeeper.resolveDID(did);
|
|
328
|
+
const previd = current.didDocumentMetadata?.versionId;
|
|
329
|
+
const operation = {
|
|
330
|
+
type: "update",
|
|
331
|
+
did,
|
|
332
|
+
previd,
|
|
333
|
+
doc,
|
|
334
|
+
};
|
|
335
|
+
const msgHash = this.cipher.hashJSON(operation);
|
|
336
|
+
const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
|
|
337
|
+
const signed = {
|
|
338
|
+
...operation,
|
|
339
|
+
signature: {
|
|
340
|
+
signer: did,
|
|
341
|
+
signed: new Date().toISOString(),
|
|
342
|
+
hash: msgHash,
|
|
343
|
+
value: signature,
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
return await this.gatekeeper.updateDID(signed);
|
|
347
|
+
}
|
|
348
|
+
async backupWallet(registry = this.defaultRegistry, wallet) {
|
|
349
|
+
if (!wallet) {
|
|
350
|
+
wallet = await this.loadWallet();
|
|
351
|
+
}
|
|
352
|
+
const keypair = await this.hdKeyPair();
|
|
353
|
+
const seedBank = await this.resolveSeedBank();
|
|
354
|
+
const msg = JSON.stringify(wallet);
|
|
355
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
|
|
356
|
+
const operation = {
|
|
357
|
+
type: "create",
|
|
358
|
+
created: new Date().toISOString(),
|
|
359
|
+
registration: {
|
|
360
|
+
version: 1,
|
|
361
|
+
type: "asset",
|
|
362
|
+
registry: registry,
|
|
363
|
+
},
|
|
364
|
+
controller: seedBank.didDocument?.id,
|
|
365
|
+
data: { backup: backup },
|
|
366
|
+
};
|
|
367
|
+
const msgHash = this.cipher.hashJSON(operation);
|
|
368
|
+
const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
|
|
369
|
+
const signed = {
|
|
370
|
+
...operation,
|
|
371
|
+
signature: {
|
|
372
|
+
signer: seedBank.didDocument?.id,
|
|
373
|
+
signed: new Date().toISOString(),
|
|
374
|
+
hash: msgHash,
|
|
375
|
+
value: signature,
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const backupDID = await this.gatekeeper.createDID(signed);
|
|
379
|
+
if (seedBank.didDocumentData && typeof seedBank.didDocumentData === 'object' && !Array.isArray(seedBank.didDocumentData)) {
|
|
380
|
+
const data = seedBank.didDocumentData;
|
|
381
|
+
data.wallet = backupDID;
|
|
382
|
+
await this.updateSeedBank(seedBank);
|
|
383
|
+
}
|
|
384
|
+
return backupDID;
|
|
385
|
+
}
|
|
386
|
+
async recoverWallet(did) {
|
|
387
|
+
try {
|
|
388
|
+
if (!did) {
|
|
389
|
+
const seedBank = await this.resolveSeedBank();
|
|
390
|
+
if (seedBank.didDocumentData && typeof seedBank.didDocumentData === 'object' && !Array.isArray(seedBank.didDocumentData)) {
|
|
391
|
+
const data = seedBank.didDocumentData;
|
|
392
|
+
did = data.wallet;
|
|
393
|
+
}
|
|
394
|
+
if (!did) {
|
|
395
|
+
throw new InvalidParameterError('No backup DID found');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const keypair = await this.hdKeyPair();
|
|
399
|
+
const data = await this.resolveAsset(did);
|
|
400
|
+
if (!data) {
|
|
401
|
+
throw new InvalidParameterError('No asset data found');
|
|
402
|
+
}
|
|
403
|
+
const castData = data;
|
|
404
|
+
if (typeof castData.backup !== 'string') {
|
|
405
|
+
throw new InvalidParameterError('Asset "backup" is missing or not a string');
|
|
406
|
+
}
|
|
407
|
+
const backup = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, castData.backup);
|
|
408
|
+
let wallet = JSON.parse(backup);
|
|
409
|
+
if (isWalletFile(wallet)) {
|
|
410
|
+
const mnemonic = await this.decryptMnemonic();
|
|
411
|
+
// Backup might have a different mnemonic passphase so re-encrypt
|
|
412
|
+
wallet.seed.mnemonicEnc = await encMnemonic(mnemonic, this.passphrase);
|
|
413
|
+
}
|
|
414
|
+
await this.mutateWallet(async (current) => {
|
|
415
|
+
// Clear all existing properties from the current wallet
|
|
416
|
+
// This ensures a clean slate before restoring the recovered wallet
|
|
417
|
+
for (const k in current) {
|
|
418
|
+
delete current[k];
|
|
419
|
+
}
|
|
420
|
+
// Upgrade the recovered wallet to the latest version if needed
|
|
421
|
+
wallet = await this.upgradeWallet(wallet);
|
|
422
|
+
// Decrypt the wallet if needed
|
|
423
|
+
wallet = isWalletEncFile(wallet) ? await this.decryptWalletFromStorage(wallet) : wallet;
|
|
424
|
+
// Copy all properties from the recovered wallet into the cleared current wallet
|
|
425
|
+
// This effectively replaces the current wallet with the recovered one
|
|
426
|
+
Object.assign(current, wallet);
|
|
427
|
+
});
|
|
428
|
+
return this.loadWallet();
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
// If we can't recover the wallet, just return the current one
|
|
432
|
+
return this.loadWallet();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async listIds() {
|
|
436
|
+
const wallet = await this.loadWallet();
|
|
437
|
+
return Object.keys(wallet.ids);
|
|
438
|
+
}
|
|
439
|
+
async getCurrentId() {
|
|
440
|
+
const wallet = await this.loadWallet();
|
|
441
|
+
return wallet.current;
|
|
442
|
+
}
|
|
443
|
+
async setCurrentId(name) {
|
|
444
|
+
await this.mutateWallet((wallet) => {
|
|
445
|
+
if (!(name in wallet.ids)) {
|
|
446
|
+
throw new UnknownIDError();
|
|
447
|
+
}
|
|
448
|
+
wallet.current = name;
|
|
449
|
+
});
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
didMatch(did1, did2) {
|
|
453
|
+
const suffix1 = did1.split(':').pop();
|
|
454
|
+
const suffix2 = did2.split(':').pop();
|
|
455
|
+
return (suffix1 === suffix2);
|
|
456
|
+
}
|
|
457
|
+
async fetchIdInfo(id, wallet) {
|
|
458
|
+
// Callers should pass in the wallet if they are going to modify and save it later
|
|
459
|
+
if (!wallet) {
|
|
460
|
+
wallet = await this.loadWallet();
|
|
461
|
+
}
|
|
462
|
+
let idInfo = null;
|
|
463
|
+
if (id) {
|
|
464
|
+
if (id.startsWith('did')) {
|
|
465
|
+
for (const name of Object.keys(wallet.ids)) {
|
|
466
|
+
const info = wallet.ids[name];
|
|
467
|
+
if (this.didMatch(id, info.did)) {
|
|
468
|
+
idInfo = info;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
idInfo = wallet.ids[id];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
if (!wallet.current) {
|
|
479
|
+
throw new KeymasterError('No current ID');
|
|
480
|
+
}
|
|
481
|
+
idInfo = wallet.ids[wallet.current];
|
|
482
|
+
}
|
|
483
|
+
if (!idInfo) {
|
|
484
|
+
throw new UnknownIDError();
|
|
485
|
+
}
|
|
486
|
+
return idInfo;
|
|
487
|
+
}
|
|
488
|
+
async hdKeyPair() {
|
|
489
|
+
const wallet = await this.loadWallet();
|
|
490
|
+
const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
|
|
491
|
+
return this.cipher.generateJwk(hdkey.privateKey);
|
|
492
|
+
}
|
|
493
|
+
getPublicKeyJwk(doc) {
|
|
494
|
+
// TBD Return the right public key, not just the first one
|
|
495
|
+
if (!doc.didDocument) {
|
|
496
|
+
throw new KeymasterError('Missing didDocument.');
|
|
497
|
+
}
|
|
498
|
+
const verificationMethods = doc.didDocument.verificationMethod;
|
|
499
|
+
if (!verificationMethods || verificationMethods.length === 0) {
|
|
500
|
+
throw new KeymasterError('The DID document does not contain any verification methods.');
|
|
501
|
+
}
|
|
502
|
+
const publicKeyJwk = verificationMethods[0].publicKeyJwk;
|
|
503
|
+
if (!publicKeyJwk) {
|
|
504
|
+
throw new KeymasterError('The publicKeyJwk is missing in the first verification method.');
|
|
505
|
+
}
|
|
506
|
+
return publicKeyJwk;
|
|
507
|
+
}
|
|
508
|
+
async fetchKeyPair(name) {
|
|
509
|
+
const wallet = await this.loadWallet();
|
|
510
|
+
const id = await this.fetchIdInfo(name);
|
|
511
|
+
const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
|
|
512
|
+
const doc = await this.resolveDID(id.did, { confirm: true });
|
|
513
|
+
const confirmedPublicKeyJwk = this.getPublicKeyJwk(doc);
|
|
514
|
+
for (let i = id.index; i >= 0; i--) {
|
|
515
|
+
const path = `m/44'/0'/${id.account}'/0/${i}`;
|
|
516
|
+
const didkey = hdkey.derive(path);
|
|
517
|
+
const keypair = this.cipher.generateJwk(didkey.privateKey);
|
|
518
|
+
if (keypair.publicJwk.x === confirmedPublicKeyJwk.x &&
|
|
519
|
+
keypair.publicJwk.y === confirmedPublicKeyJwk.y) {
|
|
520
|
+
return keypair;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
async createAsset(data, options = {}) {
|
|
526
|
+
let { registry = this.defaultRegistry, controller, validUntil, name } = options;
|
|
527
|
+
if (validUntil) {
|
|
528
|
+
const validate = new Date(validUntil);
|
|
529
|
+
if (isNaN(validate.getTime())) {
|
|
530
|
+
throw new InvalidParameterError('options.validUntil');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (name) {
|
|
534
|
+
const wallet = await this.loadWallet();
|
|
535
|
+
this.validateName(name, wallet);
|
|
536
|
+
}
|
|
537
|
+
if (!data) {
|
|
538
|
+
throw new InvalidParameterError('data');
|
|
539
|
+
}
|
|
540
|
+
const id = await this.fetchIdInfo(controller);
|
|
541
|
+
const block = await this.gatekeeper.getBlock(registry);
|
|
542
|
+
const blockid = block?.hash;
|
|
543
|
+
const operation = {
|
|
544
|
+
type: "create",
|
|
545
|
+
created: new Date().toISOString(),
|
|
546
|
+
blockid,
|
|
547
|
+
registration: {
|
|
548
|
+
version: 1,
|
|
549
|
+
type: "asset",
|
|
550
|
+
registry,
|
|
551
|
+
validUntil
|
|
552
|
+
},
|
|
553
|
+
controller: id.did,
|
|
554
|
+
data,
|
|
555
|
+
};
|
|
556
|
+
const signed = await this.addSignature(operation, controller);
|
|
557
|
+
const did = await this.gatekeeper.createDID(signed);
|
|
558
|
+
// Keep assets that will be garbage-collected out of the owned list
|
|
559
|
+
if (!validUntil) {
|
|
560
|
+
await this.addToOwned(did);
|
|
561
|
+
}
|
|
562
|
+
if (name) {
|
|
563
|
+
await this.addName(name, did);
|
|
564
|
+
}
|
|
565
|
+
return did;
|
|
566
|
+
}
|
|
567
|
+
async cloneAsset(id, options = {}) {
|
|
568
|
+
const assetDoc = await this.resolveDID(id);
|
|
569
|
+
if (assetDoc.didDocumentRegistration?.type !== 'asset') {
|
|
570
|
+
throw new InvalidParameterError('id');
|
|
571
|
+
}
|
|
572
|
+
const assetData = assetDoc.didDocumentData || {};
|
|
573
|
+
const cloneData = { ...assetData, cloned: assetDoc.didDocument.id };
|
|
574
|
+
return this.createAsset(cloneData, options);
|
|
575
|
+
}
|
|
576
|
+
async generateImageAsset(buffer) {
|
|
577
|
+
let metadata;
|
|
578
|
+
try {
|
|
579
|
+
metadata = imageSize(buffer);
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
throw new InvalidParameterError('buffer');
|
|
583
|
+
}
|
|
584
|
+
const cid = await this.gatekeeper.addData(buffer);
|
|
585
|
+
const image = {
|
|
586
|
+
cid,
|
|
587
|
+
bytes: buffer.length,
|
|
588
|
+
...metadata,
|
|
589
|
+
type: `image/${metadata.type}`
|
|
590
|
+
};
|
|
591
|
+
return image;
|
|
592
|
+
}
|
|
593
|
+
async createImage(buffer, options = {}) {
|
|
594
|
+
const image = await this.generateImageAsset(buffer);
|
|
595
|
+
return this.createAsset({ image }, options);
|
|
596
|
+
}
|
|
597
|
+
async updateImage(id, buffer) {
|
|
598
|
+
const image = await this.generateImageAsset(buffer);
|
|
599
|
+
return this.updateAsset(id, { image });
|
|
600
|
+
}
|
|
601
|
+
async getImage(id) {
|
|
602
|
+
const asset = await this.resolveAsset(id);
|
|
603
|
+
const image = asset.image;
|
|
604
|
+
if (!image || !image.cid) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const buffer = await this.gatekeeper.getData(image.cid);
|
|
608
|
+
if (buffer) {
|
|
609
|
+
image.data = buffer;
|
|
610
|
+
}
|
|
611
|
+
return image;
|
|
612
|
+
}
|
|
613
|
+
async testImage(id) {
|
|
614
|
+
try {
|
|
615
|
+
const image = await this.getImage(id);
|
|
616
|
+
return image !== null;
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async getMimeType(buffer) {
|
|
623
|
+
// Try magic number detection
|
|
624
|
+
const result = await fileTypeFromBuffer(buffer);
|
|
625
|
+
if (result)
|
|
626
|
+
return result.mime;
|
|
627
|
+
// Convert to UTF-8 string if decodable
|
|
628
|
+
const text = buffer.toString('utf8');
|
|
629
|
+
// Check for JSON
|
|
630
|
+
try {
|
|
631
|
+
JSON.parse(text);
|
|
632
|
+
return 'application/json';
|
|
633
|
+
}
|
|
634
|
+
catch { }
|
|
635
|
+
// Default to plain text if printable ASCII
|
|
636
|
+
// eslint-disable-next-line
|
|
637
|
+
if (/^[\x09\x0A\x0D\x20-\x7E]*$/.test(text.replace(/\n/g, ''))) {
|
|
638
|
+
return 'text/plain';
|
|
639
|
+
}
|
|
640
|
+
// Fallback
|
|
641
|
+
return 'application/octet-stream';
|
|
642
|
+
}
|
|
643
|
+
async generateFileAsset(filename, buffer) {
|
|
644
|
+
const cid = await this.gatekeeper.addData(buffer);
|
|
645
|
+
const type = await this.getMimeType(buffer);
|
|
646
|
+
const file = {
|
|
647
|
+
cid,
|
|
648
|
+
filename,
|
|
649
|
+
type,
|
|
650
|
+
bytes: buffer.length,
|
|
651
|
+
};
|
|
652
|
+
return file;
|
|
653
|
+
}
|
|
654
|
+
async createDocument(buffer, options = {}) {
|
|
655
|
+
const filename = options.filename || 'document';
|
|
656
|
+
const document = await this.generateFileAsset(filename, buffer);
|
|
657
|
+
return this.createAsset({ document }, options);
|
|
658
|
+
}
|
|
659
|
+
async updateDocument(id, buffer, options = {}) {
|
|
660
|
+
const filename = options.filename || 'document';
|
|
661
|
+
const document = await this.generateFileAsset(filename, buffer);
|
|
662
|
+
return this.updateAsset(id, { document });
|
|
663
|
+
}
|
|
664
|
+
async getDocument(id) {
|
|
665
|
+
const asset = await this.resolveAsset(id);
|
|
666
|
+
return asset.document ?? null;
|
|
667
|
+
}
|
|
668
|
+
async testDocument(id) {
|
|
669
|
+
try {
|
|
670
|
+
const document = await this.getDocument(id);
|
|
671
|
+
return document !== null;
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async encryptMessage(msg, receiver, options = {}) {
|
|
678
|
+
const { encryptForSender = true, includeHash = false, } = options;
|
|
679
|
+
const id = await this.fetchIdInfo();
|
|
680
|
+
const senderKeypair = await this.fetchKeyPair();
|
|
681
|
+
if (!senderKeypair) {
|
|
682
|
+
throw new KeymasterError('No valid sender keypair');
|
|
683
|
+
}
|
|
684
|
+
const doc = await this.resolveDID(receiver, { confirm: true });
|
|
685
|
+
const receivePublicJwk = this.getPublicKeyJwk(doc);
|
|
686
|
+
const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg) : null;
|
|
687
|
+
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
|
|
688
|
+
const cipher_hash = includeHash ? this.cipher.hashMessage(msg) : null;
|
|
689
|
+
const encrypted = {
|
|
690
|
+
sender: id.did,
|
|
691
|
+
created: new Date().toISOString(),
|
|
692
|
+
cipher_hash,
|
|
693
|
+
cipher_sender,
|
|
694
|
+
cipher_receiver,
|
|
695
|
+
};
|
|
696
|
+
return await this.createAsset({ encrypted }, options);
|
|
697
|
+
}
|
|
698
|
+
async decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext) {
|
|
699
|
+
const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
|
|
700
|
+
// Try all private keys for this ID, starting with the most recent and working backward
|
|
701
|
+
let index = id.index;
|
|
702
|
+
while (index >= 0) {
|
|
703
|
+
const path = `m/44'/0'/${id.account}'/0/${index}`;
|
|
704
|
+
const didkey = hdkey.derive(path);
|
|
705
|
+
const receiverKeypair = this.cipher.generateJwk(didkey.privateKey);
|
|
706
|
+
try {
|
|
707
|
+
return this.cipher.decryptMessage(senderPublicJwk, receiverKeypair.privateJwk, ciphertext);
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
index -= 1;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
throw new KeymasterError("ID can't decrypt ciphertext");
|
|
714
|
+
}
|
|
715
|
+
async decryptMessage(did) {
|
|
716
|
+
const wallet = await this.loadWallet();
|
|
717
|
+
const id = await this.fetchIdInfo();
|
|
718
|
+
const asset = await this.resolveAsset(did);
|
|
719
|
+
if (!asset) {
|
|
720
|
+
throw new InvalidParameterError('did not encrypted');
|
|
721
|
+
}
|
|
722
|
+
const castAsset = asset;
|
|
723
|
+
if (!castAsset.encrypted && !castAsset.cipher_hash) {
|
|
724
|
+
throw new InvalidParameterError('did not encrypted');
|
|
725
|
+
}
|
|
726
|
+
const crypt = (castAsset.encrypted ? castAsset.encrypted : castAsset);
|
|
727
|
+
const doc = await this.resolveDID(crypt.sender, { confirm: true, versionTime: crypt.created });
|
|
728
|
+
const senderPublicJwk = this.getPublicKeyJwk(doc);
|
|
729
|
+
const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
|
|
730
|
+
return await this.decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext);
|
|
731
|
+
}
|
|
732
|
+
async encryptJSON(json, did, options = {}) {
|
|
733
|
+
const plaintext = JSON.stringify(json);
|
|
734
|
+
return this.encryptMessage(plaintext, did, options);
|
|
735
|
+
}
|
|
736
|
+
async decryptJSON(did) {
|
|
737
|
+
const plaintext = await this.decryptMessage(did);
|
|
738
|
+
try {
|
|
739
|
+
return JSON.parse(plaintext);
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
throw new InvalidParameterError('did not encrypted JSON');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async addSignature(obj, controller) {
|
|
746
|
+
if (obj == null) {
|
|
747
|
+
throw new InvalidParameterError('obj');
|
|
748
|
+
}
|
|
749
|
+
// Fetches current ID if name is missing
|
|
750
|
+
const id = await this.fetchIdInfo(controller);
|
|
751
|
+
const keypair = await this.fetchKeyPair(controller);
|
|
752
|
+
if (!keypair) {
|
|
753
|
+
throw new KeymasterError('addSignature: no keypair');
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
const msgHash = this.cipher.hashJSON(obj);
|
|
757
|
+
const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
|
|
758
|
+
return {
|
|
759
|
+
...obj,
|
|
760
|
+
signature: {
|
|
761
|
+
signer: id.did,
|
|
762
|
+
signed: new Date().toISOString(),
|
|
763
|
+
hash: msgHash,
|
|
764
|
+
value: signature,
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
throw new InvalidParameterError('obj');
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async verifySignature(obj) {
|
|
773
|
+
if (!obj?.signature) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
const { signature } = obj;
|
|
777
|
+
if (!signature.signer) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
const jsonCopy = JSON.parse(JSON.stringify(obj));
|
|
781
|
+
delete jsonCopy.signature;
|
|
782
|
+
const msgHash = this.cipher.hashJSON(jsonCopy);
|
|
783
|
+
if (signature.hash && signature.hash !== msgHash) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
const doc = await this.resolveDID(signature.signer, { versionTime: signature.signed });
|
|
787
|
+
const publicJwk = this.getPublicKeyJwk(doc);
|
|
788
|
+
try {
|
|
789
|
+
return this.cipher.verifySig(msgHash, signature.value, publicJwk);
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
async updateDID(id, doc) {
|
|
796
|
+
const did = await this.lookupDID(id);
|
|
797
|
+
const current = await this.resolveDID(did);
|
|
798
|
+
const previd = current.didDocumentMetadata?.versionId;
|
|
799
|
+
// Strip metadata fields from the update doc
|
|
800
|
+
delete doc.didDocumentMetadata;
|
|
801
|
+
delete doc.didResolutionMetadata;
|
|
802
|
+
const block = await this.gatekeeper.getBlock(current.didDocumentRegistration.registry);
|
|
803
|
+
const blockid = block?.hash;
|
|
804
|
+
const operation = {
|
|
805
|
+
type: "update",
|
|
806
|
+
did,
|
|
807
|
+
previd,
|
|
808
|
+
blockid,
|
|
809
|
+
doc,
|
|
810
|
+
};
|
|
811
|
+
let controller;
|
|
812
|
+
if (current.didDocumentRegistration?.type === 'agent') {
|
|
813
|
+
controller = current.didDocument?.id;
|
|
814
|
+
}
|
|
815
|
+
else if (current.didDocumentRegistration?.type === 'asset') {
|
|
816
|
+
controller = current.didDocument?.controller;
|
|
817
|
+
}
|
|
818
|
+
const signed = await this.addSignature(operation, controller);
|
|
819
|
+
return this.gatekeeper.updateDID(signed);
|
|
820
|
+
}
|
|
821
|
+
async revokeDID(id) {
|
|
822
|
+
const did = await this.lookupDID(id);
|
|
823
|
+
const current = await this.resolveDID(did);
|
|
824
|
+
const previd = current.didDocumentMetadata?.versionId;
|
|
825
|
+
const block = await this.gatekeeper.getBlock(current.didDocumentRegistration.registry);
|
|
826
|
+
const blockid = block?.hash;
|
|
827
|
+
const operation = {
|
|
828
|
+
type: "delete",
|
|
829
|
+
did,
|
|
830
|
+
previd,
|
|
831
|
+
blockid
|
|
832
|
+
};
|
|
833
|
+
let controller;
|
|
834
|
+
if (current.didDocumentRegistration?.type === 'agent') {
|
|
835
|
+
controller = current.didDocument?.id;
|
|
836
|
+
}
|
|
837
|
+
else if (current.didDocumentRegistration?.type === 'asset') {
|
|
838
|
+
controller = current.didDocument?.controller;
|
|
839
|
+
}
|
|
840
|
+
const signed = await this.addSignature(operation, controller);
|
|
841
|
+
const ok = await this.gatekeeper.deleteDID(signed);
|
|
842
|
+
if (ok && current.didDocument?.controller) {
|
|
843
|
+
await this.removeFromOwned(did, current.didDocument.controller);
|
|
844
|
+
}
|
|
845
|
+
return ok;
|
|
846
|
+
}
|
|
847
|
+
async addToOwned(did, owner) {
|
|
848
|
+
await this.mutateWallet(async (wallet) => {
|
|
849
|
+
const id = await this.fetchIdInfo(owner, wallet);
|
|
850
|
+
const owned = new Set(id.owned);
|
|
851
|
+
owned.add(did);
|
|
852
|
+
id.owned = Array.from(owned);
|
|
853
|
+
});
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
async removeFromOwned(did, owner) {
|
|
857
|
+
let ownerFound = false;
|
|
858
|
+
await this.mutateWallet(async (wallet) => {
|
|
859
|
+
const id = await this.fetchIdInfo(owner, wallet);
|
|
860
|
+
if (!id.owned) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
ownerFound = true;
|
|
864
|
+
id.owned = id.owned.filter(item => item !== did);
|
|
865
|
+
});
|
|
866
|
+
return ownerFound;
|
|
867
|
+
}
|
|
868
|
+
async addToHeld(did) {
|
|
869
|
+
await this.mutateWallet((wallet) => {
|
|
870
|
+
const id = wallet.ids[wallet.current];
|
|
871
|
+
const held = new Set(id.held);
|
|
872
|
+
held.add(did);
|
|
873
|
+
id.held = Array.from(held);
|
|
874
|
+
});
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
async removeFromHeld(did) {
|
|
878
|
+
let changed = false;
|
|
879
|
+
await this.mutateWallet((wallet) => {
|
|
880
|
+
const id = wallet.ids[wallet.current];
|
|
881
|
+
const held = new Set(id.held);
|
|
882
|
+
if (held.delete(did)) {
|
|
883
|
+
id.held = Array.from(held);
|
|
884
|
+
changed = true;
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
return changed;
|
|
888
|
+
}
|
|
889
|
+
async lookupDID(name) {
|
|
890
|
+
try {
|
|
891
|
+
if (name.startsWith('did:')) {
|
|
892
|
+
return name;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
throw new InvalidDIDError();
|
|
897
|
+
}
|
|
898
|
+
const wallet = await this.loadWallet();
|
|
899
|
+
if (wallet.names && name in wallet.names) {
|
|
900
|
+
return wallet.names[name];
|
|
901
|
+
}
|
|
902
|
+
if (wallet.ids && name in wallet.ids) {
|
|
903
|
+
return wallet.ids[name].did;
|
|
904
|
+
}
|
|
905
|
+
throw new UnknownIDError();
|
|
906
|
+
}
|
|
907
|
+
async resolveDID(did, options) {
|
|
908
|
+
const actualDid = await this.lookupDID(did);
|
|
909
|
+
const docs = await this.gatekeeper.resolveDID(actualDid, options);
|
|
910
|
+
if (docs.didResolutionMetadata?.error) {
|
|
911
|
+
if (docs.didResolutionMetadata.error === 'notFound') {
|
|
912
|
+
throw new InvalidDIDError('unknown');
|
|
913
|
+
}
|
|
914
|
+
if (docs.didResolutionMetadata.error === 'invalidDid') {
|
|
915
|
+
throw new InvalidDIDError('bad format');
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const controller = docs.didDocument?.controller || docs.didDocument?.id;
|
|
919
|
+
const isOwned = await this.idInWallet(controller);
|
|
920
|
+
// Augment the DID document metadata with the DID ownership status
|
|
921
|
+
docs.didDocumentMetadata = {
|
|
922
|
+
...docs.didDocumentMetadata,
|
|
923
|
+
isOwned,
|
|
924
|
+
};
|
|
925
|
+
return docs;
|
|
926
|
+
}
|
|
927
|
+
async idInWallet(did) {
|
|
928
|
+
try {
|
|
929
|
+
await this.fetchIdInfo(did);
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
async resolveAsset(did, options) {
|
|
937
|
+
const doc = await this.resolveDID(did, options);
|
|
938
|
+
if (!doc?.didDocument?.controller || !doc?.didDocumentData || doc.didDocumentMetadata?.deactivated) {
|
|
939
|
+
return {};
|
|
940
|
+
}
|
|
941
|
+
return doc.didDocumentData;
|
|
942
|
+
}
|
|
943
|
+
async updateAsset(did, data) {
|
|
944
|
+
const doc = await this.resolveDID(did);
|
|
945
|
+
const currentData = doc.didDocumentData || {};
|
|
946
|
+
const updatedData = {
|
|
947
|
+
...currentData,
|
|
948
|
+
...data
|
|
949
|
+
};
|
|
950
|
+
return this.updateDID(did, { didDocumentData: updatedData });
|
|
951
|
+
}
|
|
952
|
+
async transferAsset(id, controller) {
|
|
953
|
+
const assetDoc = await this.resolveDID(id);
|
|
954
|
+
if (assetDoc.didDocumentRegistration?.type !== 'asset') {
|
|
955
|
+
throw new InvalidParameterError('id');
|
|
956
|
+
}
|
|
957
|
+
if (assetDoc.didDocument.controller === controller) {
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
const agentDoc = await this.resolveDID(controller);
|
|
961
|
+
if (agentDoc.didDocumentRegistration?.type !== 'agent') {
|
|
962
|
+
throw new InvalidParameterError('controller');
|
|
963
|
+
}
|
|
964
|
+
const assetDID = assetDoc.didDocument.id;
|
|
965
|
+
const prevOwner = assetDoc.didDocument.controller;
|
|
966
|
+
const updatedDidDocument = {
|
|
967
|
+
...assetDoc.didDocument,
|
|
968
|
+
controller: agentDoc.didDocument.id,
|
|
969
|
+
};
|
|
970
|
+
const ok = await this.updateDID(id, { didDocument: updatedDidDocument });
|
|
971
|
+
if (ok && assetDID && prevOwner) {
|
|
972
|
+
await this.removeFromOwned(assetDID, prevOwner);
|
|
973
|
+
try {
|
|
974
|
+
await this.addToOwned(assetDID, controller);
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
// New controller is not in our wallet
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return ok;
|
|
981
|
+
}
|
|
982
|
+
async listAssets(owner) {
|
|
983
|
+
const id = await this.fetchIdInfo(owner);
|
|
984
|
+
return id.owned || [];
|
|
985
|
+
}
|
|
986
|
+
validateName(name, wallet) {
|
|
987
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
988
|
+
throw new InvalidParameterError('name must be a non-empty string');
|
|
989
|
+
}
|
|
990
|
+
name = name.trim(); // Remove leading/trailing whitespace
|
|
991
|
+
if (name.length > this.maxNameLength) {
|
|
992
|
+
throw new InvalidParameterError(`name too long`);
|
|
993
|
+
}
|
|
994
|
+
if (/[^\P{Cc}]/u.test(name)) {
|
|
995
|
+
throw new InvalidParameterError('name contains unprintable characters');
|
|
996
|
+
}
|
|
997
|
+
const alreadyUsedError = 'name already used';
|
|
998
|
+
if (wallet && wallet.names && name in wallet.names) {
|
|
999
|
+
throw new InvalidParameterError(alreadyUsedError);
|
|
1000
|
+
}
|
|
1001
|
+
if (wallet && wallet.ids && name in wallet.ids) {
|
|
1002
|
+
throw new InvalidParameterError(alreadyUsedError);
|
|
1003
|
+
}
|
|
1004
|
+
return name;
|
|
1005
|
+
}
|
|
1006
|
+
async createId(name, options = {}) {
|
|
1007
|
+
let did = '';
|
|
1008
|
+
await this.mutateWallet(async (wallet) => {
|
|
1009
|
+
const account = wallet.counter;
|
|
1010
|
+
const index = 0;
|
|
1011
|
+
const signed = await this.createIdOperation(name, account, options);
|
|
1012
|
+
did = await this.gatekeeper.createDID(signed);
|
|
1013
|
+
wallet.ids[name] = { did, account, index };
|
|
1014
|
+
wallet.counter += 1;
|
|
1015
|
+
wallet.current = name;
|
|
1016
|
+
});
|
|
1017
|
+
return did;
|
|
1018
|
+
}
|
|
1019
|
+
async createIdOperation(name, account = 0, options = {}) {
|
|
1020
|
+
const { registry = this.defaultRegistry } = options;
|
|
1021
|
+
const wallet = await this.loadWallet();
|
|
1022
|
+
name = this.validateName(name, wallet);
|
|
1023
|
+
const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
|
|
1024
|
+
const path = `m/44'/0'/${account}'/0/0`;
|
|
1025
|
+
const didkey = hdkey.derive(path);
|
|
1026
|
+
const keypair = this.cipher.generateJwk(didkey.privateKey);
|
|
1027
|
+
const block = await this.gatekeeper.getBlock(registry);
|
|
1028
|
+
const blockid = block?.hash;
|
|
1029
|
+
const operation = {
|
|
1030
|
+
type: 'create',
|
|
1031
|
+
created: new Date().toISOString(),
|
|
1032
|
+
blockid,
|
|
1033
|
+
registration: {
|
|
1034
|
+
version: 1,
|
|
1035
|
+
type: 'agent',
|
|
1036
|
+
registry
|
|
1037
|
+
},
|
|
1038
|
+
publicJwk: keypair.publicJwk,
|
|
1039
|
+
};
|
|
1040
|
+
const msgHash = this.cipher.hashJSON(operation);
|
|
1041
|
+
const signature = this.cipher.signHash(msgHash, keypair.privateJwk);
|
|
1042
|
+
const signed = {
|
|
1043
|
+
...operation,
|
|
1044
|
+
signature: {
|
|
1045
|
+
signed: new Date().toISOString(),
|
|
1046
|
+
hash: msgHash,
|
|
1047
|
+
value: signature
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
return signed;
|
|
1051
|
+
}
|
|
1052
|
+
async removeId(name) {
|
|
1053
|
+
await this.mutateWallet((wallet) => {
|
|
1054
|
+
if (!(name in wallet.ids)) {
|
|
1055
|
+
throw new UnknownIDError();
|
|
1056
|
+
}
|
|
1057
|
+
delete wallet.ids[name];
|
|
1058
|
+
if (wallet.current === name) {
|
|
1059
|
+
wallet.current = Object.keys(wallet.ids)[0] || '';
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
async renameId(id, name) {
|
|
1065
|
+
await this.mutateWallet((wallet) => {
|
|
1066
|
+
name = this.validateName(name);
|
|
1067
|
+
if (!(id in wallet.ids)) {
|
|
1068
|
+
throw new UnknownIDError();
|
|
1069
|
+
}
|
|
1070
|
+
if (name in wallet.ids) {
|
|
1071
|
+
throw new InvalidParameterError('name already used');
|
|
1072
|
+
}
|
|
1073
|
+
wallet.ids[name] = wallet.ids[id];
|
|
1074
|
+
delete wallet.ids[id];
|
|
1075
|
+
if (wallet.current && wallet.current === id) {
|
|
1076
|
+
wallet.current = name;
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
async backupId(id) {
|
|
1082
|
+
// Backs up current ID if id is not provided
|
|
1083
|
+
const wallet = await this.loadWallet();
|
|
1084
|
+
const name = id || wallet.current;
|
|
1085
|
+
if (!name) {
|
|
1086
|
+
throw new InvalidParameterError('no current ID');
|
|
1087
|
+
}
|
|
1088
|
+
const idInfo = await this.fetchIdInfo(name, wallet);
|
|
1089
|
+
const keypair = await this.hdKeyPair();
|
|
1090
|
+
const data = {
|
|
1091
|
+
name: name,
|
|
1092
|
+
id: idInfo,
|
|
1093
|
+
};
|
|
1094
|
+
const msg = JSON.stringify(data);
|
|
1095
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, keypair.privateJwk, msg);
|
|
1096
|
+
const doc = await this.resolveDID(idInfo.did);
|
|
1097
|
+
const registry = doc.didDocumentRegistration?.registry;
|
|
1098
|
+
if (!registry) {
|
|
1099
|
+
throw new InvalidParameterError('no registry found for agent DID');
|
|
1100
|
+
}
|
|
1101
|
+
const vaultDid = await this.createAsset({ backup: backup }, { registry, controller: name });
|
|
1102
|
+
if (doc.didDocumentData) {
|
|
1103
|
+
const currentData = doc.didDocumentData;
|
|
1104
|
+
const updatedData = { ...currentData, vault: vaultDid };
|
|
1105
|
+
return this.updateDID(name, { didDocumentData: updatedData });
|
|
1106
|
+
}
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
async recoverId(did) {
|
|
1110
|
+
try {
|
|
1111
|
+
const keypair = await this.hdKeyPair();
|
|
1112
|
+
const doc = await this.resolveDID(did);
|
|
1113
|
+
const docData = doc.didDocumentData;
|
|
1114
|
+
if (!docData.vault) {
|
|
1115
|
+
throw new InvalidDIDError('didDocumentData missing vault');
|
|
1116
|
+
}
|
|
1117
|
+
const vault = await this.resolveAsset(docData.vault);
|
|
1118
|
+
if (typeof vault.backup !== 'string') {
|
|
1119
|
+
throw new InvalidDIDError('backup not found in vault');
|
|
1120
|
+
}
|
|
1121
|
+
const backup = this.cipher.decryptMessage(keypair.publicJwk, keypair.privateJwk, vault.backup);
|
|
1122
|
+
const data = JSON.parse(backup);
|
|
1123
|
+
await this.mutateWallet((wallet) => {
|
|
1124
|
+
if (wallet.ids[data.name]) {
|
|
1125
|
+
throw new KeymasterError(`${data.name} already exists in wallet`);
|
|
1126
|
+
}
|
|
1127
|
+
wallet.ids[data.name] = data.id;
|
|
1128
|
+
wallet.current = data.name;
|
|
1129
|
+
wallet.counter += 1;
|
|
1130
|
+
});
|
|
1131
|
+
return data.name;
|
|
1132
|
+
}
|
|
1133
|
+
catch (error) {
|
|
1134
|
+
if (error.type === 'Keymaster') {
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
throw new InvalidDIDError();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
async rotateKeys() {
|
|
1143
|
+
let ok = false;
|
|
1144
|
+
await this.mutateWallet(async (wallet) => {
|
|
1145
|
+
const id = wallet.ids[wallet.current];
|
|
1146
|
+
const nextIndex = id.index + 1;
|
|
1147
|
+
const hdkey = await this.getHDKeyFromCacheOrMnemonic(wallet);
|
|
1148
|
+
const path = `m/44'/0'/${id.account}'/0/${nextIndex}`;
|
|
1149
|
+
const didkey = hdkey.derive(path);
|
|
1150
|
+
const keypair = this.cipher.generateJwk(didkey.privateKey);
|
|
1151
|
+
const doc = await this.resolveDID(id.did);
|
|
1152
|
+
if (!doc.didDocumentMetadata?.confirmed) {
|
|
1153
|
+
throw new KeymasterError('Cannot rotate keys');
|
|
1154
|
+
}
|
|
1155
|
+
if (!doc.didDocument?.verificationMethod) {
|
|
1156
|
+
throw new KeymasterError('DID Document missing verificationMethod');
|
|
1157
|
+
}
|
|
1158
|
+
const vmethod = { ...doc.didDocument.verificationMethod[0] };
|
|
1159
|
+
vmethod.id = `#key-${nextIndex + 1}`;
|
|
1160
|
+
vmethod.publicKeyJwk = keypair.publicJwk;
|
|
1161
|
+
const updatedDidDocument = {
|
|
1162
|
+
...doc.didDocument,
|
|
1163
|
+
verificationMethod: [vmethod],
|
|
1164
|
+
authentication: [vmethod.id],
|
|
1165
|
+
};
|
|
1166
|
+
ok = await this.updateDID(id.did, { didDocument: updatedDidDocument });
|
|
1167
|
+
if (!ok) {
|
|
1168
|
+
throw new KeymasterError('Cannot rotate keys');
|
|
1169
|
+
}
|
|
1170
|
+
id.index = nextIndex; // persist in same mutation
|
|
1171
|
+
});
|
|
1172
|
+
return ok;
|
|
1173
|
+
}
|
|
1174
|
+
async listNames(options = {}) {
|
|
1175
|
+
const { includeIDs = false } = options;
|
|
1176
|
+
const wallet = await this.loadWallet();
|
|
1177
|
+
const names = wallet.names || {};
|
|
1178
|
+
if (includeIDs) {
|
|
1179
|
+
for (const [name, id] of Object.entries(wallet.ids || {})) {
|
|
1180
|
+
names[name] = id.did;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return names;
|
|
1184
|
+
}
|
|
1185
|
+
async addName(name, did) {
|
|
1186
|
+
await this.mutateWallet((wallet) => {
|
|
1187
|
+
if (!wallet.names) {
|
|
1188
|
+
wallet.names = {};
|
|
1189
|
+
}
|
|
1190
|
+
name = this.validateName(name, wallet);
|
|
1191
|
+
wallet.names[name] = did;
|
|
1192
|
+
});
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
async getName(name) {
|
|
1196
|
+
const wallet = await this.loadWallet();
|
|
1197
|
+
if (wallet.names && name in wallet.names) {
|
|
1198
|
+
return wallet.names[name];
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
async removeName(name) {
|
|
1203
|
+
await this.mutateWallet((wallet) => {
|
|
1204
|
+
if (!wallet.names || !(name in wallet.names)) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
delete wallet.names[name];
|
|
1208
|
+
});
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
async testAgent(id) {
|
|
1212
|
+
const doc = await this.resolveDID(id);
|
|
1213
|
+
return doc.didDocumentRegistration?.type === 'agent';
|
|
1214
|
+
}
|
|
1215
|
+
async bindCredential(schemaId, subjectId, options = {}) {
|
|
1216
|
+
let { validFrom, validUntil, credential } = options;
|
|
1217
|
+
if (!validFrom) {
|
|
1218
|
+
validFrom = new Date().toISOString();
|
|
1219
|
+
}
|
|
1220
|
+
const id = await this.fetchIdInfo();
|
|
1221
|
+
const type = await this.lookupDID(schemaId);
|
|
1222
|
+
const subjectDID = await this.lookupDID(subjectId);
|
|
1223
|
+
if (!credential) {
|
|
1224
|
+
const schema = await this.getSchema(type);
|
|
1225
|
+
credential = this.generateSchema(schema);
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
"@context": [
|
|
1229
|
+
"https://www.w3.org/ns/credentials/v2",
|
|
1230
|
+
"https://www.w3.org/ns/credentials/examples/v2"
|
|
1231
|
+
],
|
|
1232
|
+
type: ["VerifiableCredential", type],
|
|
1233
|
+
issuer: id.did,
|
|
1234
|
+
validFrom,
|
|
1235
|
+
validUntil,
|
|
1236
|
+
credentialSubject: {
|
|
1237
|
+
id: subjectDID,
|
|
1238
|
+
},
|
|
1239
|
+
credential,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
async issueCredential(credential, options = {}) {
|
|
1243
|
+
const id = await this.fetchIdInfo();
|
|
1244
|
+
if (options.schema && options.subject) {
|
|
1245
|
+
credential = await this.bindCredential(options.schema, options.subject, { credential, ...options });
|
|
1246
|
+
}
|
|
1247
|
+
if (credential.issuer !== id.did) {
|
|
1248
|
+
throw new InvalidParameterError('credential.issuer');
|
|
1249
|
+
}
|
|
1250
|
+
const signed = await this.addSignature(credential);
|
|
1251
|
+
return this.encryptJSON(signed, credential.credentialSubject.id, { ...options, includeHash: true });
|
|
1252
|
+
}
|
|
1253
|
+
async sendCredential(did, options = {}) {
|
|
1254
|
+
const vc = await this.getCredential(did);
|
|
1255
|
+
if (!vc) {
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
const registry = this.ephemeralRegistry;
|
|
1259
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
|
|
1260
|
+
const message = {
|
|
1261
|
+
to: [vc.credentialSubject.id],
|
|
1262
|
+
dids: [did],
|
|
1263
|
+
};
|
|
1264
|
+
return this.createNotice(message, { registry, validUntil, ...options });
|
|
1265
|
+
}
|
|
1266
|
+
isVerifiableCredential(obj) {
|
|
1267
|
+
if (typeof obj !== 'object' || !obj) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
const vc = obj;
|
|
1271
|
+
return !(!Array.isArray(vc["@context"]) || !Array.isArray(vc.type) || !vc.issuer || !vc.credentialSubject);
|
|
1272
|
+
}
|
|
1273
|
+
async updateCredential(did, credential) {
|
|
1274
|
+
did = await this.lookupDID(did);
|
|
1275
|
+
const originalVC = await this.decryptJSON(did);
|
|
1276
|
+
if (!this.isVerifiableCredential(originalVC)) {
|
|
1277
|
+
throw new InvalidParameterError("did is not a credential");
|
|
1278
|
+
}
|
|
1279
|
+
if (!credential ||
|
|
1280
|
+
!credential.credential ||
|
|
1281
|
+
!credential.credentialSubject ||
|
|
1282
|
+
!credential.credentialSubject.id) {
|
|
1283
|
+
throw new InvalidParameterError('credential');
|
|
1284
|
+
}
|
|
1285
|
+
delete credential.signature;
|
|
1286
|
+
const signed = await this.addSignature(credential);
|
|
1287
|
+
const msg = JSON.stringify(signed);
|
|
1288
|
+
const id = await this.fetchIdInfo();
|
|
1289
|
+
const senderKeypair = await this.fetchKeyPair();
|
|
1290
|
+
if (!senderKeypair) {
|
|
1291
|
+
throw new KeymasterError('No valid sender keypair');
|
|
1292
|
+
}
|
|
1293
|
+
const holder = credential.credentialSubject.id;
|
|
1294
|
+
const holderDoc = await this.resolveDID(holder, { confirm: true });
|
|
1295
|
+
const receivePublicJwk = this.getPublicKeyJwk(holderDoc);
|
|
1296
|
+
const cipher_sender = this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg);
|
|
1297
|
+
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
|
|
1298
|
+
const msgHash = this.cipher.hashMessage(msg);
|
|
1299
|
+
const encrypted = {
|
|
1300
|
+
sender: id.did,
|
|
1301
|
+
created: new Date().toISOString(),
|
|
1302
|
+
cipher_hash: msgHash,
|
|
1303
|
+
cipher_sender: cipher_sender,
|
|
1304
|
+
cipher_receiver: cipher_receiver,
|
|
1305
|
+
};
|
|
1306
|
+
return this.updateDID(did, { didDocumentData: { encrypted } });
|
|
1307
|
+
}
|
|
1308
|
+
async revokeCredential(credential) {
|
|
1309
|
+
const did = await this.lookupDID(credential);
|
|
1310
|
+
return this.revokeDID(did);
|
|
1311
|
+
}
|
|
1312
|
+
async listIssued(issuer) {
|
|
1313
|
+
const id = await this.fetchIdInfo(issuer);
|
|
1314
|
+
const issued = [];
|
|
1315
|
+
if (id.owned) {
|
|
1316
|
+
for (const did of id.owned) {
|
|
1317
|
+
try {
|
|
1318
|
+
const credential = await this.decryptJSON(did);
|
|
1319
|
+
if (this.isVerifiableCredential(credential) &&
|
|
1320
|
+
credential.issuer === id.did) {
|
|
1321
|
+
issued.push(did);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) { }
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return issued;
|
|
1328
|
+
}
|
|
1329
|
+
async acceptCredential(did) {
|
|
1330
|
+
try {
|
|
1331
|
+
const id = await this.fetchIdInfo();
|
|
1332
|
+
const credential = await this.lookupDID(did);
|
|
1333
|
+
const vc = await this.decryptJSON(credential);
|
|
1334
|
+
if (this.isVerifiableCredential(vc) &&
|
|
1335
|
+
vc.credentialSubject?.id !== id.did) {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
return this.addToHeld(credential);
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
async getCredential(id) {
|
|
1345
|
+
const did = await this.lookupDID(id);
|
|
1346
|
+
const vc = await this.decryptJSON(did);
|
|
1347
|
+
if (!this.isVerifiableCredential(vc)) {
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
return vc;
|
|
1351
|
+
}
|
|
1352
|
+
async removeCredential(id) {
|
|
1353
|
+
const did = await this.lookupDID(id);
|
|
1354
|
+
return this.removeFromHeld(did);
|
|
1355
|
+
}
|
|
1356
|
+
async listCredentials(id) {
|
|
1357
|
+
const idInfo = await this.fetchIdInfo(id);
|
|
1358
|
+
return idInfo.held || [];
|
|
1359
|
+
}
|
|
1360
|
+
async publishCredential(did, options = {}) {
|
|
1361
|
+
const { reveal = false } = options;
|
|
1362
|
+
const id = await this.fetchIdInfo();
|
|
1363
|
+
const credential = await this.lookupDID(did);
|
|
1364
|
+
const vc = await this.decryptJSON(credential);
|
|
1365
|
+
if (!this.isVerifiableCredential(vc)) {
|
|
1366
|
+
throw new InvalidParameterError("did is not a credential");
|
|
1367
|
+
}
|
|
1368
|
+
if (vc.credentialSubject?.id !== id.did) {
|
|
1369
|
+
throw new InvalidParameterError('only subject can publish a credential');
|
|
1370
|
+
}
|
|
1371
|
+
const doc = await this.resolveDID(id.did);
|
|
1372
|
+
if (!doc.didDocumentData) {
|
|
1373
|
+
doc.didDocumentData = {};
|
|
1374
|
+
}
|
|
1375
|
+
const data = doc.didDocumentData;
|
|
1376
|
+
if (!data.manifest) {
|
|
1377
|
+
data.manifest = {};
|
|
1378
|
+
}
|
|
1379
|
+
if (!reveal) {
|
|
1380
|
+
// Remove the credential values
|
|
1381
|
+
vc.credential = null;
|
|
1382
|
+
}
|
|
1383
|
+
data.manifest[credential] = vc;
|
|
1384
|
+
const ok = await this.updateDID(id.did, { didDocumentData: doc.didDocumentData });
|
|
1385
|
+
if (ok) {
|
|
1386
|
+
return vc;
|
|
1387
|
+
}
|
|
1388
|
+
throw new KeymasterError('update DID failed');
|
|
1389
|
+
}
|
|
1390
|
+
async unpublishCredential(did) {
|
|
1391
|
+
const id = await this.fetchIdInfo();
|
|
1392
|
+
const doc = await this.resolveDID(id.did);
|
|
1393
|
+
const credential = await this.lookupDID(did);
|
|
1394
|
+
const data = doc.didDocumentData;
|
|
1395
|
+
if (credential && data.manifest && credential in data.manifest) {
|
|
1396
|
+
delete data.manifest[credential];
|
|
1397
|
+
await this.updateDID(id.did, { didDocumentData: doc.didDocumentData });
|
|
1398
|
+
return `OK credential ${did} removed from manifest`;
|
|
1399
|
+
}
|
|
1400
|
+
throw new InvalidParameterError('did');
|
|
1401
|
+
}
|
|
1402
|
+
async createChallenge(challenge = {}, options = {}) {
|
|
1403
|
+
if (!challenge || typeof challenge !== 'object' || Array.isArray(challenge)) {
|
|
1404
|
+
throw new InvalidParameterError('challenge');
|
|
1405
|
+
}
|
|
1406
|
+
if (challenge.credentials && !Array.isArray(challenge.credentials)) {
|
|
1407
|
+
throw new InvalidParameterError('challenge.credentials');
|
|
1408
|
+
// TBD validate each credential spec
|
|
1409
|
+
}
|
|
1410
|
+
if (!options.registry) {
|
|
1411
|
+
options.registry = this.ephemeralRegistry;
|
|
1412
|
+
}
|
|
1413
|
+
if (!options.validUntil) {
|
|
1414
|
+
const expires = new Date();
|
|
1415
|
+
expires.setHours(expires.getHours() + 1); // Add 1 hour
|
|
1416
|
+
options.validUntil = expires.toISOString();
|
|
1417
|
+
}
|
|
1418
|
+
return this.createAsset({ challenge }, options);
|
|
1419
|
+
}
|
|
1420
|
+
async findMatchingCredential(credential) {
|
|
1421
|
+
const id = await this.fetchIdInfo();
|
|
1422
|
+
if (!id.held) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
for (let did of id.held) {
|
|
1426
|
+
try {
|
|
1427
|
+
const doc = await this.decryptJSON(did);
|
|
1428
|
+
if (!this.isVerifiableCredential(doc)) {
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (doc.credentialSubject?.id !== id.did) {
|
|
1432
|
+
// This VC is issued by the ID, not held
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (credential.issuers && !credential.issuers.includes(doc.issuer)) {
|
|
1436
|
+
// Attestor not trusted by Verifier
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (doc.type && !doc.type.includes(credential.schema)) {
|
|
1440
|
+
// Wrong type
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
// TBD test for VC expiry too
|
|
1444
|
+
return did;
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
// Not encrypted, so can't be a VC
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
async createResponse(challengeDID, options = {}) {
|
|
1452
|
+
let { retries = 0, delay = 1000 } = options;
|
|
1453
|
+
if (!options.registry) {
|
|
1454
|
+
options.registry = this.ephemeralRegistry;
|
|
1455
|
+
}
|
|
1456
|
+
if (!options.validUntil) {
|
|
1457
|
+
const expires = new Date();
|
|
1458
|
+
expires.setHours(expires.getHours() + 1); // Add 1 hour
|
|
1459
|
+
options.validUntil = expires.toISOString();
|
|
1460
|
+
}
|
|
1461
|
+
let doc;
|
|
1462
|
+
while (retries >= 0) {
|
|
1463
|
+
try {
|
|
1464
|
+
doc = await this.resolveDID(challengeDID);
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
if (retries === 0)
|
|
1469
|
+
throw error; // If no retries left, throw the error
|
|
1470
|
+
retries--; // Decrease the retry count
|
|
1471
|
+
await new Promise(resolve => setTimeout(resolve, delay)); // Wait for delay milleseconds
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (!doc) {
|
|
1475
|
+
throw new InvalidParameterError('challengeDID does not resolve');
|
|
1476
|
+
}
|
|
1477
|
+
const result = await this.resolveAsset(challengeDID);
|
|
1478
|
+
if (!result) {
|
|
1479
|
+
throw new InvalidParameterError('challengeDID');
|
|
1480
|
+
}
|
|
1481
|
+
const challenge = result.challenge;
|
|
1482
|
+
if (!challenge) {
|
|
1483
|
+
throw new InvalidParameterError('challengeDID');
|
|
1484
|
+
}
|
|
1485
|
+
const requestor = doc.didDocument?.controller;
|
|
1486
|
+
if (!requestor) {
|
|
1487
|
+
throw new InvalidParameterError('requestor undefined');
|
|
1488
|
+
}
|
|
1489
|
+
// TBD check challenge isValid for expired?
|
|
1490
|
+
const matches = [];
|
|
1491
|
+
if (challenge.credentials) {
|
|
1492
|
+
for (let credential of challenge.credentials) {
|
|
1493
|
+
const vc = await this.findMatchingCredential(credential);
|
|
1494
|
+
if (vc) {
|
|
1495
|
+
matches.push(vc);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
const pairs = [];
|
|
1500
|
+
for (let vcDid of matches) {
|
|
1501
|
+
const plaintext = await this.decryptMessage(vcDid);
|
|
1502
|
+
const vpDid = await this.encryptMessage(plaintext, requestor, { ...options, includeHash: true });
|
|
1503
|
+
pairs.push({ vc: vcDid, vp: vpDid });
|
|
1504
|
+
}
|
|
1505
|
+
const requested = challenge.credentials?.length ?? 0;
|
|
1506
|
+
const fulfilled = matches.length;
|
|
1507
|
+
const match = (requested === fulfilled);
|
|
1508
|
+
const response = {
|
|
1509
|
+
challenge: challengeDID,
|
|
1510
|
+
credentials: pairs,
|
|
1511
|
+
requested: requested,
|
|
1512
|
+
fulfilled: fulfilled,
|
|
1513
|
+
match: match
|
|
1514
|
+
};
|
|
1515
|
+
return await this.encryptJSON({ response }, requestor, options);
|
|
1516
|
+
}
|
|
1517
|
+
async verifyResponse(responseDID, options = {}) {
|
|
1518
|
+
let { retries = 0, delay = 1000 } = options;
|
|
1519
|
+
let responseDoc;
|
|
1520
|
+
while (retries >= 0) {
|
|
1521
|
+
try {
|
|
1522
|
+
responseDoc = await this.resolveDID(responseDID);
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
if (retries === 0)
|
|
1527
|
+
throw error; // If no retries left, throw the error
|
|
1528
|
+
retries--; // Decrease the retry count
|
|
1529
|
+
await new Promise(resolve => setTimeout(resolve, delay)); // Wait for delay milliseconds
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (!responseDoc) {
|
|
1533
|
+
throw new InvalidParameterError('responseDID does not resolve');
|
|
1534
|
+
}
|
|
1535
|
+
const wrapper = await this.decryptJSON(responseDID);
|
|
1536
|
+
if (typeof wrapper !== 'object' || !wrapper || !('response' in wrapper)) {
|
|
1537
|
+
throw new InvalidParameterError('responseDID not a valid challenge response');
|
|
1538
|
+
}
|
|
1539
|
+
const { response } = wrapper;
|
|
1540
|
+
const result = await this.resolveAsset(response.challenge);
|
|
1541
|
+
if (!result) {
|
|
1542
|
+
throw new InvalidParameterError('challenge not found');
|
|
1543
|
+
}
|
|
1544
|
+
const challenge = result.challenge;
|
|
1545
|
+
if (!challenge) {
|
|
1546
|
+
throw new InvalidParameterError('challengeDID');
|
|
1547
|
+
}
|
|
1548
|
+
const vps = [];
|
|
1549
|
+
for (let credential of response.credentials) {
|
|
1550
|
+
const vcData = await this.resolveAsset(credential.vc);
|
|
1551
|
+
const vpData = await this.resolveAsset(credential.vp);
|
|
1552
|
+
const castVCData = vcData;
|
|
1553
|
+
const castVPData = vpData;
|
|
1554
|
+
if (!vcData || !vpData || !castVCData.encrypted || !castVPData.encrypted) {
|
|
1555
|
+
// VC revoked
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
const vcHash = castVCData.encrypted;
|
|
1559
|
+
const vpHash = castVPData.encrypted;
|
|
1560
|
+
if (vcHash.cipher_hash !== vpHash.cipher_hash) {
|
|
1561
|
+
// can't verify that the contents of VP match the VC
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const vp = await this.decryptJSON(credential.vp);
|
|
1565
|
+
const isValid = await this.verifySignature(vp);
|
|
1566
|
+
if (!isValid) {
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
if (!vp.type || !Array.isArray(vp.type)) {
|
|
1570
|
+
continue;
|
|
1571
|
+
}
|
|
1572
|
+
// Check VP against VCs specified in challenge
|
|
1573
|
+
if (vp.type.length >= 2 && vp.type[1].startsWith('did:')) {
|
|
1574
|
+
const schema = vp.type[1];
|
|
1575
|
+
const credential = challenge.credentials?.find(item => item.schema === schema);
|
|
1576
|
+
if (!credential) {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
// Check if issuer of VP is in the trusted issuer list
|
|
1580
|
+
if (credential.issuers && credential.issuers.length > 0 && !credential.issuers.includes(vp.issuer)) {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
vps.push(vp);
|
|
1585
|
+
}
|
|
1586
|
+
response.vps = vps;
|
|
1587
|
+
response.match = vps.length === (challenge.credentials?.length ?? 0);
|
|
1588
|
+
response.responder = responseDoc.didDocument?.controller;
|
|
1589
|
+
return response;
|
|
1590
|
+
}
|
|
1591
|
+
async createGroup(name, options = {}) {
|
|
1592
|
+
const group = {
|
|
1593
|
+
name: name,
|
|
1594
|
+
members: []
|
|
1595
|
+
};
|
|
1596
|
+
return this.createAsset({ group }, options);
|
|
1597
|
+
}
|
|
1598
|
+
async getGroup(id) {
|
|
1599
|
+
const asset = await this.resolveAsset(id);
|
|
1600
|
+
if (!asset) {
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
// TEMP during did:cid, return old version groups
|
|
1604
|
+
const castOldAsset = asset;
|
|
1605
|
+
if (castOldAsset.members) {
|
|
1606
|
+
return castOldAsset;
|
|
1607
|
+
}
|
|
1608
|
+
const castAsset = asset;
|
|
1609
|
+
if (!castAsset.group) {
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
return castAsset.group;
|
|
1613
|
+
}
|
|
1614
|
+
async addGroupMember(groupId, memberId) {
|
|
1615
|
+
const groupDID = await this.lookupDID(groupId);
|
|
1616
|
+
const memberDID = await this.lookupDID(memberId);
|
|
1617
|
+
// Can't add a group to itself
|
|
1618
|
+
if (memberDID === groupDID) {
|
|
1619
|
+
throw new InvalidParameterError("can't add a group to itself");
|
|
1620
|
+
}
|
|
1621
|
+
try {
|
|
1622
|
+
// test for valid member DID
|
|
1623
|
+
await this.resolveDID(memberDID);
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
throw new InvalidParameterError('memberId');
|
|
1627
|
+
}
|
|
1628
|
+
const group = await this.getGroup(groupId);
|
|
1629
|
+
if (!group?.members) {
|
|
1630
|
+
throw new InvalidParameterError('groupId');
|
|
1631
|
+
}
|
|
1632
|
+
// If already a member, return immediately
|
|
1633
|
+
if (group.members.includes(memberDID)) {
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
// Can't add a mutual membership relation
|
|
1637
|
+
const isMember = await this.testGroup(memberId, groupId);
|
|
1638
|
+
if (isMember) {
|
|
1639
|
+
throw new InvalidParameterError("can't create mutual membership");
|
|
1640
|
+
}
|
|
1641
|
+
const members = new Set(group.members);
|
|
1642
|
+
members.add(memberDID);
|
|
1643
|
+
group.members = Array.from(members);
|
|
1644
|
+
return this.updateAsset(groupDID, { group });
|
|
1645
|
+
}
|
|
1646
|
+
async removeGroupMember(groupId, memberId) {
|
|
1647
|
+
const groupDID = await this.lookupDID(groupId);
|
|
1648
|
+
const memberDID = await this.lookupDID(memberId);
|
|
1649
|
+
const group = await this.getGroup(groupDID);
|
|
1650
|
+
if (!group?.members) {
|
|
1651
|
+
throw new InvalidParameterError('groupId');
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
// test for valid member DID
|
|
1655
|
+
await this.resolveDID(memberDID);
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
throw new InvalidParameterError('memberId');
|
|
1659
|
+
}
|
|
1660
|
+
// If not already a member, return immediately
|
|
1661
|
+
if (!group.members.includes(memberDID)) {
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
const members = new Set(group.members);
|
|
1665
|
+
members.delete(memberDID);
|
|
1666
|
+
group.members = Array.from(members);
|
|
1667
|
+
return this.updateAsset(groupDID, { group });
|
|
1668
|
+
}
|
|
1669
|
+
async testGroup(groupId, memberId) {
|
|
1670
|
+
try {
|
|
1671
|
+
const group = await this.getGroup(groupId);
|
|
1672
|
+
if (!group) {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
if (!memberId) {
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
const didMember = await this.lookupDID(memberId);
|
|
1679
|
+
let isMember = group.members.includes(didMember);
|
|
1680
|
+
if (!isMember) {
|
|
1681
|
+
for (const did of group.members) {
|
|
1682
|
+
isMember = await this.testGroup(did, didMember);
|
|
1683
|
+
if (isMember) {
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return isMember;
|
|
1689
|
+
}
|
|
1690
|
+
catch (error) {
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
async listGroups(owner) {
|
|
1695
|
+
const assets = await this.listAssets(owner);
|
|
1696
|
+
const groups = [];
|
|
1697
|
+
for (const did of assets) {
|
|
1698
|
+
const isGroup = await this.testGroup(did);
|
|
1699
|
+
if (isGroup) {
|
|
1700
|
+
groups.push(did);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return groups;
|
|
1704
|
+
}
|
|
1705
|
+
validateSchema(schema) {
|
|
1706
|
+
try {
|
|
1707
|
+
// Attempt to instantiate the schema
|
|
1708
|
+
this.generateSchema(schema);
|
|
1709
|
+
return true;
|
|
1710
|
+
}
|
|
1711
|
+
catch (error) {
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
generateSchema(schema) {
|
|
1716
|
+
if (typeof schema !== 'object' ||
|
|
1717
|
+
!schema ||
|
|
1718
|
+
!('$schema' in schema) ||
|
|
1719
|
+
!('properties' in schema)) {
|
|
1720
|
+
throw new InvalidParameterError('schema');
|
|
1721
|
+
}
|
|
1722
|
+
const template = {};
|
|
1723
|
+
const props = schema.properties;
|
|
1724
|
+
for (const property of Object.keys(props)) {
|
|
1725
|
+
template[property] = "TBD";
|
|
1726
|
+
}
|
|
1727
|
+
return template;
|
|
1728
|
+
}
|
|
1729
|
+
async createSchema(schema, options = {}) {
|
|
1730
|
+
if (!schema) {
|
|
1731
|
+
schema = DefaultSchema;
|
|
1732
|
+
}
|
|
1733
|
+
if (!this.validateSchema(schema)) {
|
|
1734
|
+
throw new InvalidParameterError('schema');
|
|
1735
|
+
}
|
|
1736
|
+
return this.createAsset({ schema }, options);
|
|
1737
|
+
}
|
|
1738
|
+
async getSchema(id) {
|
|
1739
|
+
const asset = await this.resolveAsset(id);
|
|
1740
|
+
if (!asset) {
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
// TEMP during did:cid, return old version schemas
|
|
1744
|
+
const castOldAsset = asset;
|
|
1745
|
+
if (castOldAsset.properties) {
|
|
1746
|
+
return asset;
|
|
1747
|
+
}
|
|
1748
|
+
const castAsset = asset;
|
|
1749
|
+
if (!castAsset.schema) {
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
return castAsset.schema;
|
|
1753
|
+
}
|
|
1754
|
+
async setSchema(id, schema) {
|
|
1755
|
+
if (!this.validateSchema(schema)) {
|
|
1756
|
+
throw new InvalidParameterError('schema');
|
|
1757
|
+
}
|
|
1758
|
+
return this.updateAsset(id, { schema });
|
|
1759
|
+
}
|
|
1760
|
+
// TBD add optional 2nd parameter that will validate JSON against the schema
|
|
1761
|
+
async testSchema(id) {
|
|
1762
|
+
try {
|
|
1763
|
+
const schema = await this.getSchema(id);
|
|
1764
|
+
// TBD Need a better way because any random object with keys can be a valid schema
|
|
1765
|
+
if (!schema || Object.keys(schema).length === 0) {
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
return this.validateSchema(schema);
|
|
1769
|
+
}
|
|
1770
|
+
catch (error) {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async listSchemas(owner) {
|
|
1775
|
+
const assets = await this.listAssets(owner);
|
|
1776
|
+
const schemas = [];
|
|
1777
|
+
for (const did of assets) {
|
|
1778
|
+
const isSchema = await this.testSchema(did);
|
|
1779
|
+
if (isSchema) {
|
|
1780
|
+
schemas.push(did);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return schemas;
|
|
1784
|
+
}
|
|
1785
|
+
async createTemplate(schemaId) {
|
|
1786
|
+
const isSchema = await this.testSchema(schemaId);
|
|
1787
|
+
if (!isSchema) {
|
|
1788
|
+
throw new InvalidParameterError('schemaId');
|
|
1789
|
+
}
|
|
1790
|
+
const schemaDID = await this.lookupDID(schemaId);
|
|
1791
|
+
const schema = await this.getSchema(schemaDID);
|
|
1792
|
+
const template = this.generateSchema(schema);
|
|
1793
|
+
template['$schema'] = schemaDID;
|
|
1794
|
+
return template;
|
|
1795
|
+
}
|
|
1796
|
+
async pollTemplate() {
|
|
1797
|
+
const now = new Date();
|
|
1798
|
+
const nextWeek = new Date();
|
|
1799
|
+
nextWeek.setDate(now.getDate() + 7);
|
|
1800
|
+
return {
|
|
1801
|
+
type: 'poll',
|
|
1802
|
+
version: 1,
|
|
1803
|
+
description: 'What is this poll about?',
|
|
1804
|
+
roster: 'DID of the eligible voter group',
|
|
1805
|
+
options: ['yes', 'no', 'abstain'],
|
|
1806
|
+
deadline: nextWeek.toISOString(),
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
async createPoll(poll, options = {}) {
|
|
1810
|
+
if (poll.type !== 'poll') {
|
|
1811
|
+
throw new InvalidParameterError('poll');
|
|
1812
|
+
}
|
|
1813
|
+
if (poll.version !== 1) {
|
|
1814
|
+
throw new InvalidParameterError('poll.version');
|
|
1815
|
+
}
|
|
1816
|
+
if (!poll.description) {
|
|
1817
|
+
throw new InvalidParameterError('poll.description');
|
|
1818
|
+
}
|
|
1819
|
+
if (!poll.options || !Array.isArray(poll.options) || poll.options.length < 2 || poll.options.length > 10) {
|
|
1820
|
+
throw new InvalidParameterError('poll.options');
|
|
1821
|
+
}
|
|
1822
|
+
if (!poll.roster) {
|
|
1823
|
+
// eslint-disable-next-line
|
|
1824
|
+
throw new InvalidParameterError('poll.roster');
|
|
1825
|
+
}
|
|
1826
|
+
try {
|
|
1827
|
+
const isValidGroup = await this.testGroup(poll.roster);
|
|
1828
|
+
if (!isValidGroup) {
|
|
1829
|
+
throw new InvalidParameterError('poll.roster');
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
catch {
|
|
1833
|
+
throw new InvalidParameterError('poll.roster');
|
|
1834
|
+
}
|
|
1835
|
+
if (!poll.deadline) {
|
|
1836
|
+
// eslint-disable-next-line
|
|
1837
|
+
throw new InvalidParameterError('poll.deadline');
|
|
1838
|
+
}
|
|
1839
|
+
const deadline = new Date(poll.deadline);
|
|
1840
|
+
if (isNaN(deadline.getTime())) {
|
|
1841
|
+
throw new InvalidParameterError('poll.deadline');
|
|
1842
|
+
}
|
|
1843
|
+
if (deadline < new Date()) {
|
|
1844
|
+
throw new InvalidParameterError('poll.deadline');
|
|
1845
|
+
}
|
|
1846
|
+
return this.createAsset({ poll }, options);
|
|
1847
|
+
}
|
|
1848
|
+
async getPoll(id) {
|
|
1849
|
+
const asset = await this.resolveAsset(id);
|
|
1850
|
+
// TEMP during did:cid, return old version poll
|
|
1851
|
+
const castOldAsset = asset;
|
|
1852
|
+
if (castOldAsset.options) {
|
|
1853
|
+
return castOldAsset;
|
|
1854
|
+
}
|
|
1855
|
+
const castAsset = asset;
|
|
1856
|
+
if (!castAsset.poll) {
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
return castAsset.poll;
|
|
1860
|
+
}
|
|
1861
|
+
async testPoll(id) {
|
|
1862
|
+
try {
|
|
1863
|
+
const poll = await this.getPoll(id);
|
|
1864
|
+
return poll !== null;
|
|
1865
|
+
}
|
|
1866
|
+
catch (error) {
|
|
1867
|
+
return false;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
async listPolls(owner) {
|
|
1871
|
+
const assets = await this.listAssets(owner);
|
|
1872
|
+
const polls = [];
|
|
1873
|
+
for (const did of assets) {
|
|
1874
|
+
const isPoll = await this.testPoll(did);
|
|
1875
|
+
if (isPoll) {
|
|
1876
|
+
polls.push(did);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return polls;
|
|
1880
|
+
}
|
|
1881
|
+
async viewPoll(pollId) {
|
|
1882
|
+
const id = await this.fetchIdInfo();
|
|
1883
|
+
const poll = await this.getPoll(pollId);
|
|
1884
|
+
if (!poll) {
|
|
1885
|
+
throw new InvalidParameterError('pollId');
|
|
1886
|
+
}
|
|
1887
|
+
let hasVoted = false;
|
|
1888
|
+
if (poll.ballots) {
|
|
1889
|
+
hasVoted = !!poll.ballots[id.did];
|
|
1890
|
+
}
|
|
1891
|
+
const voteExpired = Date.now() > new Date(poll.deadline).getTime();
|
|
1892
|
+
const isEligible = await this.testGroup(poll.roster, id.did);
|
|
1893
|
+
const doc = await this.resolveDID(pollId);
|
|
1894
|
+
const view = {
|
|
1895
|
+
description: poll.description,
|
|
1896
|
+
options: poll.options,
|
|
1897
|
+
deadline: poll.deadline,
|
|
1898
|
+
isOwner: (doc.didDocument?.controller === id.did),
|
|
1899
|
+
isEligible: isEligible,
|
|
1900
|
+
voteExpired: voteExpired,
|
|
1901
|
+
hasVoted: hasVoted,
|
|
1902
|
+
};
|
|
1903
|
+
if (id.did === doc.didDocument?.controller) {
|
|
1904
|
+
let voted = 0;
|
|
1905
|
+
const results = {
|
|
1906
|
+
tally: [],
|
|
1907
|
+
ballots: [],
|
|
1908
|
+
};
|
|
1909
|
+
results.tally.push({
|
|
1910
|
+
vote: 0,
|
|
1911
|
+
option: 'spoil',
|
|
1912
|
+
count: 0,
|
|
1913
|
+
});
|
|
1914
|
+
for (let i = 0; i < poll.options.length; i++) {
|
|
1915
|
+
results.tally.push({
|
|
1916
|
+
vote: i + 1,
|
|
1917
|
+
option: poll.options[i],
|
|
1918
|
+
count: 0,
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
for (let voter in poll.ballots) {
|
|
1922
|
+
const ballot = poll.ballots[voter];
|
|
1923
|
+
const decrypted = await this.decryptJSON(ballot.ballot);
|
|
1924
|
+
const vote = decrypted.vote;
|
|
1925
|
+
if (results.ballots) {
|
|
1926
|
+
results.ballots.push({
|
|
1927
|
+
...ballot,
|
|
1928
|
+
voter,
|
|
1929
|
+
vote,
|
|
1930
|
+
option: poll.options[vote - 1],
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
voted += 1;
|
|
1934
|
+
results.tally[vote].count += 1;
|
|
1935
|
+
}
|
|
1936
|
+
const roster = await this.getGroup(poll.roster);
|
|
1937
|
+
const total = roster.members.length;
|
|
1938
|
+
results.votes = {
|
|
1939
|
+
eligible: total,
|
|
1940
|
+
received: voted,
|
|
1941
|
+
pending: total - voted,
|
|
1942
|
+
};
|
|
1943
|
+
results.final = voteExpired || (voted === total);
|
|
1944
|
+
view.results = results;
|
|
1945
|
+
}
|
|
1946
|
+
return view;
|
|
1947
|
+
}
|
|
1948
|
+
async votePoll(pollId, vote, options = {}) {
|
|
1949
|
+
const { spoil = false } = options;
|
|
1950
|
+
const id = await this.fetchIdInfo();
|
|
1951
|
+
const didPoll = await this.lookupDID(pollId);
|
|
1952
|
+
const doc = await this.resolveDID(didPoll);
|
|
1953
|
+
const poll = await this.getPoll(pollId);
|
|
1954
|
+
if (!poll) {
|
|
1955
|
+
throw new InvalidParameterError('pollId');
|
|
1956
|
+
}
|
|
1957
|
+
const eligible = await this.testGroup(poll.roster, id.did);
|
|
1958
|
+
const expired = Date.now() > new Date(poll.deadline).getTime();
|
|
1959
|
+
const owner = doc.didDocument?.controller;
|
|
1960
|
+
if (!owner) {
|
|
1961
|
+
throw new KeymasterError('owner mising from poll');
|
|
1962
|
+
}
|
|
1963
|
+
if (!eligible) {
|
|
1964
|
+
throw new InvalidParameterError('voter not in roster');
|
|
1965
|
+
}
|
|
1966
|
+
if (expired) {
|
|
1967
|
+
throw new InvalidParameterError('poll has expired');
|
|
1968
|
+
}
|
|
1969
|
+
let ballot;
|
|
1970
|
+
if (spoil) {
|
|
1971
|
+
ballot = {
|
|
1972
|
+
poll: didPoll,
|
|
1973
|
+
vote: 0,
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
else {
|
|
1977
|
+
const max = poll.options.length;
|
|
1978
|
+
if (!Number.isInteger(vote) || vote < 1 || vote > max) {
|
|
1979
|
+
throw new InvalidParameterError('vote');
|
|
1980
|
+
}
|
|
1981
|
+
ballot = {
|
|
1982
|
+
poll: didPoll,
|
|
1983
|
+
vote: vote,
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
// Encrypt for receiver only
|
|
1987
|
+
return await this.encryptJSON(ballot, owner, { ...options, encryptForSender: false });
|
|
1988
|
+
}
|
|
1989
|
+
async updatePoll(ballot) {
|
|
1990
|
+
const id = await this.fetchIdInfo();
|
|
1991
|
+
const didBallot = await this.lookupDID(ballot);
|
|
1992
|
+
const docBallot = await this.resolveDID(ballot);
|
|
1993
|
+
const didVoter = docBallot.didDocument.controller;
|
|
1994
|
+
let dataBallot;
|
|
1995
|
+
try {
|
|
1996
|
+
dataBallot = await this.decryptJSON(didBallot);
|
|
1997
|
+
if (!dataBallot.poll || !dataBallot.vote) {
|
|
1998
|
+
throw new InvalidParameterError('ballot');
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
catch {
|
|
2002
|
+
throw new InvalidParameterError('ballot');
|
|
2003
|
+
}
|
|
2004
|
+
const didPoll = dataBallot.poll;
|
|
2005
|
+
const docPoll = await this.resolveDID(didPoll);
|
|
2006
|
+
const didOwner = docPoll.didDocument.controller;
|
|
2007
|
+
const poll = await this.getPoll(didPoll);
|
|
2008
|
+
if (!poll) {
|
|
2009
|
+
throw new KeymasterError('Cannot find poll related to ballot');
|
|
2010
|
+
}
|
|
2011
|
+
if (id.did !== didOwner) {
|
|
2012
|
+
throw new InvalidParameterError('only owner can update a poll');
|
|
2013
|
+
}
|
|
2014
|
+
const eligible = await this.testGroup(poll.roster, didVoter);
|
|
2015
|
+
if (!eligible) {
|
|
2016
|
+
throw new InvalidParameterError('voter not in roster');
|
|
2017
|
+
}
|
|
2018
|
+
const expired = Date.now() > new Date(poll.deadline).getTime();
|
|
2019
|
+
if (expired) {
|
|
2020
|
+
throw new InvalidParameterError('poll has expired');
|
|
2021
|
+
}
|
|
2022
|
+
const max = poll.options.length;
|
|
2023
|
+
const vote = dataBallot.vote;
|
|
2024
|
+
if (!vote || vote < 0 || vote > max) {
|
|
2025
|
+
throw new InvalidParameterError('ballot.vote');
|
|
2026
|
+
}
|
|
2027
|
+
if (!poll.ballots) {
|
|
2028
|
+
poll.ballots = {};
|
|
2029
|
+
}
|
|
2030
|
+
poll.ballots[didVoter] = {
|
|
2031
|
+
ballot: didBallot,
|
|
2032
|
+
received: new Date().toISOString(),
|
|
2033
|
+
};
|
|
2034
|
+
return this.updateAsset(didPoll, { poll });
|
|
2035
|
+
}
|
|
2036
|
+
async publishPoll(pollId, options = {}) {
|
|
2037
|
+
const { reveal = false } = options;
|
|
2038
|
+
const id = await this.fetchIdInfo();
|
|
2039
|
+
const doc = await this.resolveDID(pollId);
|
|
2040
|
+
const owner = doc.didDocument?.controller;
|
|
2041
|
+
if (id.did !== owner) {
|
|
2042
|
+
throw new InvalidParameterError('only owner can publish a poll');
|
|
2043
|
+
}
|
|
2044
|
+
const view = await this.viewPoll(pollId);
|
|
2045
|
+
if (!view.results?.final) {
|
|
2046
|
+
throw new InvalidParameterError('poll not final');
|
|
2047
|
+
}
|
|
2048
|
+
if (!reveal && view.results.ballots) {
|
|
2049
|
+
delete view.results.ballots;
|
|
2050
|
+
}
|
|
2051
|
+
const poll = await this.getPoll(pollId);
|
|
2052
|
+
if (!poll) {
|
|
2053
|
+
throw new InvalidParameterError(pollId);
|
|
2054
|
+
}
|
|
2055
|
+
poll.results = view.results;
|
|
2056
|
+
return this.updateAsset(pollId, { poll });
|
|
2057
|
+
}
|
|
2058
|
+
async unpublishPoll(pollId) {
|
|
2059
|
+
const id = await this.fetchIdInfo();
|
|
2060
|
+
const doc = await this.resolveDID(pollId);
|
|
2061
|
+
const owner = doc.didDocument?.controller;
|
|
2062
|
+
if (id.did !== owner) {
|
|
2063
|
+
throw new InvalidParameterError(pollId);
|
|
2064
|
+
}
|
|
2065
|
+
const poll = await this.getPoll(pollId);
|
|
2066
|
+
if (!poll) {
|
|
2067
|
+
throw new InvalidParameterError(pollId);
|
|
2068
|
+
}
|
|
2069
|
+
delete poll.results;
|
|
2070
|
+
return this.updateAsset(pollId, { poll });
|
|
2071
|
+
}
|
|
2072
|
+
async createGroupVault(options = {}) {
|
|
2073
|
+
const id = await this.fetchIdInfo();
|
|
2074
|
+
const idKeypair = await this.fetchKeyPair();
|
|
2075
|
+
// version defaults to 1. To make version undefined (unit testing), set options.version to 0
|
|
2076
|
+
const version = typeof options.version === 'undefined'
|
|
2077
|
+
? 1
|
|
2078
|
+
: (typeof options.version === 'number' && options.version === 1 ? options.version : undefined);
|
|
2079
|
+
const salt = this.cipher.generateRandomSalt();
|
|
2080
|
+
const vaultKeypair = this.cipher.generateRandomJwk();
|
|
2081
|
+
const keys = {};
|
|
2082
|
+
const config = this.cipher.encryptMessage(idKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify(options));
|
|
2083
|
+
const publicJwk = options.secretMembers ? idKeypair.publicJwk : vaultKeypair.publicJwk; // If secret, encrypt for the owner only
|
|
2084
|
+
const members = this.cipher.encryptMessage(publicJwk, vaultKeypair.privateJwk, JSON.stringify({}));
|
|
2085
|
+
const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify({}));
|
|
2086
|
+
const sha256 = this.cipher.hashJSON({});
|
|
2087
|
+
const groupVault = {
|
|
2088
|
+
version,
|
|
2089
|
+
publicJwk: vaultKeypair.publicJwk,
|
|
2090
|
+
salt,
|
|
2091
|
+
config,
|
|
2092
|
+
members,
|
|
2093
|
+
keys,
|
|
2094
|
+
items,
|
|
2095
|
+
sha256,
|
|
2096
|
+
};
|
|
2097
|
+
await this.addMemberKey(groupVault, id.did, vaultKeypair.privateJwk);
|
|
2098
|
+
return this.createAsset({ groupVault }, options);
|
|
2099
|
+
}
|
|
2100
|
+
async getGroupVault(groupVaultId, options) {
|
|
2101
|
+
const asset = await this.resolveAsset(groupVaultId, options);
|
|
2102
|
+
if (!asset.groupVault) {
|
|
2103
|
+
throw new InvalidParameterError('groupVaultId');
|
|
2104
|
+
}
|
|
2105
|
+
return asset.groupVault;
|
|
2106
|
+
}
|
|
2107
|
+
async testGroupVault(id, options) {
|
|
2108
|
+
try {
|
|
2109
|
+
const groupVault = await this.getGroupVault(id, options);
|
|
2110
|
+
return groupVault !== null;
|
|
2111
|
+
}
|
|
2112
|
+
catch (error) {
|
|
2113
|
+
return false;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
generateSaltedId(groupVault, memberDID) {
|
|
2117
|
+
if (!groupVault.version) {
|
|
2118
|
+
return this.cipher.hashMessage(groupVault.salt + memberDID);
|
|
2119
|
+
}
|
|
2120
|
+
const suffix = memberDID.split(':').pop();
|
|
2121
|
+
return this.cipher.hashMessage(groupVault.salt + suffix);
|
|
2122
|
+
}
|
|
2123
|
+
async decryptGroupVault(groupVault) {
|
|
2124
|
+
const wallet = await this.loadWallet();
|
|
2125
|
+
const id = await this.fetchIdInfo();
|
|
2126
|
+
const myMemberId = this.generateSaltedId(groupVault, id.did);
|
|
2127
|
+
const myVaultKey = groupVault.keys[myMemberId];
|
|
2128
|
+
if (!myVaultKey) {
|
|
2129
|
+
throw new KeymasterError('No access to group vault');
|
|
2130
|
+
}
|
|
2131
|
+
const privKeyJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, myVaultKey);
|
|
2132
|
+
const privateJwk = JSON.parse(privKeyJSON);
|
|
2133
|
+
let config = {};
|
|
2134
|
+
let isOwner = false;
|
|
2135
|
+
try {
|
|
2136
|
+
const configJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, groupVault.config);
|
|
2137
|
+
config = JSON.parse(configJSON);
|
|
2138
|
+
isOwner = true;
|
|
2139
|
+
}
|
|
2140
|
+
catch (error) {
|
|
2141
|
+
// Can't decrypt config if not the owner
|
|
2142
|
+
}
|
|
2143
|
+
let members = {};
|
|
2144
|
+
if (config.secretMembers) {
|
|
2145
|
+
try {
|
|
2146
|
+
const membersJSON = await this.decryptWithDerivedKeys(wallet, id, groupVault.publicJwk, groupVault.members);
|
|
2147
|
+
members = JSON.parse(membersJSON);
|
|
2148
|
+
}
|
|
2149
|
+
catch (error) {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
else {
|
|
2153
|
+
try {
|
|
2154
|
+
const membersJSON = this.cipher.decryptMessage(groupVault.publicJwk, privateJwk, groupVault.members);
|
|
2155
|
+
members = JSON.parse(membersJSON);
|
|
2156
|
+
}
|
|
2157
|
+
catch (error) {
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const itemsJSON = this.cipher.decryptMessage(groupVault.publicJwk, privateJwk, groupVault.items);
|
|
2161
|
+
const items = JSON.parse(itemsJSON);
|
|
2162
|
+
return {
|
|
2163
|
+
isOwner,
|
|
2164
|
+
privateJwk,
|
|
2165
|
+
config,
|
|
2166
|
+
members,
|
|
2167
|
+
items,
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
async checkGroupVaultOwner(vaultId) {
|
|
2171
|
+
const id = await this.fetchIdInfo();
|
|
2172
|
+
const vaultDoc = await this.resolveDID(vaultId);
|
|
2173
|
+
const controller = vaultDoc.didDocument?.controller;
|
|
2174
|
+
if (controller !== id.did) {
|
|
2175
|
+
throw new KeymasterError('Only vault owner can modify the vault');
|
|
2176
|
+
}
|
|
2177
|
+
return controller;
|
|
2178
|
+
}
|
|
2179
|
+
async addMemberKey(groupVault, memberDID, privateJwk) {
|
|
2180
|
+
const memberDoc = await this.resolveDID(memberDID, { confirm: true });
|
|
2181
|
+
const memberPublicJwk = this.getPublicKeyJwk(memberDoc);
|
|
2182
|
+
const memberKey = this.cipher.encryptMessage(memberPublicJwk, privateJwk, JSON.stringify(privateJwk));
|
|
2183
|
+
const memberKeyId = this.generateSaltedId(groupVault, memberDID);
|
|
2184
|
+
groupVault.keys[memberKeyId] = memberKey;
|
|
2185
|
+
}
|
|
2186
|
+
async checkVaultVersion(vaultId, groupVault) {
|
|
2187
|
+
if (groupVault.version === 1) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
if (!groupVault.version) {
|
|
2191
|
+
const id = await this.fetchIdInfo();
|
|
2192
|
+
const { privateJwk, members } = await this.decryptGroupVault(groupVault);
|
|
2193
|
+
groupVault.version = 1;
|
|
2194
|
+
groupVault.keys = {};
|
|
2195
|
+
await this.addMemberKey(groupVault, id.did, privateJwk);
|
|
2196
|
+
for (const memberDID of Object.keys(members)) {
|
|
2197
|
+
await this.addMemberKey(groupVault, memberDID, privateJwk);
|
|
2198
|
+
}
|
|
2199
|
+
await this.updateAsset(vaultId, { groupVault });
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
throw new KeymasterError('Unsupported group vault version');
|
|
2203
|
+
}
|
|
2204
|
+
getAgentDID(doc) {
|
|
2205
|
+
if (doc.didDocumentRegistration?.type !== 'agent') {
|
|
2206
|
+
throw new KeymasterError('Document is not an agent');
|
|
2207
|
+
}
|
|
2208
|
+
const did = doc.didDocument?.id;
|
|
2209
|
+
if (!did) {
|
|
2210
|
+
throw new KeymasterError('Agent document does not have a DID');
|
|
2211
|
+
}
|
|
2212
|
+
return did;
|
|
2213
|
+
}
|
|
2214
|
+
async addGroupVaultMember(vaultId, memberId) {
|
|
2215
|
+
const owner = await this.checkGroupVaultOwner(vaultId);
|
|
2216
|
+
const idKeypair = await this.fetchKeyPair();
|
|
2217
|
+
const groupVault = await this.getGroupVault(vaultId);
|
|
2218
|
+
const { privateJwk, config, members } = await this.decryptGroupVault(groupVault);
|
|
2219
|
+
const memberDoc = await this.resolveDID(memberId, { confirm: true });
|
|
2220
|
+
const memberDID = this.getAgentDID(memberDoc);
|
|
2221
|
+
// Don't allow adding the vault owner
|
|
2222
|
+
if (owner === memberDID) {
|
|
2223
|
+
return false;
|
|
2224
|
+
}
|
|
2225
|
+
members[memberDID] = { added: new Date().toISOString() };
|
|
2226
|
+
const publicJwk = config.secretMembers ? idKeypair.publicJwk : groupVault.publicJwk;
|
|
2227
|
+
groupVault.members = this.cipher.encryptMessage(publicJwk, privateJwk, JSON.stringify(members));
|
|
2228
|
+
await this.addMemberKey(groupVault, memberDID, privateJwk);
|
|
2229
|
+
return this.updateAsset(vaultId, { groupVault });
|
|
2230
|
+
}
|
|
2231
|
+
async removeGroupVaultMember(vaultId, memberId) {
|
|
2232
|
+
const owner = await this.checkGroupVaultOwner(vaultId);
|
|
2233
|
+
const idKeypair = await this.fetchKeyPair();
|
|
2234
|
+
const groupVault = await this.getGroupVault(vaultId);
|
|
2235
|
+
const { privateJwk, config, members } = await this.decryptGroupVault(groupVault);
|
|
2236
|
+
const memberDoc = await this.resolveDID(memberId, { confirm: true });
|
|
2237
|
+
const memberDID = this.getAgentDID(memberDoc);
|
|
2238
|
+
// Don't allow removing the vault owner
|
|
2239
|
+
if (owner === memberDID) {
|
|
2240
|
+
return false;
|
|
2241
|
+
}
|
|
2242
|
+
delete members[memberDID];
|
|
2243
|
+
const publicJwk = config.secretMembers ? idKeypair.publicJwk : groupVault.publicJwk;
|
|
2244
|
+
groupVault.members = this.cipher.encryptMessage(publicJwk, privateJwk, JSON.stringify(members));
|
|
2245
|
+
const memberKeyId = this.generateSaltedId(groupVault, memberDID);
|
|
2246
|
+
delete groupVault.keys[memberKeyId];
|
|
2247
|
+
return this.updateAsset(vaultId, { groupVault });
|
|
2248
|
+
}
|
|
2249
|
+
async listGroupVaultMembers(vaultId) {
|
|
2250
|
+
const groupVault = await this.getGroupVault(vaultId);
|
|
2251
|
+
const { members, isOwner } = await this.decryptGroupVault(groupVault);
|
|
2252
|
+
if (isOwner) {
|
|
2253
|
+
await this.checkVaultVersion(vaultId, groupVault);
|
|
2254
|
+
}
|
|
2255
|
+
return members;
|
|
2256
|
+
}
|
|
2257
|
+
async addGroupVaultItem(vaultId, name, buffer) {
|
|
2258
|
+
await this.checkGroupVaultOwner(vaultId);
|
|
2259
|
+
const groupVault = await this.getGroupVault(vaultId);
|
|
2260
|
+
const { privateJwk, items } = await this.decryptGroupVault(groupVault);
|
|
2261
|
+
const validName = this.validateName(name);
|
|
2262
|
+
const encryptedData = this.cipher.encryptBytes(groupVault.publicJwk, privateJwk, buffer);
|
|
2263
|
+
const cid = await this.gatekeeper.addText(encryptedData);
|
|
2264
|
+
const sha256 = this.cipher.hashMessage(buffer);
|
|
2265
|
+
const type = await this.getMimeType(buffer);
|
|
2266
|
+
const data = encryptedData.length < this.maxDataLength ? encryptedData : undefined;
|
|
2267
|
+
items[validName] = {
|
|
2268
|
+
cid,
|
|
2269
|
+
sha256,
|
|
2270
|
+
bytes: buffer.length,
|
|
2271
|
+
type,
|
|
2272
|
+
added: new Date().toISOString(),
|
|
2273
|
+
data,
|
|
2274
|
+
};
|
|
2275
|
+
groupVault.items = this.cipher.encryptMessage(groupVault.publicJwk, privateJwk, JSON.stringify(items));
|
|
2276
|
+
groupVault.sha256 = this.cipher.hashJSON(items);
|
|
2277
|
+
return this.updateAsset(vaultId, { groupVault });
|
|
2278
|
+
}
|
|
2279
|
+
async removeGroupVaultItem(vaultId, name) {
|
|
2280
|
+
await this.checkGroupVaultOwner(vaultId);
|
|
2281
|
+
const groupVault = await this.getGroupVault(vaultId);
|
|
2282
|
+
const { privateJwk, items } = await this.decryptGroupVault(groupVault);
|
|
2283
|
+
delete items[name];
|
|
2284
|
+
groupVault.items = this.cipher.encryptMessage(groupVault.publicJwk, privateJwk, JSON.stringify(items));
|
|
2285
|
+
groupVault.sha256 = this.cipher.hashJSON(items);
|
|
2286
|
+
return this.updateAsset(vaultId, { groupVault });
|
|
2287
|
+
}
|
|
2288
|
+
async listGroupVaultItems(vaultId, options) {
|
|
2289
|
+
const groupVault = await this.getGroupVault(vaultId, options);
|
|
2290
|
+
const { items } = await this.decryptGroupVault(groupVault);
|
|
2291
|
+
return items;
|
|
2292
|
+
}
|
|
2293
|
+
async getGroupVaultItem(vaultId, name, options) {
|
|
2294
|
+
try {
|
|
2295
|
+
const groupVault = await this.getGroupVault(vaultId, options);
|
|
2296
|
+
const { privateJwk, items } = await this.decryptGroupVault(groupVault);
|
|
2297
|
+
if (items[name]) {
|
|
2298
|
+
const encryptedData = items[name].data || await this.gatekeeper.getText(items[name].cid);
|
|
2299
|
+
if (encryptedData) {
|
|
2300
|
+
const bytes = this.cipher.decryptBytes(groupVault.publicJwk, privateJwk, encryptedData);
|
|
2301
|
+
return Buffer.from(bytes);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
return null;
|
|
2305
|
+
}
|
|
2306
|
+
catch (error) {
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
async listDmail() {
|
|
2311
|
+
const wallet = await this.loadWallet();
|
|
2312
|
+
const id = await this.fetchIdInfo(undefined, wallet);
|
|
2313
|
+
const list = id.dmail || {};
|
|
2314
|
+
const dmailList = {};
|
|
2315
|
+
const nameList = await this.listNames({ includeIDs: true });
|
|
2316
|
+
const didToName = Object.entries(nameList).reduce((acc, [name, did]) => {
|
|
2317
|
+
acc[did] = name;
|
|
2318
|
+
return acc;
|
|
2319
|
+
}, {});
|
|
2320
|
+
for (const did of Object.keys(list)) {
|
|
2321
|
+
const message = await this.getDmailMessage(did);
|
|
2322
|
+
if (!message) {
|
|
2323
|
+
continue; // Skip if no dmail found for this DID
|
|
2324
|
+
}
|
|
2325
|
+
const tags = list[did].tags ?? [];
|
|
2326
|
+
const docs = await this.resolveDID(did);
|
|
2327
|
+
const controller = docs.didDocument?.controller ?? '';
|
|
2328
|
+
const sender = didToName[controller] ?? controller;
|
|
2329
|
+
const date = docs.didDocumentMetadata?.updated ?? '';
|
|
2330
|
+
const to = message.to.map(did => didToName[did] ?? did);
|
|
2331
|
+
const cc = message.cc.map(did => didToName[did] ?? did);
|
|
2332
|
+
const attachments = await this.listDmailAttachments(did);
|
|
2333
|
+
dmailList[did] = {
|
|
2334
|
+
message,
|
|
2335
|
+
to,
|
|
2336
|
+
cc,
|
|
2337
|
+
tags,
|
|
2338
|
+
sender,
|
|
2339
|
+
date,
|
|
2340
|
+
attachments,
|
|
2341
|
+
docs,
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
return dmailList;
|
|
2345
|
+
}
|
|
2346
|
+
verifyTagList(tags) {
|
|
2347
|
+
if (!Array.isArray(tags)) {
|
|
2348
|
+
throw new InvalidParameterError('tags');
|
|
2349
|
+
}
|
|
2350
|
+
const tagSet = new Set();
|
|
2351
|
+
for (const tag of tags) {
|
|
2352
|
+
try {
|
|
2353
|
+
tagSet.add(this.validateName(tag));
|
|
2354
|
+
}
|
|
2355
|
+
catch (error) {
|
|
2356
|
+
throw new InvalidParameterError(`Invalid tag: '${tag}'`);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
return tagSet.size > 0 ? Array.from(tagSet) : [];
|
|
2360
|
+
}
|
|
2361
|
+
async fileDmail(did, tags) {
|
|
2362
|
+
const verifiedTags = this.verifyTagList(tags);
|
|
2363
|
+
await this.mutateWallet(async (wallet) => {
|
|
2364
|
+
const id = await this.fetchIdInfo(undefined, wallet);
|
|
2365
|
+
if (!id.dmail) {
|
|
2366
|
+
id.dmail = {};
|
|
2367
|
+
}
|
|
2368
|
+
id.dmail[did] = { tags: verifiedTags };
|
|
2369
|
+
});
|
|
2370
|
+
return true;
|
|
2371
|
+
}
|
|
2372
|
+
async removeDmail(did) {
|
|
2373
|
+
await this.mutateWallet(async (wallet) => {
|
|
2374
|
+
const id = await this.fetchIdInfo(undefined, wallet);
|
|
2375
|
+
if (!id.dmail || !id.dmail[did]) {
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
delete id.dmail[did];
|
|
2379
|
+
});
|
|
2380
|
+
return true;
|
|
2381
|
+
}
|
|
2382
|
+
async verifyRecipientList(list) {
|
|
2383
|
+
if (!Array.isArray(list)) {
|
|
2384
|
+
throw new InvalidParameterError('list');
|
|
2385
|
+
}
|
|
2386
|
+
const nameList = await this.listNames({ includeIDs: true });
|
|
2387
|
+
let newList = [];
|
|
2388
|
+
for (const id of list) {
|
|
2389
|
+
if (typeof id !== 'string') {
|
|
2390
|
+
throw new InvalidParameterError(`Invalid recipient type: ${typeof id}`);
|
|
2391
|
+
}
|
|
2392
|
+
if (id in nameList) {
|
|
2393
|
+
const did = nameList[id];
|
|
2394
|
+
const isAgent = await this.testAgent(did);
|
|
2395
|
+
if (isAgent) {
|
|
2396
|
+
newList.push(did);
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
throw new InvalidParameterError(`Invalid recipient: ${id}`);
|
|
2400
|
+
}
|
|
2401
|
+
if (isValidDID(id)) {
|
|
2402
|
+
newList.push(id);
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
throw new InvalidParameterError(`Invalid recipient: ${id}`);
|
|
2406
|
+
}
|
|
2407
|
+
return newList;
|
|
2408
|
+
}
|
|
2409
|
+
async verifyDmail(message) {
|
|
2410
|
+
const to = await this.verifyRecipientList(message.to);
|
|
2411
|
+
const cc = await this.verifyRecipientList(message.cc);
|
|
2412
|
+
if (to.length === 0) {
|
|
2413
|
+
throw new InvalidParameterError('dmail.to');
|
|
2414
|
+
}
|
|
2415
|
+
if (!message.subject || typeof message.subject !== 'string' || message.subject.trim() === '') {
|
|
2416
|
+
throw new InvalidParameterError('dmail.subject');
|
|
2417
|
+
}
|
|
2418
|
+
if (!message.body || typeof message.body !== 'string' || message.body.trim() === '') {
|
|
2419
|
+
throw new InvalidParameterError('dmail.body');
|
|
2420
|
+
}
|
|
2421
|
+
return {
|
|
2422
|
+
...message,
|
|
2423
|
+
to,
|
|
2424
|
+
cc,
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
async createDmail(message, options = {}) {
|
|
2428
|
+
const dmail = await this.verifyDmail(message);
|
|
2429
|
+
const did = await this.createGroupVault(options);
|
|
2430
|
+
for (const toDID of dmail.to) {
|
|
2431
|
+
await this.addGroupVaultMember(did, toDID);
|
|
2432
|
+
}
|
|
2433
|
+
for (const ccDID of dmail.cc) {
|
|
2434
|
+
await this.addGroupVaultMember(did, ccDID);
|
|
2435
|
+
}
|
|
2436
|
+
const buffer = Buffer.from(JSON.stringify({ dmail }), 'utf-8');
|
|
2437
|
+
await this.addGroupVaultItem(did, DmailTags.DMAIL, buffer);
|
|
2438
|
+
await this.fileDmail(did, [DmailTags.DRAFT]);
|
|
2439
|
+
return did;
|
|
2440
|
+
}
|
|
2441
|
+
async updateDmail(did, message) {
|
|
2442
|
+
const dmail = await this.verifyDmail(message);
|
|
2443
|
+
for (const toDID of dmail.to) {
|
|
2444
|
+
await this.addGroupVaultMember(did, toDID);
|
|
2445
|
+
}
|
|
2446
|
+
for (const ccDID of dmail.cc) {
|
|
2447
|
+
await this.addGroupVaultMember(did, ccDID);
|
|
2448
|
+
}
|
|
2449
|
+
const buffer = Buffer.from(JSON.stringify({ dmail }), 'utf-8');
|
|
2450
|
+
return this.addGroupVaultItem(did, DmailTags.DMAIL, buffer);
|
|
2451
|
+
}
|
|
2452
|
+
async sendDmail(did) {
|
|
2453
|
+
const dmail = await this.getDmailMessage(did);
|
|
2454
|
+
if (!dmail) {
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
const registry = this.ephemeralRegistry;
|
|
2458
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
|
|
2459
|
+
const message = {
|
|
2460
|
+
to: [...dmail.to, ...dmail.cc],
|
|
2461
|
+
dids: [did],
|
|
2462
|
+
};
|
|
2463
|
+
const notice = await this.createNotice(message, { registry, validUntil });
|
|
2464
|
+
if (notice) {
|
|
2465
|
+
await this.fileDmail(did, [DmailTags.SENT]);
|
|
2466
|
+
}
|
|
2467
|
+
return notice;
|
|
2468
|
+
}
|
|
2469
|
+
async getDmailMessage(did, options) {
|
|
2470
|
+
const isGroupVault = await this.testGroupVault(did, options);
|
|
2471
|
+
if (!isGroupVault) {
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
const buffer = await this.getGroupVaultItem(did, DmailTags.DMAIL, options);
|
|
2475
|
+
if (!buffer) {
|
|
2476
|
+
return null;
|
|
2477
|
+
}
|
|
2478
|
+
try {
|
|
2479
|
+
const data = JSON.parse(buffer.toString('utf-8'));
|
|
2480
|
+
return data.dmail;
|
|
2481
|
+
}
|
|
2482
|
+
catch (error) {
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
async listDmailAttachments(did, options) {
|
|
2487
|
+
let items = await this.listGroupVaultItems(did, options);
|
|
2488
|
+
delete items[DmailTags.DMAIL]; // Remove the dmail item itself from attachments
|
|
2489
|
+
return items;
|
|
2490
|
+
}
|
|
2491
|
+
async addDmailAttachment(did, name, buffer) {
|
|
2492
|
+
if (name === DmailTags.DMAIL) {
|
|
2493
|
+
throw new InvalidParameterError('Cannot add attachment with reserved name "dmail"');
|
|
2494
|
+
}
|
|
2495
|
+
return this.addGroupVaultItem(did, name, buffer);
|
|
2496
|
+
}
|
|
2497
|
+
async removeDmailAttachment(did, name) {
|
|
2498
|
+
if (name === DmailTags.DMAIL) {
|
|
2499
|
+
throw new InvalidParameterError('Cannot remove attachment with reserved name "dmail"');
|
|
2500
|
+
}
|
|
2501
|
+
return this.removeGroupVaultItem(did, name);
|
|
2502
|
+
}
|
|
2503
|
+
async getDmailAttachment(did, name) {
|
|
2504
|
+
return this.getGroupVaultItem(did, name);
|
|
2505
|
+
}
|
|
2506
|
+
async importDmail(did) {
|
|
2507
|
+
const dmail = await this.getDmailMessage(did);
|
|
2508
|
+
if (!dmail) {
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
return this.fileDmail(did, [DmailTags.INBOX, DmailTags.UNREAD]);
|
|
2512
|
+
}
|
|
2513
|
+
async verifyDIDList(didList) {
|
|
2514
|
+
if (!Array.isArray(didList)) {
|
|
2515
|
+
throw new InvalidParameterError('didList');
|
|
2516
|
+
}
|
|
2517
|
+
for (const did of didList) {
|
|
2518
|
+
if (!isValidDID(did)) {
|
|
2519
|
+
throw new InvalidParameterError(`Invalid DID: ${did}`);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return didList;
|
|
2523
|
+
}
|
|
2524
|
+
async verifyNotice(notice) {
|
|
2525
|
+
const to = await this.verifyRecipientList(notice.to);
|
|
2526
|
+
const dids = await this.verifyDIDList(notice.dids);
|
|
2527
|
+
if (to.length === 0) {
|
|
2528
|
+
throw new InvalidParameterError('notice.to');
|
|
2529
|
+
}
|
|
2530
|
+
if (dids.length === 0) {
|
|
2531
|
+
throw new InvalidParameterError('notice.dids');
|
|
2532
|
+
}
|
|
2533
|
+
return { to, dids };
|
|
2534
|
+
}
|
|
2535
|
+
async createNotice(message, options = {}) {
|
|
2536
|
+
const notice = await this.verifyNotice(message);
|
|
2537
|
+
return this.createAsset({ notice }, options);
|
|
2538
|
+
}
|
|
2539
|
+
async updateNotice(id, message) {
|
|
2540
|
+
const notice = await this.verifyNotice(message);
|
|
2541
|
+
return this.updateAsset(id, { notice });
|
|
2542
|
+
}
|
|
2543
|
+
async addToNotices(did, tags) {
|
|
2544
|
+
const verifiedTags = this.verifyTagList(tags);
|
|
2545
|
+
await this.mutateWallet(async (wallet) => {
|
|
2546
|
+
const id = await this.fetchIdInfo(undefined, wallet);
|
|
2547
|
+
if (!id.notices)
|
|
2548
|
+
id.notices = {};
|
|
2549
|
+
id.notices[did] = { tags: verifiedTags };
|
|
2550
|
+
});
|
|
2551
|
+
return true;
|
|
2552
|
+
}
|
|
2553
|
+
async importNotice(did) {
|
|
2554
|
+
const wallet = await this.loadWallet();
|
|
2555
|
+
const id = await this.fetchIdInfo(undefined, wallet);
|
|
2556
|
+
if (id.notices && id.notices[did]) {
|
|
2557
|
+
return true; // Already imported
|
|
2558
|
+
}
|
|
2559
|
+
const asset = await this.resolveAsset(did);
|
|
2560
|
+
if (!asset || !asset.notice) {
|
|
2561
|
+
return false; // Not a notice
|
|
2562
|
+
}
|
|
2563
|
+
if (!asset.notice.to.includes(id.did)) {
|
|
2564
|
+
return false; // Not for this user
|
|
2565
|
+
}
|
|
2566
|
+
for (const noticeDID of asset.notice.dids) {
|
|
2567
|
+
const dmail = await this.getDmailMessage(noticeDID);
|
|
2568
|
+
if (dmail) {
|
|
2569
|
+
const imported = await this.importDmail(noticeDID);
|
|
2570
|
+
if (imported) {
|
|
2571
|
+
await this.addToNotices(did, [NoticeTags.DMAIL]);
|
|
2572
|
+
}
|
|
2573
|
+
continue;
|
|
2574
|
+
}
|
|
2575
|
+
const isBallot = await this.isBallot(noticeDID);
|
|
2576
|
+
if (isBallot) {
|
|
2577
|
+
let imported = false;
|
|
2578
|
+
try {
|
|
2579
|
+
imported = await this.updatePoll(noticeDID);
|
|
2580
|
+
}
|
|
2581
|
+
catch { }
|
|
2582
|
+
if (imported) {
|
|
2583
|
+
await this.addToNotices(did, [NoticeTags.BALLOT]);
|
|
2584
|
+
}
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
const poll = await this.getPoll(noticeDID);
|
|
2588
|
+
if (poll) {
|
|
2589
|
+
const names = await this.listNames();
|
|
2590
|
+
if (!Object.values(names).includes(noticeDID)) {
|
|
2591
|
+
await this.addUnnamedPoll(noticeDID);
|
|
2592
|
+
}
|
|
2593
|
+
await this.addToNotices(did, [NoticeTags.POLL]);
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
const isCredential = await this.acceptCredential(noticeDID);
|
|
2597
|
+
if (isCredential) {
|
|
2598
|
+
await this.addToNotices(did, [NoticeTags.CREDENTIAL]);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
return false;
|
|
2602
|
+
}
|
|
2603
|
+
return true;
|
|
2604
|
+
}
|
|
2605
|
+
async searchNotices() {
|
|
2606
|
+
if (!this.searchEngine) {
|
|
2607
|
+
return false; // Search engine not available
|
|
2608
|
+
}
|
|
2609
|
+
const id = await this.fetchIdInfo();
|
|
2610
|
+
if (!id.notices) {
|
|
2611
|
+
id.notices = {};
|
|
2612
|
+
}
|
|
2613
|
+
// Search for all notice DIDs sent to the current ID
|
|
2614
|
+
const where = {
|
|
2615
|
+
"didDocumentData.notice.to[*]": {
|
|
2616
|
+
"$in": [id.did]
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
let notices;
|
|
2620
|
+
try {
|
|
2621
|
+
// TBD search engine should not return expired notices
|
|
2622
|
+
notices = await this.searchEngine.search({ where });
|
|
2623
|
+
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
throw new KeymasterError('Failed to search for notices');
|
|
2626
|
+
}
|
|
2627
|
+
for (const notice of notices) {
|
|
2628
|
+
if (notice in id.notices) {
|
|
2629
|
+
continue; // Already imported
|
|
2630
|
+
}
|
|
2631
|
+
try {
|
|
2632
|
+
await this.importNotice(notice);
|
|
2633
|
+
}
|
|
2634
|
+
catch (error) {
|
|
2635
|
+
continue; // Skip if notice is expired or invalid
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
async cleanupNotices() {
|
|
2641
|
+
await this.mutateWallet(async (wallet) => {
|
|
2642
|
+
const id = await this.fetchIdInfo(undefined, wallet);
|
|
2643
|
+
if (!id.notices) {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
for (const nDid of Object.keys(id.notices)) {
|
|
2647
|
+
try {
|
|
2648
|
+
const asset = await this.resolveAsset(nDid);
|
|
2649
|
+
if (!asset || !asset.notice) {
|
|
2650
|
+
delete id.notices[nDid]; // revoked or invalid
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
catch {
|
|
2654
|
+
delete id.notices[nDid]; // expired/unresolvable
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
});
|
|
2658
|
+
return true;
|
|
2659
|
+
}
|
|
2660
|
+
async refreshNotices() {
|
|
2661
|
+
await this.searchNotices();
|
|
2662
|
+
return this.cleanupNotices();
|
|
2663
|
+
}
|
|
2664
|
+
async exportEncryptedWallet() {
|
|
2665
|
+
const wallet = await this.loadWallet();
|
|
2666
|
+
return this.encryptWalletForStorage(wallet);
|
|
2667
|
+
}
|
|
2668
|
+
async isBallot(ballotDid) {
|
|
2669
|
+
let payload;
|
|
2670
|
+
try {
|
|
2671
|
+
payload = await this.decryptJSON(ballotDid);
|
|
2672
|
+
}
|
|
2673
|
+
catch {
|
|
2674
|
+
return false;
|
|
2675
|
+
}
|
|
2676
|
+
return payload && typeof payload.poll === "string" && typeof payload.vote === "number";
|
|
2677
|
+
}
|
|
2678
|
+
async addUnnamedPoll(did) {
|
|
2679
|
+
const fallbackName = did.slice(-32);
|
|
2680
|
+
try {
|
|
2681
|
+
await this.addName(fallbackName, did);
|
|
2682
|
+
}
|
|
2683
|
+
catch { }
|
|
2684
|
+
}
|
|
2685
|
+
async getHDKeyFromCacheOrMnemonic(wallet) {
|
|
2686
|
+
if (this._hdkeyCache) {
|
|
2687
|
+
return this._hdkeyCache;
|
|
2688
|
+
}
|
|
2689
|
+
const mnemonic = await this.getMnemonicForDerivation(wallet);
|
|
2690
|
+
this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
|
|
2691
|
+
return this._hdkeyCache;
|
|
2692
|
+
}
|
|
2693
|
+
async encryptWalletForStorage(decrypted) {
|
|
2694
|
+
const { version, seed, ...rest } = decrypted;
|
|
2695
|
+
const safeSeed = { mnemonicEnc: seed.mnemonicEnc };
|
|
2696
|
+
const hdkey = await this.getHDKeyFromCacheOrMnemonic(decrypted);
|
|
2697
|
+
const { publicJwk, privateJwk } = this.cipher.generateJwk(hdkey.privateKey);
|
|
2698
|
+
const plaintext = JSON.stringify(rest);
|
|
2699
|
+
const enc = this.cipher.encryptMessage(publicJwk, privateJwk, plaintext);
|
|
2700
|
+
return { version: version, seed: safeSeed, enc };
|
|
2701
|
+
}
|
|
2702
|
+
async decryptWalletFromStorage(stored) {
|
|
2703
|
+
let mnemonic;
|
|
2704
|
+
try {
|
|
2705
|
+
mnemonic = await decMnemonic(stored.seed.mnemonicEnc, this.passphrase);
|
|
2706
|
+
}
|
|
2707
|
+
catch {
|
|
2708
|
+
throw new KeymasterError('Incorrect passphrase.');
|
|
2709
|
+
}
|
|
2710
|
+
this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
|
|
2711
|
+
const { publicJwk, privateJwk } = this.cipher.generateJwk(this._hdkeyCache.privateKey);
|
|
2712
|
+
const plaintext = this.cipher.decryptMessage(publicJwk, privateJwk, stored.enc);
|
|
2713
|
+
const data = JSON.parse(plaintext);
|
|
2714
|
+
const wallet = { version: stored.version, seed: stored.seed, ...data };
|
|
2715
|
+
return wallet;
|
|
2716
|
+
}
|
|
2717
|
+
async decryptWallet(wallet) {
|
|
2718
|
+
if (isWalletEncFile(wallet)) {
|
|
2719
|
+
wallet = await this.decryptWalletFromStorage(wallet);
|
|
2720
|
+
}
|
|
2721
|
+
if (!isWalletFile(wallet)) {
|
|
2722
|
+
throw new KeymasterError("Unsupported wallet version.");
|
|
2723
|
+
}
|
|
2724
|
+
return wallet;
|
|
2725
|
+
}
|
|
2726
|
+
async upgradeWallet(wallet) {
|
|
2727
|
+
if (wallet.version !== 1) {
|
|
2728
|
+
throw new KeymasterError("Unsupported wallet version.");
|
|
2729
|
+
}
|
|
2730
|
+
return wallet;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
//# sourceMappingURL=keymaster.js.map
|