@didcid/keymaster 0.3.9 → 0.4.0
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/README.md +2 -2
- package/dist/cjs/index.cjs +0 -1
- package/dist/cjs/keymaster-client.cjs +88 -15
- package/dist/cjs/keymaster.cjs +1990 -234
- package/dist/cjs/node.cjs +0 -1
- package/dist/esm/cli.js +174 -16
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/keymaster-client.js +88 -15
- package/dist/esm/keymaster-client.js.map +1 -1
- package/dist/esm/keymaster.js +415 -234
- package/dist/esm/keymaster.js.map +1 -1
- package/dist/types/keymaster-client.d.ts +19 -12
- package/dist/types/keymaster.d.ts +25 -13
- package/dist/types/types.d.ts +36 -25
- package/package.json +4 -12
- package/dist/cjs/encryption.cjs +0 -59
- package/dist/esm/encryption.js +0 -55
- package/dist/esm/encryption.js.map +0 -1
- package/dist/types/encryption.d.ts +0 -10
package/dist/esm/keymaster.js
CHANGED
|
@@ -4,7 +4,7 @@ import { base64url } from 'multiformats/bases/base64';
|
|
|
4
4
|
import { InvalidDIDError, InvalidParameterError, KeymasterError, UnknownIDError } from '@didcid/common/errors';
|
|
5
5
|
import { isWalletEncFile, isWalletFile } from './db/typeGuards.js';
|
|
6
6
|
import { isValidDID } from '@didcid/ipfs/utils';
|
|
7
|
-
import {
|
|
7
|
+
import { decryptWithPassphrase, encryptWithPassphrase } from '@didcid/cipher/passphrase';
|
|
8
8
|
function hexToBase64url(hex) {
|
|
9
9
|
const bytes = Buffer.from(hex, 'hex');
|
|
10
10
|
return base64url.baseEncode(bytes);
|
|
@@ -42,6 +42,11 @@ export var NoticeTags;
|
|
|
42
42
|
NoticeTags["POLL"] = "poll";
|
|
43
43
|
NoticeTags["CREDENTIAL"] = "credential";
|
|
44
44
|
})(NoticeTags || (NoticeTags = {}));
|
|
45
|
+
export var PollItems;
|
|
46
|
+
(function (PollItems) {
|
|
47
|
+
PollItems["POLL"] = "poll";
|
|
48
|
+
PollItems["RESULTS"] = "results";
|
|
49
|
+
})(PollItems || (PollItems = {}));
|
|
45
50
|
export default class Keymaster {
|
|
46
51
|
passphrase;
|
|
47
52
|
gatekeeper;
|
|
@@ -129,7 +134,7 @@ export default class Keymaster {
|
|
|
129
134
|
catch (error) {
|
|
130
135
|
throw new InvalidParameterError('mnemonic');
|
|
131
136
|
}
|
|
132
|
-
const mnemonicEnc = await
|
|
137
|
+
const mnemonicEnc = await encryptWithPassphrase(mnemonic, this.passphrase);
|
|
133
138
|
const wallet = {
|
|
134
139
|
version: 2,
|
|
135
140
|
seed: { mnemonicEnc },
|
|
@@ -147,7 +152,7 @@ export default class Keymaster {
|
|
|
147
152
|
return this.getMnemonicForDerivation(wallet);
|
|
148
153
|
}
|
|
149
154
|
async getMnemonicForDerivation(wallet) {
|
|
150
|
-
return
|
|
155
|
+
return decryptWithPassphrase(wallet.seed.mnemonicEnc, this.passphrase);
|
|
151
156
|
}
|
|
152
157
|
async checkWallet() {
|
|
153
158
|
const wallet = await this.loadWallet();
|
|
@@ -359,7 +364,7 @@ export default class Keymaster {
|
|
|
359
364
|
const keypair = await this.hdKeyPair();
|
|
360
365
|
const seedBank = await this.resolveSeedBank();
|
|
361
366
|
const msg = JSON.stringify(wallet);
|
|
362
|
-
const backup = this.cipher.encryptMessage(keypair.publicJwk,
|
|
367
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, msg);
|
|
363
368
|
const operation = {
|
|
364
369
|
type: "create",
|
|
365
370
|
created: new Date().toISOString(),
|
|
@@ -412,12 +417,12 @@ export default class Keymaster {
|
|
|
412
417
|
if (typeof castData.backup !== 'string') {
|
|
413
418
|
throw new InvalidParameterError('Asset "backup" is missing or not a string');
|
|
414
419
|
}
|
|
415
|
-
const backup = this.cipher.decryptMessage(keypair.
|
|
420
|
+
const backup = this.cipher.decryptMessage(keypair.privateJwk, castData.backup, keypair.publicJwk);
|
|
416
421
|
let wallet = JSON.parse(backup);
|
|
417
422
|
if (isWalletFile(wallet)) {
|
|
418
423
|
const mnemonic = await this.decryptMnemonic();
|
|
419
424
|
// Backup might have a different mnemonic passphase so re-encrypt
|
|
420
|
-
wallet.seed.mnemonicEnc = await
|
|
425
|
+
wallet.seed.mnemonicEnc = await encryptWithPassphrase(mnemonic, this.passphrase);
|
|
421
426
|
}
|
|
422
427
|
await this.mutateWallet(async (current) => {
|
|
423
428
|
// Clear all existing properties from the current wallet
|
|
@@ -581,7 +586,7 @@ export default class Keymaster {
|
|
|
581
586
|
const cloneData = { ...assetData, cloned: assetDoc.didDocument.id };
|
|
582
587
|
return this.createAsset(cloneData, options);
|
|
583
588
|
}
|
|
584
|
-
async generateImageAsset(buffer) {
|
|
589
|
+
async generateImageAsset(filename, buffer) {
|
|
585
590
|
let metadata;
|
|
586
591
|
try {
|
|
587
592
|
metadata = imageSize(buffer);
|
|
@@ -590,33 +595,38 @@ export default class Keymaster {
|
|
|
590
595
|
throw new InvalidParameterError('buffer');
|
|
591
596
|
}
|
|
592
597
|
const cid = await this.gatekeeper.addData(buffer);
|
|
593
|
-
const
|
|
598
|
+
const file = {
|
|
594
599
|
cid,
|
|
600
|
+
filename,
|
|
601
|
+
type: `image/${metadata.type}`,
|
|
595
602
|
bytes: buffer.length,
|
|
596
|
-
...metadata,
|
|
597
|
-
type: `image/${metadata.type}`
|
|
598
603
|
};
|
|
599
|
-
|
|
604
|
+
const image = {
|
|
605
|
+
width: metadata.width,
|
|
606
|
+
height: metadata.height,
|
|
607
|
+
};
|
|
608
|
+
return { file, image };
|
|
600
609
|
}
|
|
601
610
|
async createImage(buffer, options = {}) {
|
|
602
|
-
const
|
|
603
|
-
|
|
611
|
+
const filename = options.filename || 'image';
|
|
612
|
+
const { file, image } = await this.generateImageAsset(filename, buffer);
|
|
613
|
+
return this.createAsset({ file, image }, options);
|
|
604
614
|
}
|
|
605
|
-
async updateImage(id, buffer) {
|
|
606
|
-
const
|
|
607
|
-
|
|
615
|
+
async updateImage(id, buffer, options = {}) {
|
|
616
|
+
const filename = options.filename || 'image';
|
|
617
|
+
const { file, image } = await this.generateImageAsset(filename, buffer);
|
|
618
|
+
return this.mergeData(id, { file, image });
|
|
608
619
|
}
|
|
609
620
|
async getImage(id) {
|
|
610
621
|
const asset = await this.resolveAsset(id);
|
|
611
|
-
|
|
612
|
-
if (!image || !image.cid) {
|
|
622
|
+
if (!asset.file || !asset.file.cid || !asset.image) {
|
|
613
623
|
return null;
|
|
614
624
|
}
|
|
615
|
-
const buffer = await this.gatekeeper.getData(
|
|
625
|
+
const buffer = await this.gatekeeper.getData(asset.file.cid);
|
|
616
626
|
if (buffer) {
|
|
617
|
-
|
|
627
|
+
asset.file.data = buffer;
|
|
618
628
|
}
|
|
619
|
-
return image;
|
|
629
|
+
return { file: asset.file, image: asset.image };
|
|
620
630
|
}
|
|
621
631
|
async testImage(id) {
|
|
622
632
|
try {
|
|
@@ -659,24 +669,32 @@ export default class Keymaster {
|
|
|
659
669
|
};
|
|
660
670
|
return file;
|
|
661
671
|
}
|
|
662
|
-
async
|
|
663
|
-
const filename = options.filename || '
|
|
664
|
-
const
|
|
665
|
-
return this.createAsset({
|
|
672
|
+
async createFile(buffer, options = {}) {
|
|
673
|
+
const filename = options.filename || 'file';
|
|
674
|
+
const file = await this.generateFileAsset(filename, buffer);
|
|
675
|
+
return this.createAsset({ file }, options);
|
|
666
676
|
}
|
|
667
|
-
async
|
|
668
|
-
const filename = options.filename || '
|
|
669
|
-
const
|
|
670
|
-
return this.mergeData(id, {
|
|
677
|
+
async updateFile(id, buffer, options = {}) {
|
|
678
|
+
const filename = options.filename || 'file';
|
|
679
|
+
const file = await this.generateFileAsset(filename, buffer);
|
|
680
|
+
return this.mergeData(id, { file });
|
|
671
681
|
}
|
|
672
|
-
async
|
|
682
|
+
async getFile(id) {
|
|
673
683
|
const asset = await this.resolveAsset(id);
|
|
674
|
-
|
|
684
|
+
const file = asset.file;
|
|
685
|
+
if (!file || !file.cid) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const buffer = await this.gatekeeper.getData(file.cid);
|
|
689
|
+
if (buffer) {
|
|
690
|
+
file.data = buffer;
|
|
691
|
+
}
|
|
692
|
+
return file;
|
|
675
693
|
}
|
|
676
|
-
async
|
|
694
|
+
async testFile(id) {
|
|
677
695
|
try {
|
|
678
|
-
const
|
|
679
|
-
return
|
|
696
|
+
const file = await this.getFile(id);
|
|
697
|
+
return file !== null;
|
|
680
698
|
}
|
|
681
699
|
catch (error) {
|
|
682
700
|
return false;
|
|
@@ -684,19 +702,16 @@ export default class Keymaster {
|
|
|
684
702
|
}
|
|
685
703
|
async encryptMessage(msg, receiver, options = {}) {
|
|
686
704
|
const { encryptForSender = true, includeHash = false, } = options;
|
|
687
|
-
const id = await this.fetchIdInfo();
|
|
688
705
|
const senderKeypair = await this.fetchKeyPair();
|
|
689
706
|
if (!senderKeypair) {
|
|
690
707
|
throw new KeymasterError('No valid sender keypair');
|
|
691
708
|
}
|
|
692
709
|
const doc = await this.resolveDID(receiver, { confirm: true });
|
|
693
710
|
const receivePublicJwk = this.getPublicKeyJwk(doc);
|
|
694
|
-
const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk,
|
|
695
|
-
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk,
|
|
711
|
+
const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk, msg) : null;
|
|
712
|
+
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, msg);
|
|
696
713
|
const cipher_hash = includeHash ? this.cipher.hashMessage(msg) : null;
|
|
697
714
|
const encrypted = {
|
|
698
|
-
sender: id.did,
|
|
699
|
-
created: new Date().toISOString(),
|
|
700
715
|
cipher_hash,
|
|
701
716
|
cipher_sender,
|
|
702
717
|
cipher_receiver,
|
|
@@ -712,7 +727,7 @@ export default class Keymaster {
|
|
|
712
727
|
const didkey = hdkey.derive(path);
|
|
713
728
|
const receiverKeypair = this.cipher.generateJwk(didkey.privateKey);
|
|
714
729
|
try {
|
|
715
|
-
return this.cipher.decryptMessage(
|
|
730
|
+
return this.cipher.decryptMessage(receiverKeypair.privateJwk, ciphertext, senderPublicJwk);
|
|
716
731
|
}
|
|
717
732
|
catch (error) {
|
|
718
733
|
index -= 1;
|
|
@@ -723,7 +738,8 @@ export default class Keymaster {
|
|
|
723
738
|
async decryptMessage(did) {
|
|
724
739
|
const wallet = await this.loadWallet();
|
|
725
740
|
const id = await this.fetchIdInfo();
|
|
726
|
-
const
|
|
741
|
+
const msgDoc = await this.resolveDID(did);
|
|
742
|
+
const asset = msgDoc.didDocumentData;
|
|
727
743
|
if (!asset) {
|
|
728
744
|
throw new InvalidParameterError('did not encrypted');
|
|
729
745
|
}
|
|
@@ -732,9 +748,16 @@ export default class Keymaster {
|
|
|
732
748
|
throw new InvalidParameterError('did not encrypted');
|
|
733
749
|
}
|
|
734
750
|
const crypt = (castAsset.encrypted ? castAsset.encrypted : castAsset);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
751
|
+
// Derive sender and created from the message DID document,
|
|
752
|
+
// falling back to fields in the asset for legacy messages
|
|
753
|
+
const sender = crypt.sender || msgDoc.didDocument?.controller;
|
|
754
|
+
const created = crypt.created || msgDoc.didDocumentMetadata?.created;
|
|
755
|
+
if (!sender) {
|
|
756
|
+
throw new InvalidParameterError('Sender DID could not be determined from message or DID document');
|
|
757
|
+
}
|
|
758
|
+
const senderDoc = await this.resolveDID(sender, { confirm: true, versionTime: created });
|
|
759
|
+
const senderPublicJwk = this.getPublicKeyJwk(senderDoc);
|
|
760
|
+
const ciphertext = (sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
|
|
738
761
|
return await this.decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext);
|
|
739
762
|
}
|
|
740
763
|
async encryptJSON(json, did, options = {}) {
|
|
@@ -1115,7 +1138,7 @@ export default class Keymaster {
|
|
|
1115
1138
|
id: idInfo,
|
|
1116
1139
|
};
|
|
1117
1140
|
const msg = JSON.stringify(data);
|
|
1118
|
-
const backup = this.cipher.encryptMessage(keypair.publicJwk,
|
|
1141
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, msg);
|
|
1119
1142
|
const doc = await this.resolveDID(idInfo.did);
|
|
1120
1143
|
const registry = doc.didDocumentRegistration?.registry;
|
|
1121
1144
|
if (!registry) {
|
|
@@ -1141,7 +1164,7 @@ export default class Keymaster {
|
|
|
1141
1164
|
if (typeof backupStore.backup !== 'string') {
|
|
1142
1165
|
throw new InvalidDIDError('backup not found in backupStore');
|
|
1143
1166
|
}
|
|
1144
|
-
const backup = this.cipher.decryptMessage(keypair.
|
|
1167
|
+
const backup = this.cipher.decryptMessage(keypair.privateJwk, backupStore.backup, keypair.publicJwk);
|
|
1145
1168
|
const data = JSON.parse(backup);
|
|
1146
1169
|
await this.mutateWallet((wallet) => {
|
|
1147
1170
|
if (wallet.ids[data.name]) {
|
|
@@ -1242,7 +1265,13 @@ export default class Keymaster {
|
|
|
1242
1265
|
validFrom = new Date().toISOString();
|
|
1243
1266
|
}
|
|
1244
1267
|
const id = await this.fetchIdInfo();
|
|
1245
|
-
|
|
1268
|
+
let subjectURI;
|
|
1269
|
+
try {
|
|
1270
|
+
subjectURI = await this.lookupDID(subjectId);
|
|
1271
|
+
}
|
|
1272
|
+
catch {
|
|
1273
|
+
subjectURI = subjectId;
|
|
1274
|
+
}
|
|
1246
1275
|
const vc = {
|
|
1247
1276
|
"@context": [
|
|
1248
1277
|
"https://www.w3.org/ns/credentials/v2",
|
|
@@ -1253,7 +1282,7 @@ export default class Keymaster {
|
|
|
1253
1282
|
validFrom,
|
|
1254
1283
|
validUntil,
|
|
1255
1284
|
credentialSubject: {
|
|
1256
|
-
id:
|
|
1285
|
+
id: subjectURI,
|
|
1257
1286
|
},
|
|
1258
1287
|
};
|
|
1259
1288
|
// If schema provided, add credentialSchema and generate claims from schema
|
|
@@ -1278,7 +1307,7 @@ export default class Keymaster {
|
|
|
1278
1307
|
}
|
|
1279
1308
|
if (claims && Object.keys(claims).length) {
|
|
1280
1309
|
vc.credentialSubject = {
|
|
1281
|
-
id:
|
|
1310
|
+
id: subjectURI,
|
|
1282
1311
|
...claims,
|
|
1283
1312
|
};
|
|
1284
1313
|
}
|
|
@@ -1293,21 +1322,32 @@ export default class Keymaster {
|
|
|
1293
1322
|
throw new InvalidParameterError('credential.issuer');
|
|
1294
1323
|
}
|
|
1295
1324
|
const signed = await this.addProof(credential);
|
|
1296
|
-
|
|
1325
|
+
const subjectId = credential.credentialSubject.id;
|
|
1326
|
+
if (this.isManagedDID(subjectId)) {
|
|
1327
|
+
return this.encryptJSON(signed, subjectId, { ...options, includeHash: true });
|
|
1328
|
+
}
|
|
1329
|
+
return this.encryptJSON(signed, id.did, { ...options, includeHash: true, encryptForSender: false });
|
|
1297
1330
|
}
|
|
1298
1331
|
async sendCredential(did, options = {}) {
|
|
1299
1332
|
const vc = await this.getCredential(did);
|
|
1300
1333
|
if (!vc) {
|
|
1301
1334
|
return null;
|
|
1302
1335
|
}
|
|
1336
|
+
const subjectId = vc.credentialSubject.id;
|
|
1337
|
+
if (!this.isManagedDID(subjectId)) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1303
1340
|
const registry = this.ephemeralRegistry;
|
|
1304
1341
|
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
|
|
1305
1342
|
const message = {
|
|
1306
|
-
to: [
|
|
1343
|
+
to: [subjectId],
|
|
1307
1344
|
dids: [did],
|
|
1308
1345
|
};
|
|
1309
1346
|
return this.createNotice(message, { registry, validUntil, ...options });
|
|
1310
1347
|
}
|
|
1348
|
+
isManagedDID(value) {
|
|
1349
|
+
return value.startsWith('did:cid:');
|
|
1350
|
+
}
|
|
1311
1351
|
isVerifiableCredential(obj) {
|
|
1312
1352
|
if (typeof obj !== 'object' || !obj) {
|
|
1313
1353
|
return false;
|
|
@@ -1329,24 +1369,29 @@ export default class Keymaster {
|
|
|
1329
1369
|
delete credential.proof;
|
|
1330
1370
|
const signed = await this.addProof(credential);
|
|
1331
1371
|
const msg = JSON.stringify(signed);
|
|
1332
|
-
const id = await this.fetchIdInfo();
|
|
1333
1372
|
const senderKeypair = await this.fetchKeyPair();
|
|
1334
1373
|
if (!senderKeypair) {
|
|
1335
1374
|
throw new KeymasterError('No valid sender keypair');
|
|
1336
1375
|
}
|
|
1337
1376
|
const holder = credential.credentialSubject.id;
|
|
1338
|
-
const holderDoc = await this.resolveDID(holder, { confirm: true });
|
|
1339
|
-
const receivePublicJwk = this.getPublicKeyJwk(holderDoc);
|
|
1340
|
-
const cipher_sender = this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg);
|
|
1341
|
-
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
|
|
1342
1377
|
const msgHash = this.cipher.hashMessage(msg);
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1378
|
+
let encrypted;
|
|
1379
|
+
if (this.isManagedDID(holder)) {
|
|
1380
|
+
const holderDoc = await this.resolveDID(holder, { confirm: true });
|
|
1381
|
+
const receivePublicJwk = this.getPublicKeyJwk(holderDoc);
|
|
1382
|
+
encrypted = {
|
|
1383
|
+
cipher_hash: msgHash,
|
|
1384
|
+
cipher_sender: this.cipher.encryptMessage(senderKeypair.publicJwk, msg),
|
|
1385
|
+
cipher_receiver: this.cipher.encryptMessage(receivePublicJwk, msg),
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
encrypted = {
|
|
1390
|
+
cipher_hash: msgHash,
|
|
1391
|
+
cipher_sender: null,
|
|
1392
|
+
cipher_receiver: this.cipher.encryptMessage(senderKeypair.publicJwk, msg),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1350
1395
|
return this.updateDID(did, { didDocumentData: { encrypted } });
|
|
1351
1396
|
}
|
|
1352
1397
|
async revokeCredential(credential) {
|
|
@@ -1842,72 +1887,64 @@ export default class Keymaster {
|
|
|
1842
1887
|
const nextWeek = new Date();
|
|
1843
1888
|
nextWeek.setDate(now.getDate() + 7);
|
|
1844
1889
|
return {
|
|
1845
|
-
|
|
1846
|
-
|
|
1890
|
+
version: 2,
|
|
1891
|
+
name: 'poll-name',
|
|
1847
1892
|
description: 'What is this poll about?',
|
|
1848
|
-
roster: 'DID of the eligible voter group',
|
|
1849
1893
|
options: ['yes', 'no', 'abstain'],
|
|
1850
1894
|
deadline: nextWeek.toISOString(),
|
|
1851
1895
|
};
|
|
1852
1896
|
}
|
|
1853
|
-
async createPoll(
|
|
1854
|
-
if (
|
|
1855
|
-
throw new InvalidParameterError('poll');
|
|
1856
|
-
}
|
|
1857
|
-
if (poll.version !== 1) {
|
|
1897
|
+
async createPoll(config, options = {}) {
|
|
1898
|
+
if (config.version !== 2) {
|
|
1858
1899
|
throw new InvalidParameterError('poll.version');
|
|
1859
1900
|
}
|
|
1860
|
-
if (!
|
|
1901
|
+
if (!config.name) {
|
|
1902
|
+
throw new InvalidParameterError('poll.name');
|
|
1903
|
+
}
|
|
1904
|
+
if (!config.description) {
|
|
1861
1905
|
throw new InvalidParameterError('poll.description');
|
|
1862
1906
|
}
|
|
1863
|
-
if (!
|
|
1907
|
+
if (!config.options || !Array.isArray(config.options) || config.options.length < 2 || config.options.length > 10) {
|
|
1864
1908
|
throw new InvalidParameterError('poll.options');
|
|
1865
1909
|
}
|
|
1866
|
-
if (!
|
|
1867
|
-
// eslint-disable-next-line
|
|
1868
|
-
throw new InvalidParameterError('poll.roster');
|
|
1869
|
-
}
|
|
1870
|
-
try {
|
|
1871
|
-
const isValidGroup = await this.testGroup(poll.roster);
|
|
1872
|
-
if (!isValidGroup) {
|
|
1873
|
-
throw new InvalidParameterError('poll.roster');
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
catch {
|
|
1877
|
-
throw new InvalidParameterError('poll.roster');
|
|
1878
|
-
}
|
|
1879
|
-
if (!poll.deadline) {
|
|
1880
|
-
// eslint-disable-next-line
|
|
1910
|
+
if (!config.deadline) {
|
|
1881
1911
|
throw new InvalidParameterError('poll.deadline');
|
|
1882
1912
|
}
|
|
1883
|
-
const deadline = new Date(
|
|
1913
|
+
const deadline = new Date(config.deadline);
|
|
1884
1914
|
if (isNaN(deadline.getTime())) {
|
|
1885
1915
|
throw new InvalidParameterError('poll.deadline');
|
|
1886
1916
|
}
|
|
1887
1917
|
if (deadline < new Date()) {
|
|
1888
1918
|
throw new InvalidParameterError('poll.deadline');
|
|
1889
1919
|
}
|
|
1890
|
-
|
|
1920
|
+
const vaultDid = await this.createVault(options);
|
|
1921
|
+
const buffer = Buffer.from(JSON.stringify(config), 'utf-8');
|
|
1922
|
+
await this.addVaultItem(vaultDid, PollItems.POLL, buffer);
|
|
1923
|
+
return vaultDid;
|
|
1891
1924
|
}
|
|
1892
1925
|
async getPoll(id) {
|
|
1893
|
-
const
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
if (castOldAsset.options) {
|
|
1897
|
-
return castOldAsset;
|
|
1926
|
+
const isVault = await this.testVault(id);
|
|
1927
|
+
if (!isVault) {
|
|
1928
|
+
return null;
|
|
1898
1929
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1930
|
+
try {
|
|
1931
|
+
const buffer = await this.getVaultItem(id, PollItems.POLL);
|
|
1932
|
+
if (!buffer) {
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
const config = JSON.parse(buffer.toString('utf-8'));
|
|
1936
|
+
return config;
|
|
1937
|
+
}
|
|
1938
|
+
catch {
|
|
1901
1939
|
return null;
|
|
1902
1940
|
}
|
|
1903
|
-
return castAsset.poll;
|
|
1904
1941
|
}
|
|
1905
1942
|
async testPoll(id) {
|
|
1906
1943
|
try {
|
|
1907
|
-
const
|
|
1908
|
-
return
|
|
1944
|
+
const config = await this.getPoll(id);
|
|
1945
|
+
return config !== null;
|
|
1909
1946
|
}
|
|
1910
|
-
catch
|
|
1947
|
+
catch {
|
|
1911
1948
|
return false;
|
|
1912
1949
|
}
|
|
1913
1950
|
}
|
|
@@ -1922,113 +1959,243 @@ export default class Keymaster {
|
|
|
1922
1959
|
}
|
|
1923
1960
|
return polls;
|
|
1924
1961
|
}
|
|
1962
|
+
async addPollVoter(pollId, memberId) {
|
|
1963
|
+
const config = await this.getPoll(pollId);
|
|
1964
|
+
if (!config) {
|
|
1965
|
+
throw new InvalidParameterError('pollId');
|
|
1966
|
+
}
|
|
1967
|
+
return this.addVaultMember(pollId, memberId);
|
|
1968
|
+
}
|
|
1969
|
+
async removePollVoter(pollId, memberId) {
|
|
1970
|
+
const config = await this.getPoll(pollId);
|
|
1971
|
+
if (!config) {
|
|
1972
|
+
throw new InvalidParameterError('pollId');
|
|
1973
|
+
}
|
|
1974
|
+
return this.removeVaultMember(pollId, memberId);
|
|
1975
|
+
}
|
|
1976
|
+
async listPollVoters(pollId) {
|
|
1977
|
+
const config = await this.getPoll(pollId);
|
|
1978
|
+
if (!config) {
|
|
1979
|
+
throw new InvalidParameterError('pollId');
|
|
1980
|
+
}
|
|
1981
|
+
return this.listVaultMembers(pollId);
|
|
1982
|
+
}
|
|
1925
1983
|
async viewPoll(pollId) {
|
|
1926
1984
|
const id = await this.fetchIdInfo();
|
|
1927
|
-
const
|
|
1928
|
-
if (!
|
|
1985
|
+
const config = await this.getPoll(pollId);
|
|
1986
|
+
if (!config) {
|
|
1929
1987
|
throw new InvalidParameterError('pollId');
|
|
1930
1988
|
}
|
|
1989
|
+
const doc = await this.resolveDID(pollId);
|
|
1990
|
+
const isOwner = (doc.didDocument?.controller === id.did);
|
|
1991
|
+
const voteExpired = Date.now() > new Date(config.deadline).getTime();
|
|
1992
|
+
let isEligible = false;
|
|
1931
1993
|
let hasVoted = false;
|
|
1932
|
-
|
|
1933
|
-
|
|
1994
|
+
const ballots = [];
|
|
1995
|
+
try {
|
|
1996
|
+
const vault = await this.getVault(pollId);
|
|
1997
|
+
const members = await this.listVaultMembers(pollId);
|
|
1998
|
+
isEligible = isOwner || !!members[id.did];
|
|
1999
|
+
const items = await this.listVaultItems(pollId);
|
|
2000
|
+
for (const itemName of Object.keys(items)) {
|
|
2001
|
+
if (itemName !== PollItems.POLL && itemName !== PollItems.RESULTS) {
|
|
2002
|
+
ballots.push(itemName);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
const myBallotKey = this.generateBallotKey(vault, id.did);
|
|
2006
|
+
hasVoted = ballots.includes(myBallotKey);
|
|
2007
|
+
}
|
|
2008
|
+
catch {
|
|
2009
|
+
isEligible = false;
|
|
1934
2010
|
}
|
|
1935
|
-
const voteExpired = Date.now() > new Date(poll.deadline).getTime();
|
|
1936
|
-
const isEligible = await this.testGroup(poll.roster, id.did);
|
|
1937
|
-
const doc = await this.resolveDID(pollId);
|
|
1938
2011
|
const view = {
|
|
1939
|
-
description:
|
|
1940
|
-
options:
|
|
1941
|
-
deadline:
|
|
1942
|
-
isOwner
|
|
1943
|
-
isEligible
|
|
1944
|
-
voteExpired
|
|
1945
|
-
hasVoted
|
|
2012
|
+
description: config.description,
|
|
2013
|
+
options: config.options,
|
|
2014
|
+
deadline: config.deadline,
|
|
2015
|
+
isOwner,
|
|
2016
|
+
isEligible,
|
|
2017
|
+
voteExpired,
|
|
2018
|
+
hasVoted,
|
|
2019
|
+
ballots,
|
|
1946
2020
|
};
|
|
1947
|
-
if (
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2021
|
+
if (isOwner) {
|
|
2022
|
+
view.results = await this.computePollResults(pollId, config);
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
try {
|
|
2026
|
+
const resultsBuffer = await this.getVaultItem(pollId, PollItems.RESULTS);
|
|
2027
|
+
if (resultsBuffer) {
|
|
2028
|
+
view.results = JSON.parse(resultsBuffer.toString('utf-8'));
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
catch { }
|
|
2032
|
+
}
|
|
2033
|
+
return view;
|
|
2034
|
+
}
|
|
2035
|
+
async computePollResults(pollId, config) {
|
|
2036
|
+
const vault = await this.getVault(pollId);
|
|
2037
|
+
const members = await this.listVaultMembers(pollId);
|
|
2038
|
+
const items = await this.listVaultItems(pollId);
|
|
2039
|
+
const results = {
|
|
2040
|
+
tally: [],
|
|
2041
|
+
ballots: [],
|
|
2042
|
+
};
|
|
2043
|
+
results.tally.push({ vote: 0, option: 'spoil', count: 0 });
|
|
2044
|
+
for (let i = 0; i < config.options.length; i++) {
|
|
2045
|
+
results.tally.push({ vote: i + 1, option: config.options[i], count: 0 });
|
|
2046
|
+
}
|
|
2047
|
+
// Build ballotKey → memberDID mapping
|
|
2048
|
+
const keyToMember = {};
|
|
2049
|
+
for (const memberDID of Object.keys(members)) {
|
|
2050
|
+
const ballotKey = this.generateBallotKey(vault, memberDID);
|
|
2051
|
+
keyToMember[ballotKey] = memberDID;
|
|
2052
|
+
}
|
|
2053
|
+
// Include owner in mapping
|
|
2054
|
+
const id = await this.fetchIdInfo();
|
|
2055
|
+
const ownerKey = this.generateBallotKey(vault, id.did);
|
|
2056
|
+
keyToMember[ownerKey] = id.did;
|
|
2057
|
+
let voted = 0;
|
|
2058
|
+
for (const [itemName, itemMeta] of Object.entries(items)) {
|
|
2059
|
+
if (itemName === PollItems.POLL || itemName === PollItems.RESULTS) {
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
const ballotBuffer = await this.getVaultItem(pollId, itemName);
|
|
2063
|
+
if (!ballotBuffer) {
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
const ballotDid = ballotBuffer.toString('utf-8');
|
|
2067
|
+
const decrypted = await this.decryptJSON(ballotDid);
|
|
2068
|
+
const vote = decrypted.vote;
|
|
2069
|
+
const voterDID = keyToMember[itemName] || itemName;
|
|
2070
|
+
if (results.ballots) {
|
|
2071
|
+
results.ballots.push({
|
|
2072
|
+
voter: voterDID,
|
|
2073
|
+
vote,
|
|
2074
|
+
option: vote === 0 ? 'spoil' : config.options[vote - 1],
|
|
2075
|
+
received: itemMeta.added || '',
|
|
1963
2076
|
});
|
|
1964
2077
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
const decrypted = await this.decryptJSON(ballot.ballot);
|
|
1968
|
-
const vote = decrypted.vote;
|
|
1969
|
-
if (results.ballots) {
|
|
1970
|
-
results.ballots.push({
|
|
1971
|
-
...ballot,
|
|
1972
|
-
voter,
|
|
1973
|
-
vote,
|
|
1974
|
-
option: poll.options[vote - 1],
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
voted += 1;
|
|
2078
|
+
voted += 1;
|
|
2079
|
+
if (vote >= 0 && vote < results.tally.length) {
|
|
1978
2080
|
results.tally[vote].count += 1;
|
|
1979
2081
|
}
|
|
1980
|
-
const roster = await this.getGroup(poll.roster);
|
|
1981
|
-
const total = roster.members.length;
|
|
1982
|
-
results.votes = {
|
|
1983
|
-
eligible: total,
|
|
1984
|
-
received: voted,
|
|
1985
|
-
pending: total - voted,
|
|
1986
|
-
};
|
|
1987
|
-
results.final = voteExpired || (voted === total);
|
|
1988
|
-
view.results = results;
|
|
1989
2082
|
}
|
|
1990
|
-
|
|
2083
|
+
const total = Object.keys(members).length + 1; // +1 for owner
|
|
2084
|
+
const voteExpired = Date.now() > new Date(config.deadline).getTime();
|
|
2085
|
+
results.votes = {
|
|
2086
|
+
eligible: total,
|
|
2087
|
+
received: voted,
|
|
2088
|
+
pending: total - voted,
|
|
2089
|
+
};
|
|
2090
|
+
results.final = voteExpired || (voted === total);
|
|
2091
|
+
return results;
|
|
1991
2092
|
}
|
|
1992
2093
|
async votePoll(pollId, vote, options = {}) {
|
|
1993
|
-
const { spoil = false } = options;
|
|
1994
2094
|
const id = await this.fetchIdInfo();
|
|
1995
2095
|
const didPoll = await this.lookupDID(pollId);
|
|
1996
2096
|
const doc = await this.resolveDID(didPoll);
|
|
1997
|
-
const
|
|
1998
|
-
if (!
|
|
2097
|
+
const config = await this.getPoll(pollId);
|
|
2098
|
+
if (!config) {
|
|
1999
2099
|
throw new InvalidParameterError('pollId');
|
|
2000
2100
|
}
|
|
2001
|
-
const eligible = await this.testGroup(poll.roster, id.did);
|
|
2002
|
-
const expired = Date.now() > new Date(poll.deadline).getTime();
|
|
2003
2101
|
const owner = doc.didDocument?.controller;
|
|
2004
2102
|
if (!owner) {
|
|
2005
|
-
throw new KeymasterError('owner
|
|
2103
|
+
throw new KeymasterError('owner missing from poll');
|
|
2104
|
+
}
|
|
2105
|
+
// Check vault membership
|
|
2106
|
+
let isEligible = false;
|
|
2107
|
+
if (id.did === owner) {
|
|
2108
|
+
isEligible = true;
|
|
2006
2109
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2110
|
+
else {
|
|
2111
|
+
try {
|
|
2112
|
+
const vault = await this.getVault(didPoll);
|
|
2113
|
+
await this.decryptVault(vault);
|
|
2114
|
+
isEligible = true;
|
|
2115
|
+
}
|
|
2116
|
+
catch {
|
|
2117
|
+
isEligible = false;
|
|
2118
|
+
}
|
|
2009
2119
|
}
|
|
2120
|
+
if (!isEligible) {
|
|
2121
|
+
throw new InvalidParameterError('voter is not a poll member');
|
|
2122
|
+
}
|
|
2123
|
+
const expired = Date.now() > new Date(config.deadline).getTime();
|
|
2010
2124
|
if (expired) {
|
|
2011
2125
|
throw new InvalidParameterError('poll has expired');
|
|
2012
2126
|
}
|
|
2013
|
-
|
|
2014
|
-
if (
|
|
2015
|
-
|
|
2016
|
-
poll: didPoll,
|
|
2017
|
-
vote: 0,
|
|
2018
|
-
};
|
|
2127
|
+
const max = config.options.length;
|
|
2128
|
+
if (!Number.isInteger(vote) || vote < 0 || vote > max) {
|
|
2129
|
+
throw new InvalidParameterError('vote');
|
|
2019
2130
|
}
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2131
|
+
const ballot = {
|
|
2132
|
+
poll: didPoll,
|
|
2133
|
+
vote: vote,
|
|
2134
|
+
};
|
|
2135
|
+
// Encrypt for owner and sender (voter can view their own ballot)
|
|
2136
|
+
return await this.encryptJSON(ballot, owner, options);
|
|
2137
|
+
}
|
|
2138
|
+
async sendPoll(pollId) {
|
|
2139
|
+
const didPoll = await this.lookupDID(pollId);
|
|
2140
|
+
const config = await this.getPoll(didPoll);
|
|
2141
|
+
if (!config) {
|
|
2142
|
+
throw new InvalidParameterError('pollId');
|
|
2143
|
+
}
|
|
2144
|
+
const members = await this.listVaultMembers(didPoll);
|
|
2145
|
+
const voters = Object.keys(members);
|
|
2146
|
+
if (voters.length === 0) {
|
|
2147
|
+
throw new KeymasterError('No poll voters found');
|
|
2148
|
+
}
|
|
2149
|
+
const registry = this.ephemeralRegistry;
|
|
2150
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
2151
|
+
const message = {
|
|
2152
|
+
to: voters,
|
|
2153
|
+
dids: [didPoll],
|
|
2154
|
+
};
|
|
2155
|
+
return this.createNotice(message, { registry, validUntil });
|
|
2156
|
+
}
|
|
2157
|
+
async sendBallot(ballotDid, pollId) {
|
|
2158
|
+
const didPoll = await this.lookupDID(pollId);
|
|
2159
|
+
const config = await this.getPoll(didPoll);
|
|
2160
|
+
if (!config) {
|
|
2161
|
+
throw new InvalidParameterError('pollId is not a valid poll');
|
|
2162
|
+
}
|
|
2163
|
+
const pollDoc = await this.resolveDID(didPoll);
|
|
2164
|
+
const ownerDid = pollDoc.didDocument?.controller;
|
|
2165
|
+
if (!ownerDid) {
|
|
2166
|
+
throw new KeymasterError('poll owner not found');
|
|
2167
|
+
}
|
|
2168
|
+
const registry = this.ephemeralRegistry;
|
|
2169
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
2170
|
+
const message = {
|
|
2171
|
+
to: [ownerDid],
|
|
2172
|
+
dids: [ballotDid],
|
|
2173
|
+
};
|
|
2174
|
+
return this.createNotice(message, { registry, validUntil });
|
|
2175
|
+
}
|
|
2176
|
+
async viewBallot(ballotDid) {
|
|
2177
|
+
const docBallot = await this.resolveDID(ballotDid);
|
|
2178
|
+
const voter = docBallot.didDocument?.controller;
|
|
2179
|
+
const result = {
|
|
2180
|
+
poll: '',
|
|
2181
|
+
voter: voter || undefined,
|
|
2182
|
+
};
|
|
2183
|
+
try {
|
|
2184
|
+
const data = await this.decryptJSON(ballotDid);
|
|
2185
|
+
result.poll = data.poll;
|
|
2186
|
+
result.vote = data.vote;
|
|
2187
|
+
const config = await this.getPoll(data.poll);
|
|
2188
|
+
if (config && data.vote > 0 && data.vote <= config.options.length) {
|
|
2189
|
+
result.option = config.options[data.vote - 1];
|
|
2024
2190
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2191
|
+
else if (data.vote === 0) {
|
|
2192
|
+
result.option = 'spoil';
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
catch {
|
|
2196
|
+
// Caller cannot decrypt (not the owner) — return limited info
|
|
2029
2197
|
}
|
|
2030
|
-
|
|
2031
|
-
return await this.encryptJSON(ballot, owner, { ...options, encryptForSender: false });
|
|
2198
|
+
return result;
|
|
2032
2199
|
}
|
|
2033
2200
|
async updatePoll(ballot) {
|
|
2034
2201
|
const id = await this.fetchIdInfo();
|
|
@@ -2038,7 +2205,7 @@ export default class Keymaster {
|
|
|
2038
2205
|
let dataBallot;
|
|
2039
2206
|
try {
|
|
2040
2207
|
dataBallot = await this.decryptJSON(didBallot);
|
|
2041
|
-
if (!dataBallot.poll ||
|
|
2208
|
+
if (!dataBallot.poll || dataBallot.vote === undefined) {
|
|
2042
2209
|
throw new InvalidParameterError('ballot');
|
|
2043
2210
|
}
|
|
2044
2211
|
}
|
|
@@ -2048,34 +2215,34 @@ export default class Keymaster {
|
|
|
2048
2215
|
const didPoll = dataBallot.poll;
|
|
2049
2216
|
const docPoll = await this.resolveDID(didPoll);
|
|
2050
2217
|
const didOwner = docPoll.didDocument.controller;
|
|
2051
|
-
const
|
|
2052
|
-
if (!
|
|
2218
|
+
const config = await this.getPoll(didPoll);
|
|
2219
|
+
if (!config) {
|
|
2053
2220
|
throw new KeymasterError('Cannot find poll related to ballot');
|
|
2054
2221
|
}
|
|
2055
2222
|
if (id.did !== didOwner) {
|
|
2056
2223
|
throw new InvalidParameterError('only owner can update a poll');
|
|
2057
2224
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2225
|
+
// Check voter is a vault member
|
|
2226
|
+
const vault = await this.getVault(didPoll);
|
|
2227
|
+
const voterBallotKey = this.generateBallotKey(vault, didVoter);
|
|
2228
|
+
const members = await this.listVaultMembers(didPoll);
|
|
2229
|
+
const isMember = !!members[didVoter] || didVoter === id.did;
|
|
2230
|
+
if (!isMember) {
|
|
2231
|
+
throw new InvalidParameterError('voter is not a poll member');
|
|
2061
2232
|
}
|
|
2062
|
-
const expired = Date.now() > new Date(
|
|
2233
|
+
const expired = Date.now() > new Date(config.deadline).getTime();
|
|
2063
2234
|
if (expired) {
|
|
2064
2235
|
throw new InvalidParameterError('poll has expired');
|
|
2065
2236
|
}
|
|
2066
|
-
const max =
|
|
2237
|
+
const max = config.options.length;
|
|
2067
2238
|
const vote = dataBallot.vote;
|
|
2068
|
-
if (
|
|
2239
|
+
if (vote < 0 || vote > max) {
|
|
2069
2240
|
throw new InvalidParameterError('ballot.vote');
|
|
2070
2241
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
ballot: didBallot,
|
|
2076
|
-
received: new Date().toISOString(),
|
|
2077
|
-
};
|
|
2078
|
-
return this.mergeData(didPoll, { poll });
|
|
2242
|
+
// Store ballot DID as vault item keyed by voter's ballot key
|
|
2243
|
+
const buffer = Buffer.from(didBallot, 'utf-8');
|
|
2244
|
+
await this.addVaultItem(didPoll, voterBallotKey, buffer);
|
|
2245
|
+
return true;
|
|
2079
2246
|
}
|
|
2080
2247
|
async publishPoll(pollId, options = {}) {
|
|
2081
2248
|
const { reveal = false } = options;
|
|
@@ -2085,19 +2252,20 @@ export default class Keymaster {
|
|
|
2085
2252
|
if (id.did !== owner) {
|
|
2086
2253
|
throw new InvalidParameterError('only owner can publish a poll');
|
|
2087
2254
|
}
|
|
2088
|
-
const
|
|
2089
|
-
if (!
|
|
2090
|
-
throw new InvalidParameterError(
|
|
2255
|
+
const config = await this.getPoll(pollId);
|
|
2256
|
+
if (!config) {
|
|
2257
|
+
throw new InvalidParameterError(pollId);
|
|
2091
2258
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2259
|
+
const results = await this.computePollResults(pollId, config);
|
|
2260
|
+
if (!results.final) {
|
|
2261
|
+
throw new InvalidParameterError('poll not final');
|
|
2094
2262
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
throw new InvalidParameterError(pollId);
|
|
2263
|
+
if (!reveal) {
|
|
2264
|
+
delete results.ballots;
|
|
2098
2265
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2266
|
+
const buffer = Buffer.from(JSON.stringify(results), 'utf-8');
|
|
2267
|
+
await this.addVaultItem(pollId, PollItems.RESULTS, buffer);
|
|
2268
|
+
return true;
|
|
2101
2269
|
}
|
|
2102
2270
|
async unpublishPoll(pollId) {
|
|
2103
2271
|
const id = await this.fetchIdInfo();
|
|
@@ -2106,12 +2274,11 @@ export default class Keymaster {
|
|
|
2106
2274
|
if (id.did !== owner) {
|
|
2107
2275
|
throw new InvalidParameterError(pollId);
|
|
2108
2276
|
}
|
|
2109
|
-
const
|
|
2110
|
-
if (!
|
|
2277
|
+
const config = await this.getPoll(pollId);
|
|
2278
|
+
if (!config) {
|
|
2111
2279
|
throw new InvalidParameterError(pollId);
|
|
2112
2280
|
}
|
|
2113
|
-
|
|
2114
|
-
return this.mergeData(pollId, { poll });
|
|
2281
|
+
return this.removeVaultItem(pollId, PollItems.RESULTS);
|
|
2115
2282
|
}
|
|
2116
2283
|
async createVault(options = {}) {
|
|
2117
2284
|
const id = await this.fetchIdInfo();
|
|
@@ -2123,10 +2290,10 @@ export default class Keymaster {
|
|
|
2123
2290
|
const salt = this.cipher.generateRandomSalt();
|
|
2124
2291
|
const vaultKeypair = this.cipher.generateRandomJwk();
|
|
2125
2292
|
const keys = {};
|
|
2126
|
-
const config = this.cipher.encryptMessage(idKeypair.publicJwk,
|
|
2293
|
+
const config = this.cipher.encryptMessage(idKeypair.publicJwk, JSON.stringify(options));
|
|
2127
2294
|
const publicJwk = options.secretMembers ? idKeypair.publicJwk : vaultKeypair.publicJwk; // If secret, encrypt for the owner only
|
|
2128
|
-
const members = this.cipher.encryptMessage(publicJwk,
|
|
2129
|
-
const items = this.cipher.encryptMessage(vaultKeypair.publicJwk,
|
|
2295
|
+
const members = this.cipher.encryptMessage(publicJwk, JSON.stringify({}));
|
|
2296
|
+
const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, JSON.stringify({}));
|
|
2130
2297
|
const sha256 = this.cipher.hashJSON({});
|
|
2131
2298
|
const vault = {
|
|
2132
2299
|
version,
|
|
@@ -2157,6 +2324,9 @@ export default class Keymaster {
|
|
|
2157
2324
|
return false;
|
|
2158
2325
|
}
|
|
2159
2326
|
}
|
|
2327
|
+
generateBallotKey(vault, memberDID) {
|
|
2328
|
+
return this.generateSaltedId(vault, memberDID).slice(0, this.maxAliasLength);
|
|
2329
|
+
}
|
|
2160
2330
|
generateSaltedId(vault, memberDID) {
|
|
2161
2331
|
if (!vault.version) {
|
|
2162
2332
|
return this.cipher.hashMessage(vault.salt + memberDID);
|
|
@@ -2195,13 +2365,13 @@ export default class Keymaster {
|
|
|
2195
2365
|
}
|
|
2196
2366
|
else {
|
|
2197
2367
|
try {
|
|
2198
|
-
const membersJSON = this.cipher.decryptMessage(vault.
|
|
2368
|
+
const membersJSON = this.cipher.decryptMessage(privateJwk, vault.members, vault.publicJwk);
|
|
2199
2369
|
members = JSON.parse(membersJSON);
|
|
2200
2370
|
}
|
|
2201
2371
|
catch (error) {
|
|
2202
2372
|
}
|
|
2203
2373
|
}
|
|
2204
|
-
const itemsJSON = this.cipher.decryptMessage(vault.
|
|
2374
|
+
const itemsJSON = this.cipher.decryptMessage(privateJwk, vault.items, vault.publicJwk);
|
|
2205
2375
|
const items = JSON.parse(itemsJSON);
|
|
2206
2376
|
return {
|
|
2207
2377
|
isOwner,
|
|
@@ -2223,7 +2393,7 @@ export default class Keymaster {
|
|
|
2223
2393
|
async addMemberKey(vault, memberDID, privateJwk) {
|
|
2224
2394
|
const memberDoc = await this.resolveDID(memberDID, { confirm: true });
|
|
2225
2395
|
const memberPublicJwk = this.getPublicKeyJwk(memberDoc);
|
|
2226
|
-
const memberKey = this.cipher.encryptMessage(memberPublicJwk,
|
|
2396
|
+
const memberKey = this.cipher.encryptMessage(memberPublicJwk, JSON.stringify(privateJwk));
|
|
2227
2397
|
const memberKeyId = this.generateSaltedId(vault, memberDID);
|
|
2228
2398
|
vault.keys[memberKeyId] = memberKey;
|
|
2229
2399
|
}
|
|
@@ -2268,7 +2438,7 @@ export default class Keymaster {
|
|
|
2268
2438
|
}
|
|
2269
2439
|
members[memberDID] = { added: new Date().toISOString() };
|
|
2270
2440
|
const publicJwk = config.secretMembers ? idKeypair.publicJwk : vault.publicJwk;
|
|
2271
|
-
vault.members = this.cipher.encryptMessage(publicJwk,
|
|
2441
|
+
vault.members = this.cipher.encryptMessage(publicJwk, JSON.stringify(members));
|
|
2272
2442
|
await this.addMemberKey(vault, memberDID, privateJwk);
|
|
2273
2443
|
return this.mergeData(vaultId, { vault });
|
|
2274
2444
|
}
|
|
@@ -2276,7 +2446,7 @@ export default class Keymaster {
|
|
|
2276
2446
|
const owner = await this.checkVaultOwner(vaultId);
|
|
2277
2447
|
const idKeypair = await this.fetchKeyPair();
|
|
2278
2448
|
const vault = await this.getVault(vaultId);
|
|
2279
|
-
const {
|
|
2449
|
+
const { config, members } = await this.decryptVault(vault);
|
|
2280
2450
|
const memberDoc = await this.resolveDID(memberId, { confirm: true });
|
|
2281
2451
|
const memberDID = this.getAgentDID(memberDoc);
|
|
2282
2452
|
// Don't allow removing the vault owner
|
|
@@ -2285,7 +2455,7 @@ export default class Keymaster {
|
|
|
2285
2455
|
}
|
|
2286
2456
|
delete members[memberDID];
|
|
2287
2457
|
const publicJwk = config.secretMembers ? idKeypair.publicJwk : vault.publicJwk;
|
|
2288
|
-
vault.members = this.cipher.encryptMessage(publicJwk,
|
|
2458
|
+
vault.members = this.cipher.encryptMessage(publicJwk, JSON.stringify(members));
|
|
2289
2459
|
const memberKeyId = this.generateSaltedId(vault, memberDID);
|
|
2290
2460
|
delete vault.keys[memberKeyId];
|
|
2291
2461
|
return this.mergeData(vaultId, { vault });
|
|
@@ -2301,9 +2471,9 @@ export default class Keymaster {
|
|
|
2301
2471
|
async addVaultItem(vaultId, name, buffer) {
|
|
2302
2472
|
await this.checkVaultOwner(vaultId);
|
|
2303
2473
|
const vault = await this.getVault(vaultId);
|
|
2304
|
-
const {
|
|
2474
|
+
const { items } = await this.decryptVault(vault);
|
|
2305
2475
|
const validName = this.validateAlias(name);
|
|
2306
|
-
const encryptedData = this.cipher.encryptBytes(vault.publicJwk,
|
|
2476
|
+
const encryptedData = this.cipher.encryptBytes(vault.publicJwk, buffer);
|
|
2307
2477
|
const cid = await this.gatekeeper.addText(encryptedData);
|
|
2308
2478
|
const sha256 = this.cipher.hashMessage(buffer);
|
|
2309
2479
|
const type = await this.getMimeType(buffer);
|
|
@@ -2316,16 +2486,16 @@ export default class Keymaster {
|
|
|
2316
2486
|
added: new Date().toISOString(),
|
|
2317
2487
|
data,
|
|
2318
2488
|
};
|
|
2319
|
-
vault.items = this.cipher.encryptMessage(vault.publicJwk,
|
|
2489
|
+
vault.items = this.cipher.encryptMessage(vault.publicJwk, JSON.stringify(items));
|
|
2320
2490
|
vault.sha256 = this.cipher.hashJSON(items);
|
|
2321
2491
|
return this.mergeData(vaultId, { vault });
|
|
2322
2492
|
}
|
|
2323
2493
|
async removeVaultItem(vaultId, name) {
|
|
2324
2494
|
await this.checkVaultOwner(vaultId);
|
|
2325
2495
|
const vault = await this.getVault(vaultId);
|
|
2326
|
-
const {
|
|
2496
|
+
const { items } = await this.decryptVault(vault);
|
|
2327
2497
|
delete items[name];
|
|
2328
|
-
vault.items = this.cipher.encryptMessage(vault.publicJwk,
|
|
2498
|
+
vault.items = this.cipher.encryptMessage(vault.publicJwk, JSON.stringify(items));
|
|
2329
2499
|
vault.sha256 = this.cipher.hashJSON(items);
|
|
2330
2500
|
return this.mergeData(vaultId, { vault });
|
|
2331
2501
|
}
|
|
@@ -2344,7 +2514,7 @@ export default class Keymaster {
|
|
|
2344
2514
|
if (!encryptedData) {
|
|
2345
2515
|
throw new KeymasterError(`Failed to retrieve data for item '${name}' (CID: ${items[name].cid})`);
|
|
2346
2516
|
}
|
|
2347
|
-
const bytes = this.cipher.decryptBytes(
|
|
2517
|
+
const bytes = this.cipher.decryptBytes(privateJwk, encryptedData, vault.publicJwk);
|
|
2348
2518
|
return Buffer.from(bytes);
|
|
2349
2519
|
}
|
|
2350
2520
|
async listDmail() {
|
|
@@ -2628,7 +2798,7 @@ export default class Keymaster {
|
|
|
2628
2798
|
if (poll) {
|
|
2629
2799
|
const names = await this.listAliases();
|
|
2630
2800
|
if (!Object.values(names).includes(noticeDID)) {
|
|
2631
|
-
await this.addUnaliasedPoll(noticeDID);
|
|
2801
|
+
await this.addUnaliasedPoll(noticeDID, poll.name);
|
|
2632
2802
|
}
|
|
2633
2803
|
await this.addToNotices(did, [NoticeTags.POLL]);
|
|
2634
2804
|
continue;
|
|
@@ -2712,10 +2882,20 @@ export default class Keymaster {
|
|
|
2712
2882
|
}
|
|
2713
2883
|
return payload && typeof payload.poll === "string" && typeof payload.vote === "number";
|
|
2714
2884
|
}
|
|
2715
|
-
async addUnaliasedPoll(did) {
|
|
2716
|
-
const
|
|
2885
|
+
async addUnaliasedPoll(did, name) {
|
|
2886
|
+
const baseName = name || did.slice(-32);
|
|
2887
|
+
const aliases = await this.listAliases();
|
|
2888
|
+
let candidate = baseName;
|
|
2889
|
+
let suffix = 2;
|
|
2890
|
+
while (candidate in aliases) {
|
|
2891
|
+
if (aliases[candidate] === did) {
|
|
2892
|
+
return; // Already aliased to this DID
|
|
2893
|
+
}
|
|
2894
|
+
candidate = `${baseName}-${suffix}`;
|
|
2895
|
+
suffix++;
|
|
2896
|
+
}
|
|
2717
2897
|
try {
|
|
2718
|
-
await this.addAlias(
|
|
2898
|
+
await this.addAlias(candidate, did);
|
|
2719
2899
|
}
|
|
2720
2900
|
catch { }
|
|
2721
2901
|
}
|
|
@@ -2731,26 +2911,27 @@ export default class Keymaster {
|
|
|
2731
2911
|
const { version, seed, ...rest } = decrypted;
|
|
2732
2912
|
const safeSeed = { mnemonicEnc: seed.mnemonicEnc };
|
|
2733
2913
|
const hdkey = await this.getHDKeyFromCacheOrMnemonic(decrypted);
|
|
2734
|
-
const { publicJwk
|
|
2914
|
+
const { publicJwk } = this.cipher.generateJwk(hdkey.privateKey);
|
|
2735
2915
|
const plaintext = JSON.stringify(rest);
|
|
2736
|
-
const enc = this.cipher.encryptMessage(publicJwk,
|
|
2916
|
+
const enc = this.cipher.encryptMessage(publicJwk, plaintext);
|
|
2737
2917
|
return { version: version, seed: safeSeed, enc };
|
|
2738
2918
|
}
|
|
2739
2919
|
async decryptWalletFromStorage(stored) {
|
|
2740
2920
|
let mnemonic;
|
|
2741
2921
|
try {
|
|
2742
|
-
mnemonic = await
|
|
2922
|
+
mnemonic = await decryptWithPassphrase(stored.seed.mnemonicEnc, this.passphrase);
|
|
2743
2923
|
}
|
|
2744
2924
|
catch (error) {
|
|
2745
|
-
|
|
2746
|
-
|
|
2925
|
+
const msg = error?.message || '';
|
|
2926
|
+
// OperationError: Web Crypto API (legacy); 'invalid ghash tag': @noble/ciphers
|
|
2927
|
+
if (error?.name === 'OperationError' || msg.includes('invalid ghash tag')) {
|
|
2747
2928
|
throw new KeymasterError('Incorrect passphrase.');
|
|
2748
2929
|
}
|
|
2749
2930
|
throw error;
|
|
2750
2931
|
}
|
|
2751
2932
|
this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
|
|
2752
2933
|
const { publicJwk, privateJwk } = this.cipher.generateJwk(this._hdkeyCache.privateKey);
|
|
2753
|
-
const plaintext = this.cipher.decryptMessage(
|
|
2934
|
+
const plaintext = this.cipher.decryptMessage(privateJwk, stored.enc, publicJwk);
|
|
2754
2935
|
const data = JSON.parse(plaintext);
|
|
2755
2936
|
const wallet = { version: stored.version, seed: stored.seed, ...data };
|
|
2756
2937
|
return wallet;
|