@didcid/keymaster 0.3.10 → 0.4.1
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/dist/cjs/index.cjs +0 -1
- package/dist/cjs/keymaster-client.cjs +83 -2
- package/dist/cjs/keymaster.cjs +1966 -206
- package/dist/cjs/node.cjs +0 -1
- package/dist/esm/cli.js +123 -7
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/keymaster-client.js +83 -2
- package/dist/esm/keymaster-client.js.map +1 -1
- package/dist/esm/keymaster.js +391 -206
- package/dist/esm/keymaster.js.map +1 -1
- package/dist/types/keymaster-client.d.ts +13 -5
- package/dist/types/keymaster.d.ts +19 -6
- package/dist/types/types.d.ts +25 -17
- 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,24 @@ 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);
|
|
156
|
+
}
|
|
157
|
+
async changePassphrase(newPassphrase) {
|
|
158
|
+
if (!newPassphrase) {
|
|
159
|
+
throw new InvalidParameterError('newPassphrase');
|
|
160
|
+
}
|
|
161
|
+
const wallet = await this.loadWallet();
|
|
162
|
+
const mnemonic = await decryptWithPassphrase(wallet.seed.mnemonicEnc, this.passphrase);
|
|
163
|
+
const mnemonicEnc = await encryptWithPassphrase(mnemonic, newPassphrase);
|
|
164
|
+
wallet.seed.mnemonicEnc = mnemonicEnc;
|
|
165
|
+
this.passphrase = newPassphrase;
|
|
166
|
+
this._walletCache = wallet;
|
|
167
|
+
const encrypted = await this.encryptWalletForStorage(wallet);
|
|
168
|
+
const ok = await this.db.saveWallet(encrypted, true);
|
|
169
|
+
if (!ok) {
|
|
170
|
+
throw new KeymasterError('Failed to save wallet with new passphrase');
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
151
173
|
}
|
|
152
174
|
async checkWallet() {
|
|
153
175
|
const wallet = await this.loadWallet();
|
|
@@ -359,7 +381,7 @@ export default class Keymaster {
|
|
|
359
381
|
const keypair = await this.hdKeyPair();
|
|
360
382
|
const seedBank = await this.resolveSeedBank();
|
|
361
383
|
const msg = JSON.stringify(wallet);
|
|
362
|
-
const backup = this.cipher.encryptMessage(keypair.publicJwk,
|
|
384
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, msg);
|
|
363
385
|
const operation = {
|
|
364
386
|
type: "create",
|
|
365
387
|
created: new Date().toISOString(),
|
|
@@ -412,12 +434,12 @@ export default class Keymaster {
|
|
|
412
434
|
if (typeof castData.backup !== 'string') {
|
|
413
435
|
throw new InvalidParameterError('Asset "backup" is missing or not a string');
|
|
414
436
|
}
|
|
415
|
-
const backup = this.cipher.decryptMessage(keypair.
|
|
437
|
+
const backup = this.cipher.decryptMessage(keypair.privateJwk, castData.backup, keypair.publicJwk);
|
|
416
438
|
let wallet = JSON.parse(backup);
|
|
417
439
|
if (isWalletFile(wallet)) {
|
|
418
440
|
const mnemonic = await this.decryptMnemonic();
|
|
419
441
|
// Backup might have a different mnemonic passphase so re-encrypt
|
|
420
|
-
wallet.seed.mnemonicEnc = await
|
|
442
|
+
wallet.seed.mnemonicEnc = await encryptWithPassphrase(mnemonic, this.passphrase);
|
|
421
443
|
}
|
|
422
444
|
await this.mutateWallet(async (current) => {
|
|
423
445
|
// Clear all existing properties from the current wallet
|
|
@@ -697,19 +719,16 @@ export default class Keymaster {
|
|
|
697
719
|
}
|
|
698
720
|
async encryptMessage(msg, receiver, options = {}) {
|
|
699
721
|
const { encryptForSender = true, includeHash = false, } = options;
|
|
700
|
-
const id = await this.fetchIdInfo();
|
|
701
722
|
const senderKeypair = await this.fetchKeyPair();
|
|
702
723
|
if (!senderKeypair) {
|
|
703
724
|
throw new KeymasterError('No valid sender keypair');
|
|
704
725
|
}
|
|
705
726
|
const doc = await this.resolveDID(receiver, { confirm: true });
|
|
706
727
|
const receivePublicJwk = this.getPublicKeyJwk(doc);
|
|
707
|
-
const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk,
|
|
708
|
-
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk,
|
|
728
|
+
const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk, msg) : null;
|
|
729
|
+
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, msg);
|
|
709
730
|
const cipher_hash = includeHash ? this.cipher.hashMessage(msg) : null;
|
|
710
731
|
const encrypted = {
|
|
711
|
-
sender: id.did,
|
|
712
|
-
created: new Date().toISOString(),
|
|
713
732
|
cipher_hash,
|
|
714
733
|
cipher_sender,
|
|
715
734
|
cipher_receiver,
|
|
@@ -725,7 +744,7 @@ export default class Keymaster {
|
|
|
725
744
|
const didkey = hdkey.derive(path);
|
|
726
745
|
const receiverKeypair = this.cipher.generateJwk(didkey.privateKey);
|
|
727
746
|
try {
|
|
728
|
-
return this.cipher.decryptMessage(
|
|
747
|
+
return this.cipher.decryptMessage(receiverKeypair.privateJwk, ciphertext, senderPublicJwk);
|
|
729
748
|
}
|
|
730
749
|
catch (error) {
|
|
731
750
|
index -= 1;
|
|
@@ -736,7 +755,8 @@ export default class Keymaster {
|
|
|
736
755
|
async decryptMessage(did) {
|
|
737
756
|
const wallet = await this.loadWallet();
|
|
738
757
|
const id = await this.fetchIdInfo();
|
|
739
|
-
const
|
|
758
|
+
const msgDoc = await this.resolveDID(did);
|
|
759
|
+
const asset = msgDoc.didDocumentData;
|
|
740
760
|
if (!asset) {
|
|
741
761
|
throw new InvalidParameterError('did not encrypted');
|
|
742
762
|
}
|
|
@@ -745,9 +765,16 @@ export default class Keymaster {
|
|
|
745
765
|
throw new InvalidParameterError('did not encrypted');
|
|
746
766
|
}
|
|
747
767
|
const crypt = (castAsset.encrypted ? castAsset.encrypted : castAsset);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
768
|
+
// Derive sender and created from the message DID document,
|
|
769
|
+
// falling back to fields in the asset for legacy messages
|
|
770
|
+
const sender = crypt.sender || msgDoc.didDocument?.controller;
|
|
771
|
+
const created = crypt.created || msgDoc.didDocumentMetadata?.created;
|
|
772
|
+
if (!sender) {
|
|
773
|
+
throw new InvalidParameterError('Sender DID could not be determined from message or DID document');
|
|
774
|
+
}
|
|
775
|
+
const senderDoc = await this.resolveDID(sender, { confirm: true, versionTime: created });
|
|
776
|
+
const senderPublicJwk = this.getPublicKeyJwk(senderDoc);
|
|
777
|
+
const ciphertext = (sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
|
|
751
778
|
return await this.decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext);
|
|
752
779
|
}
|
|
753
780
|
async encryptJSON(json, did, options = {}) {
|
|
@@ -1128,7 +1155,7 @@ export default class Keymaster {
|
|
|
1128
1155
|
id: idInfo,
|
|
1129
1156
|
};
|
|
1130
1157
|
const msg = JSON.stringify(data);
|
|
1131
|
-
const backup = this.cipher.encryptMessage(keypair.publicJwk,
|
|
1158
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, msg);
|
|
1132
1159
|
const doc = await this.resolveDID(idInfo.did);
|
|
1133
1160
|
const registry = doc.didDocumentRegistration?.registry;
|
|
1134
1161
|
if (!registry) {
|
|
@@ -1154,7 +1181,7 @@ export default class Keymaster {
|
|
|
1154
1181
|
if (typeof backupStore.backup !== 'string') {
|
|
1155
1182
|
throw new InvalidDIDError('backup not found in backupStore');
|
|
1156
1183
|
}
|
|
1157
|
-
const backup = this.cipher.decryptMessage(keypair.
|
|
1184
|
+
const backup = this.cipher.decryptMessage(keypair.privateJwk, backupStore.backup, keypair.publicJwk);
|
|
1158
1185
|
const data = JSON.parse(backup);
|
|
1159
1186
|
await this.mutateWallet((wallet) => {
|
|
1160
1187
|
if (wallet.ids[data.name]) {
|
|
@@ -1255,7 +1282,13 @@ export default class Keymaster {
|
|
|
1255
1282
|
validFrom = new Date().toISOString();
|
|
1256
1283
|
}
|
|
1257
1284
|
const id = await this.fetchIdInfo();
|
|
1258
|
-
|
|
1285
|
+
let subjectURI;
|
|
1286
|
+
try {
|
|
1287
|
+
subjectURI = await this.lookupDID(subjectId);
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
subjectURI = subjectId;
|
|
1291
|
+
}
|
|
1259
1292
|
const vc = {
|
|
1260
1293
|
"@context": [
|
|
1261
1294
|
"https://www.w3.org/ns/credentials/v2",
|
|
@@ -1266,7 +1299,7 @@ export default class Keymaster {
|
|
|
1266
1299
|
validFrom,
|
|
1267
1300
|
validUntil,
|
|
1268
1301
|
credentialSubject: {
|
|
1269
|
-
id:
|
|
1302
|
+
id: subjectURI,
|
|
1270
1303
|
},
|
|
1271
1304
|
};
|
|
1272
1305
|
// If schema provided, add credentialSchema and generate claims from schema
|
|
@@ -1291,7 +1324,7 @@ export default class Keymaster {
|
|
|
1291
1324
|
}
|
|
1292
1325
|
if (claims && Object.keys(claims).length) {
|
|
1293
1326
|
vc.credentialSubject = {
|
|
1294
|
-
id:
|
|
1327
|
+
id: subjectURI,
|
|
1295
1328
|
...claims,
|
|
1296
1329
|
};
|
|
1297
1330
|
}
|
|
@@ -1306,21 +1339,32 @@ export default class Keymaster {
|
|
|
1306
1339
|
throw new InvalidParameterError('credential.issuer');
|
|
1307
1340
|
}
|
|
1308
1341
|
const signed = await this.addProof(credential);
|
|
1309
|
-
|
|
1342
|
+
const subjectId = credential.credentialSubject.id;
|
|
1343
|
+
if (this.isManagedDID(subjectId)) {
|
|
1344
|
+
return this.encryptJSON(signed, subjectId, { ...options, includeHash: true });
|
|
1345
|
+
}
|
|
1346
|
+
return this.encryptJSON(signed, id.did, { ...options, includeHash: true, encryptForSender: false });
|
|
1310
1347
|
}
|
|
1311
1348
|
async sendCredential(did, options = {}) {
|
|
1312
1349
|
const vc = await this.getCredential(did);
|
|
1313
1350
|
if (!vc) {
|
|
1314
1351
|
return null;
|
|
1315
1352
|
}
|
|
1353
|
+
const subjectId = vc.credentialSubject.id;
|
|
1354
|
+
if (!this.isManagedDID(subjectId)) {
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1316
1357
|
const registry = this.ephemeralRegistry;
|
|
1317
1358
|
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
|
|
1318
1359
|
const message = {
|
|
1319
|
-
to: [
|
|
1360
|
+
to: [subjectId],
|
|
1320
1361
|
dids: [did],
|
|
1321
1362
|
};
|
|
1322
1363
|
return this.createNotice(message, { registry, validUntil, ...options });
|
|
1323
1364
|
}
|
|
1365
|
+
isManagedDID(value) {
|
|
1366
|
+
return value.startsWith('did:cid:');
|
|
1367
|
+
}
|
|
1324
1368
|
isVerifiableCredential(obj) {
|
|
1325
1369
|
if (typeof obj !== 'object' || !obj) {
|
|
1326
1370
|
return false;
|
|
@@ -1342,24 +1386,29 @@ export default class Keymaster {
|
|
|
1342
1386
|
delete credential.proof;
|
|
1343
1387
|
const signed = await this.addProof(credential);
|
|
1344
1388
|
const msg = JSON.stringify(signed);
|
|
1345
|
-
const id = await this.fetchIdInfo();
|
|
1346
1389
|
const senderKeypair = await this.fetchKeyPair();
|
|
1347
1390
|
if (!senderKeypair) {
|
|
1348
1391
|
throw new KeymasterError('No valid sender keypair');
|
|
1349
1392
|
}
|
|
1350
1393
|
const holder = credential.credentialSubject.id;
|
|
1351
|
-
const holderDoc = await this.resolveDID(holder, { confirm: true });
|
|
1352
|
-
const receivePublicJwk = this.getPublicKeyJwk(holderDoc);
|
|
1353
|
-
const cipher_sender = this.cipher.encryptMessage(senderKeypair.publicJwk, senderKeypair.privateJwk, msg);
|
|
1354
|
-
const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
|
|
1355
1394
|
const msgHash = this.cipher.hashMessage(msg);
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1395
|
+
let encrypted;
|
|
1396
|
+
if (this.isManagedDID(holder)) {
|
|
1397
|
+
const holderDoc = await this.resolveDID(holder, { confirm: true });
|
|
1398
|
+
const receivePublicJwk = this.getPublicKeyJwk(holderDoc);
|
|
1399
|
+
encrypted = {
|
|
1400
|
+
cipher_hash: msgHash,
|
|
1401
|
+
cipher_sender: this.cipher.encryptMessage(senderKeypair.publicJwk, msg),
|
|
1402
|
+
cipher_receiver: this.cipher.encryptMessage(receivePublicJwk, msg),
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
encrypted = {
|
|
1407
|
+
cipher_hash: msgHash,
|
|
1408
|
+
cipher_sender: null,
|
|
1409
|
+
cipher_receiver: this.cipher.encryptMessage(senderKeypair.publicJwk, msg),
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1363
1412
|
return this.updateDID(did, { didDocumentData: { encrypted } });
|
|
1364
1413
|
}
|
|
1365
1414
|
async revokeCredential(credential) {
|
|
@@ -1855,72 +1904,64 @@ export default class Keymaster {
|
|
|
1855
1904
|
const nextWeek = new Date();
|
|
1856
1905
|
nextWeek.setDate(now.getDate() + 7);
|
|
1857
1906
|
return {
|
|
1858
|
-
|
|
1859
|
-
|
|
1907
|
+
version: 2,
|
|
1908
|
+
name: 'poll-name',
|
|
1860
1909
|
description: 'What is this poll about?',
|
|
1861
|
-
roster: 'DID of the eligible voter group',
|
|
1862
1910
|
options: ['yes', 'no', 'abstain'],
|
|
1863
1911
|
deadline: nextWeek.toISOString(),
|
|
1864
1912
|
};
|
|
1865
1913
|
}
|
|
1866
|
-
async createPoll(
|
|
1867
|
-
if (
|
|
1868
|
-
throw new InvalidParameterError('poll');
|
|
1869
|
-
}
|
|
1870
|
-
if (poll.version !== 1) {
|
|
1914
|
+
async createPoll(config, options = {}) {
|
|
1915
|
+
if (config.version !== 2) {
|
|
1871
1916
|
throw new InvalidParameterError('poll.version');
|
|
1872
1917
|
}
|
|
1873
|
-
if (!
|
|
1918
|
+
if (!config.name) {
|
|
1919
|
+
throw new InvalidParameterError('poll.name');
|
|
1920
|
+
}
|
|
1921
|
+
if (!config.description) {
|
|
1874
1922
|
throw new InvalidParameterError('poll.description');
|
|
1875
1923
|
}
|
|
1876
|
-
if (!
|
|
1924
|
+
if (!config.options || !Array.isArray(config.options) || config.options.length < 2 || config.options.length > 10) {
|
|
1877
1925
|
throw new InvalidParameterError('poll.options');
|
|
1878
1926
|
}
|
|
1879
|
-
if (!
|
|
1880
|
-
// eslint-disable-next-line
|
|
1881
|
-
throw new InvalidParameterError('poll.roster');
|
|
1882
|
-
}
|
|
1883
|
-
try {
|
|
1884
|
-
const isValidGroup = await this.testGroup(poll.roster);
|
|
1885
|
-
if (!isValidGroup) {
|
|
1886
|
-
throw new InvalidParameterError('poll.roster');
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
catch {
|
|
1890
|
-
throw new InvalidParameterError('poll.roster');
|
|
1891
|
-
}
|
|
1892
|
-
if (!poll.deadline) {
|
|
1893
|
-
// eslint-disable-next-line
|
|
1927
|
+
if (!config.deadline) {
|
|
1894
1928
|
throw new InvalidParameterError('poll.deadline');
|
|
1895
1929
|
}
|
|
1896
|
-
const deadline = new Date(
|
|
1930
|
+
const deadline = new Date(config.deadline);
|
|
1897
1931
|
if (isNaN(deadline.getTime())) {
|
|
1898
1932
|
throw new InvalidParameterError('poll.deadline');
|
|
1899
1933
|
}
|
|
1900
1934
|
if (deadline < new Date()) {
|
|
1901
1935
|
throw new InvalidParameterError('poll.deadline');
|
|
1902
1936
|
}
|
|
1903
|
-
|
|
1937
|
+
const vaultDid = await this.createVault(options);
|
|
1938
|
+
const buffer = Buffer.from(JSON.stringify(config), 'utf-8');
|
|
1939
|
+
await this.addVaultItem(vaultDid, PollItems.POLL, buffer);
|
|
1940
|
+
return vaultDid;
|
|
1904
1941
|
}
|
|
1905
1942
|
async getPoll(id) {
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
if (castOldAsset.options) {
|
|
1910
|
-
return castOldAsset;
|
|
1943
|
+
const isVault = await this.testVault(id);
|
|
1944
|
+
if (!isVault) {
|
|
1945
|
+
return null;
|
|
1911
1946
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1947
|
+
try {
|
|
1948
|
+
const buffer = await this.getVaultItem(id, PollItems.POLL);
|
|
1949
|
+
if (!buffer) {
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
const config = JSON.parse(buffer.toString('utf-8'));
|
|
1953
|
+
return config;
|
|
1954
|
+
}
|
|
1955
|
+
catch {
|
|
1914
1956
|
return null;
|
|
1915
1957
|
}
|
|
1916
|
-
return castAsset.poll;
|
|
1917
1958
|
}
|
|
1918
1959
|
async testPoll(id) {
|
|
1919
1960
|
try {
|
|
1920
|
-
const
|
|
1921
|
-
return
|
|
1961
|
+
const config = await this.getPoll(id);
|
|
1962
|
+
return config !== null;
|
|
1922
1963
|
}
|
|
1923
|
-
catch
|
|
1964
|
+
catch {
|
|
1924
1965
|
return false;
|
|
1925
1966
|
}
|
|
1926
1967
|
}
|
|
@@ -1935,113 +1976,243 @@ export default class Keymaster {
|
|
|
1935
1976
|
}
|
|
1936
1977
|
return polls;
|
|
1937
1978
|
}
|
|
1979
|
+
async addPollVoter(pollId, memberId) {
|
|
1980
|
+
const config = await this.getPoll(pollId);
|
|
1981
|
+
if (!config) {
|
|
1982
|
+
throw new InvalidParameterError('pollId');
|
|
1983
|
+
}
|
|
1984
|
+
return this.addVaultMember(pollId, memberId);
|
|
1985
|
+
}
|
|
1986
|
+
async removePollVoter(pollId, memberId) {
|
|
1987
|
+
const config = await this.getPoll(pollId);
|
|
1988
|
+
if (!config) {
|
|
1989
|
+
throw new InvalidParameterError('pollId');
|
|
1990
|
+
}
|
|
1991
|
+
return this.removeVaultMember(pollId, memberId);
|
|
1992
|
+
}
|
|
1993
|
+
async listPollVoters(pollId) {
|
|
1994
|
+
const config = await this.getPoll(pollId);
|
|
1995
|
+
if (!config) {
|
|
1996
|
+
throw new InvalidParameterError('pollId');
|
|
1997
|
+
}
|
|
1998
|
+
return this.listVaultMembers(pollId);
|
|
1999
|
+
}
|
|
1938
2000
|
async viewPoll(pollId) {
|
|
1939
2001
|
const id = await this.fetchIdInfo();
|
|
1940
|
-
const
|
|
1941
|
-
if (!
|
|
2002
|
+
const config = await this.getPoll(pollId);
|
|
2003
|
+
if (!config) {
|
|
1942
2004
|
throw new InvalidParameterError('pollId');
|
|
1943
2005
|
}
|
|
2006
|
+
const doc = await this.resolveDID(pollId);
|
|
2007
|
+
const isOwner = (doc.didDocument?.controller === id.did);
|
|
2008
|
+
const voteExpired = Date.now() > new Date(config.deadline).getTime();
|
|
2009
|
+
let isEligible = false;
|
|
1944
2010
|
let hasVoted = false;
|
|
1945
|
-
|
|
1946
|
-
|
|
2011
|
+
const ballots = [];
|
|
2012
|
+
try {
|
|
2013
|
+
const vault = await this.getVault(pollId);
|
|
2014
|
+
const members = await this.listVaultMembers(pollId);
|
|
2015
|
+
isEligible = isOwner || !!members[id.did];
|
|
2016
|
+
const items = await this.listVaultItems(pollId);
|
|
2017
|
+
for (const itemName of Object.keys(items)) {
|
|
2018
|
+
if (itemName !== PollItems.POLL && itemName !== PollItems.RESULTS) {
|
|
2019
|
+
ballots.push(itemName);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
const myBallotKey = this.generateBallotKey(vault, id.did);
|
|
2023
|
+
hasVoted = ballots.includes(myBallotKey);
|
|
2024
|
+
}
|
|
2025
|
+
catch {
|
|
2026
|
+
isEligible = false;
|
|
1947
2027
|
}
|
|
1948
|
-
const voteExpired = Date.now() > new Date(poll.deadline).getTime();
|
|
1949
|
-
const isEligible = await this.testGroup(poll.roster, id.did);
|
|
1950
|
-
const doc = await this.resolveDID(pollId);
|
|
1951
2028
|
const view = {
|
|
1952
|
-
description:
|
|
1953
|
-
options:
|
|
1954
|
-
deadline:
|
|
1955
|
-
isOwner
|
|
1956
|
-
isEligible
|
|
1957
|
-
voteExpired
|
|
1958
|
-
hasVoted
|
|
2029
|
+
description: config.description,
|
|
2030
|
+
options: config.options,
|
|
2031
|
+
deadline: config.deadline,
|
|
2032
|
+
isOwner,
|
|
2033
|
+
isEligible,
|
|
2034
|
+
voteExpired,
|
|
2035
|
+
hasVoted,
|
|
2036
|
+
ballots,
|
|
1959
2037
|
};
|
|
1960
|
-
if (
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2038
|
+
if (isOwner) {
|
|
2039
|
+
view.results = await this.computePollResults(pollId, config);
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
try {
|
|
2043
|
+
const resultsBuffer = await this.getVaultItem(pollId, PollItems.RESULTS);
|
|
2044
|
+
if (resultsBuffer) {
|
|
2045
|
+
view.results = JSON.parse(resultsBuffer.toString('utf-8'));
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
catch { }
|
|
2049
|
+
}
|
|
2050
|
+
return view;
|
|
2051
|
+
}
|
|
2052
|
+
async computePollResults(pollId, config) {
|
|
2053
|
+
const vault = await this.getVault(pollId);
|
|
2054
|
+
const members = await this.listVaultMembers(pollId);
|
|
2055
|
+
const items = await this.listVaultItems(pollId);
|
|
2056
|
+
const results = {
|
|
2057
|
+
tally: [],
|
|
2058
|
+
ballots: [],
|
|
2059
|
+
};
|
|
2060
|
+
results.tally.push({ vote: 0, option: 'spoil', count: 0 });
|
|
2061
|
+
for (let i = 0; i < config.options.length; i++) {
|
|
2062
|
+
results.tally.push({ vote: i + 1, option: config.options[i], count: 0 });
|
|
2063
|
+
}
|
|
2064
|
+
// Build ballotKey → memberDID mapping
|
|
2065
|
+
const keyToMember = {};
|
|
2066
|
+
for (const memberDID of Object.keys(members)) {
|
|
2067
|
+
const ballotKey = this.generateBallotKey(vault, memberDID);
|
|
2068
|
+
keyToMember[ballotKey] = memberDID;
|
|
2069
|
+
}
|
|
2070
|
+
// Include owner in mapping
|
|
2071
|
+
const id = await this.fetchIdInfo();
|
|
2072
|
+
const ownerKey = this.generateBallotKey(vault, id.did);
|
|
2073
|
+
keyToMember[ownerKey] = id.did;
|
|
2074
|
+
let voted = 0;
|
|
2075
|
+
for (const [itemName, itemMeta] of Object.entries(items)) {
|
|
2076
|
+
if (itemName === PollItems.POLL || itemName === PollItems.RESULTS) {
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const ballotBuffer = await this.getVaultItem(pollId, itemName);
|
|
2080
|
+
if (!ballotBuffer) {
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
const ballotDid = ballotBuffer.toString('utf-8');
|
|
2084
|
+
const decrypted = await this.decryptJSON(ballotDid);
|
|
2085
|
+
const vote = decrypted.vote;
|
|
2086
|
+
const voterDID = keyToMember[itemName] || itemName;
|
|
2087
|
+
if (results.ballots) {
|
|
2088
|
+
results.ballots.push({
|
|
2089
|
+
voter: voterDID,
|
|
2090
|
+
vote,
|
|
2091
|
+
option: vote === 0 ? 'spoil' : config.options[vote - 1],
|
|
2092
|
+
received: itemMeta.added || '',
|
|
1976
2093
|
});
|
|
1977
2094
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
const decrypted = await this.decryptJSON(ballot.ballot);
|
|
1981
|
-
const vote = decrypted.vote;
|
|
1982
|
-
if (results.ballots) {
|
|
1983
|
-
results.ballots.push({
|
|
1984
|
-
...ballot,
|
|
1985
|
-
voter,
|
|
1986
|
-
vote,
|
|
1987
|
-
option: poll.options[vote - 1],
|
|
1988
|
-
});
|
|
1989
|
-
}
|
|
1990
|
-
voted += 1;
|
|
2095
|
+
voted += 1;
|
|
2096
|
+
if (vote >= 0 && vote < results.tally.length) {
|
|
1991
2097
|
results.tally[vote].count += 1;
|
|
1992
2098
|
}
|
|
1993
|
-
const roster = await this.getGroup(poll.roster);
|
|
1994
|
-
const total = roster.members.length;
|
|
1995
|
-
results.votes = {
|
|
1996
|
-
eligible: total,
|
|
1997
|
-
received: voted,
|
|
1998
|
-
pending: total - voted,
|
|
1999
|
-
};
|
|
2000
|
-
results.final = voteExpired || (voted === total);
|
|
2001
|
-
view.results = results;
|
|
2002
2099
|
}
|
|
2003
|
-
|
|
2100
|
+
const total = Object.keys(members).length + 1; // +1 for owner
|
|
2101
|
+
const voteExpired = Date.now() > new Date(config.deadline).getTime();
|
|
2102
|
+
results.votes = {
|
|
2103
|
+
eligible: total,
|
|
2104
|
+
received: voted,
|
|
2105
|
+
pending: total - voted,
|
|
2106
|
+
};
|
|
2107
|
+
results.final = voteExpired || (voted === total);
|
|
2108
|
+
return results;
|
|
2004
2109
|
}
|
|
2005
2110
|
async votePoll(pollId, vote, options = {}) {
|
|
2006
|
-
const { spoil = false } = options;
|
|
2007
2111
|
const id = await this.fetchIdInfo();
|
|
2008
2112
|
const didPoll = await this.lookupDID(pollId);
|
|
2009
2113
|
const doc = await this.resolveDID(didPoll);
|
|
2010
|
-
const
|
|
2011
|
-
if (!
|
|
2114
|
+
const config = await this.getPoll(pollId);
|
|
2115
|
+
if (!config) {
|
|
2012
2116
|
throw new InvalidParameterError('pollId');
|
|
2013
2117
|
}
|
|
2014
|
-
const eligible = await this.testGroup(poll.roster, id.did);
|
|
2015
|
-
const expired = Date.now() > new Date(poll.deadline).getTime();
|
|
2016
2118
|
const owner = doc.didDocument?.controller;
|
|
2017
2119
|
if (!owner) {
|
|
2018
|
-
throw new KeymasterError('owner
|
|
2120
|
+
throw new KeymasterError('owner missing from poll');
|
|
2121
|
+
}
|
|
2122
|
+
// Check vault membership
|
|
2123
|
+
let isEligible = false;
|
|
2124
|
+
if (id.did === owner) {
|
|
2125
|
+
isEligible = true;
|
|
2126
|
+
}
|
|
2127
|
+
else {
|
|
2128
|
+
try {
|
|
2129
|
+
const vault = await this.getVault(didPoll);
|
|
2130
|
+
await this.decryptVault(vault);
|
|
2131
|
+
isEligible = true;
|
|
2132
|
+
}
|
|
2133
|
+
catch {
|
|
2134
|
+
isEligible = false;
|
|
2135
|
+
}
|
|
2019
2136
|
}
|
|
2020
|
-
if (!
|
|
2021
|
-
throw new InvalidParameterError('voter not
|
|
2137
|
+
if (!isEligible) {
|
|
2138
|
+
throw new InvalidParameterError('voter is not a poll member');
|
|
2022
2139
|
}
|
|
2140
|
+
const expired = Date.now() > new Date(config.deadline).getTime();
|
|
2023
2141
|
if (expired) {
|
|
2024
2142
|
throw new InvalidParameterError('poll has expired');
|
|
2025
2143
|
}
|
|
2026
|
-
|
|
2027
|
-
if (
|
|
2028
|
-
|
|
2029
|
-
poll: didPoll,
|
|
2030
|
-
vote: 0,
|
|
2031
|
-
};
|
|
2144
|
+
const max = config.options.length;
|
|
2145
|
+
if (!Number.isInteger(vote) || vote < 0 || vote > max) {
|
|
2146
|
+
throw new InvalidParameterError('vote');
|
|
2032
2147
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2148
|
+
const ballot = {
|
|
2149
|
+
poll: didPoll,
|
|
2150
|
+
vote: vote,
|
|
2151
|
+
};
|
|
2152
|
+
// Encrypt for owner and sender (voter can view their own ballot)
|
|
2153
|
+
return await this.encryptJSON(ballot, owner, options);
|
|
2154
|
+
}
|
|
2155
|
+
async sendPoll(pollId) {
|
|
2156
|
+
const didPoll = await this.lookupDID(pollId);
|
|
2157
|
+
const config = await this.getPoll(didPoll);
|
|
2158
|
+
if (!config) {
|
|
2159
|
+
throw new InvalidParameterError('pollId');
|
|
2160
|
+
}
|
|
2161
|
+
const members = await this.listVaultMembers(didPoll);
|
|
2162
|
+
const voters = Object.keys(members);
|
|
2163
|
+
if (voters.length === 0) {
|
|
2164
|
+
throw new KeymasterError('No poll voters found');
|
|
2165
|
+
}
|
|
2166
|
+
const registry = this.ephemeralRegistry;
|
|
2167
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
2168
|
+
const message = {
|
|
2169
|
+
to: voters,
|
|
2170
|
+
dids: [didPoll],
|
|
2171
|
+
};
|
|
2172
|
+
return this.createNotice(message, { registry, validUntil });
|
|
2173
|
+
}
|
|
2174
|
+
async sendBallot(ballotDid, pollId) {
|
|
2175
|
+
const didPoll = await this.lookupDID(pollId);
|
|
2176
|
+
const config = await this.getPoll(didPoll);
|
|
2177
|
+
if (!config) {
|
|
2178
|
+
throw new InvalidParameterError('pollId is not a valid poll');
|
|
2179
|
+
}
|
|
2180
|
+
const pollDoc = await this.resolveDID(didPoll);
|
|
2181
|
+
const ownerDid = pollDoc.didDocument?.controller;
|
|
2182
|
+
if (!ownerDid) {
|
|
2183
|
+
throw new KeymasterError('poll owner not found');
|
|
2184
|
+
}
|
|
2185
|
+
const registry = this.ephemeralRegistry;
|
|
2186
|
+
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
2187
|
+
const message = {
|
|
2188
|
+
to: [ownerDid],
|
|
2189
|
+
dids: [ballotDid],
|
|
2190
|
+
};
|
|
2191
|
+
return this.createNotice(message, { registry, validUntil });
|
|
2192
|
+
}
|
|
2193
|
+
async viewBallot(ballotDid) {
|
|
2194
|
+
const docBallot = await this.resolveDID(ballotDid);
|
|
2195
|
+
const voter = docBallot.didDocument?.controller;
|
|
2196
|
+
const result = {
|
|
2197
|
+
poll: '',
|
|
2198
|
+
voter: voter || undefined,
|
|
2199
|
+
};
|
|
2200
|
+
try {
|
|
2201
|
+
const data = await this.decryptJSON(ballotDid);
|
|
2202
|
+
result.poll = data.poll;
|
|
2203
|
+
result.vote = data.vote;
|
|
2204
|
+
const config = await this.getPoll(data.poll);
|
|
2205
|
+
if (config && data.vote > 0 && data.vote <= config.options.length) {
|
|
2206
|
+
result.option = config.options[data.vote - 1];
|
|
2037
2207
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2208
|
+
else if (data.vote === 0) {
|
|
2209
|
+
result.option = 'spoil';
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
catch {
|
|
2213
|
+
// Caller cannot decrypt (not the owner) — return limited info
|
|
2042
2214
|
}
|
|
2043
|
-
|
|
2044
|
-
return await this.encryptJSON(ballot, owner, { ...options, encryptForSender: false });
|
|
2215
|
+
return result;
|
|
2045
2216
|
}
|
|
2046
2217
|
async updatePoll(ballot) {
|
|
2047
2218
|
const id = await this.fetchIdInfo();
|
|
@@ -2051,7 +2222,7 @@ export default class Keymaster {
|
|
|
2051
2222
|
let dataBallot;
|
|
2052
2223
|
try {
|
|
2053
2224
|
dataBallot = await this.decryptJSON(didBallot);
|
|
2054
|
-
if (!dataBallot.poll ||
|
|
2225
|
+
if (!dataBallot.poll || dataBallot.vote === undefined) {
|
|
2055
2226
|
throw new InvalidParameterError('ballot');
|
|
2056
2227
|
}
|
|
2057
2228
|
}
|
|
@@ -2061,34 +2232,34 @@ export default class Keymaster {
|
|
|
2061
2232
|
const didPoll = dataBallot.poll;
|
|
2062
2233
|
const docPoll = await this.resolveDID(didPoll);
|
|
2063
2234
|
const didOwner = docPoll.didDocument.controller;
|
|
2064
|
-
const
|
|
2065
|
-
if (!
|
|
2235
|
+
const config = await this.getPoll(didPoll);
|
|
2236
|
+
if (!config) {
|
|
2066
2237
|
throw new KeymasterError('Cannot find poll related to ballot');
|
|
2067
2238
|
}
|
|
2068
2239
|
if (id.did !== didOwner) {
|
|
2069
2240
|
throw new InvalidParameterError('only owner can update a poll');
|
|
2070
2241
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2242
|
+
// Check voter is a vault member
|
|
2243
|
+
const vault = await this.getVault(didPoll);
|
|
2244
|
+
const voterBallotKey = this.generateBallotKey(vault, didVoter);
|
|
2245
|
+
const members = await this.listVaultMembers(didPoll);
|
|
2246
|
+
const isMember = !!members[didVoter] || didVoter === id.did;
|
|
2247
|
+
if (!isMember) {
|
|
2248
|
+
throw new InvalidParameterError('voter is not a poll member');
|
|
2074
2249
|
}
|
|
2075
|
-
const expired = Date.now() > new Date(
|
|
2250
|
+
const expired = Date.now() > new Date(config.deadline).getTime();
|
|
2076
2251
|
if (expired) {
|
|
2077
2252
|
throw new InvalidParameterError('poll has expired');
|
|
2078
2253
|
}
|
|
2079
|
-
const max =
|
|
2254
|
+
const max = config.options.length;
|
|
2080
2255
|
const vote = dataBallot.vote;
|
|
2081
|
-
if (
|
|
2256
|
+
if (vote < 0 || vote > max) {
|
|
2082
2257
|
throw new InvalidParameterError('ballot.vote');
|
|
2083
2258
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
ballot: didBallot,
|
|
2089
|
-
received: new Date().toISOString(),
|
|
2090
|
-
};
|
|
2091
|
-
return this.mergeData(didPoll, { poll });
|
|
2259
|
+
// Store ballot DID as vault item keyed by voter's ballot key
|
|
2260
|
+
const buffer = Buffer.from(didBallot, 'utf-8');
|
|
2261
|
+
await this.addVaultItem(didPoll, voterBallotKey, buffer);
|
|
2262
|
+
return true;
|
|
2092
2263
|
}
|
|
2093
2264
|
async publishPoll(pollId, options = {}) {
|
|
2094
2265
|
const { reveal = false } = options;
|
|
@@ -2098,19 +2269,20 @@ export default class Keymaster {
|
|
|
2098
2269
|
if (id.did !== owner) {
|
|
2099
2270
|
throw new InvalidParameterError('only owner can publish a poll');
|
|
2100
2271
|
}
|
|
2101
|
-
const
|
|
2102
|
-
if (!
|
|
2103
|
-
throw new InvalidParameterError(
|
|
2272
|
+
const config = await this.getPoll(pollId);
|
|
2273
|
+
if (!config) {
|
|
2274
|
+
throw new InvalidParameterError(pollId);
|
|
2104
2275
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2276
|
+
const results = await this.computePollResults(pollId, config);
|
|
2277
|
+
if (!results.final) {
|
|
2278
|
+
throw new InvalidParameterError('poll not final');
|
|
2107
2279
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
throw new InvalidParameterError(pollId);
|
|
2280
|
+
if (!reveal) {
|
|
2281
|
+
delete results.ballots;
|
|
2111
2282
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2283
|
+
const buffer = Buffer.from(JSON.stringify(results), 'utf-8');
|
|
2284
|
+
await this.addVaultItem(pollId, PollItems.RESULTS, buffer);
|
|
2285
|
+
return true;
|
|
2114
2286
|
}
|
|
2115
2287
|
async unpublishPoll(pollId) {
|
|
2116
2288
|
const id = await this.fetchIdInfo();
|
|
@@ -2119,12 +2291,11 @@ export default class Keymaster {
|
|
|
2119
2291
|
if (id.did !== owner) {
|
|
2120
2292
|
throw new InvalidParameterError(pollId);
|
|
2121
2293
|
}
|
|
2122
|
-
const
|
|
2123
|
-
if (!
|
|
2294
|
+
const config = await this.getPoll(pollId);
|
|
2295
|
+
if (!config) {
|
|
2124
2296
|
throw new InvalidParameterError(pollId);
|
|
2125
2297
|
}
|
|
2126
|
-
|
|
2127
|
-
return this.mergeData(pollId, { poll });
|
|
2298
|
+
return this.removeVaultItem(pollId, PollItems.RESULTS);
|
|
2128
2299
|
}
|
|
2129
2300
|
async createVault(options = {}) {
|
|
2130
2301
|
const id = await this.fetchIdInfo();
|
|
@@ -2136,10 +2307,10 @@ export default class Keymaster {
|
|
|
2136
2307
|
const salt = this.cipher.generateRandomSalt();
|
|
2137
2308
|
const vaultKeypair = this.cipher.generateRandomJwk();
|
|
2138
2309
|
const keys = {};
|
|
2139
|
-
const config = this.cipher.encryptMessage(idKeypair.publicJwk,
|
|
2310
|
+
const config = this.cipher.encryptMessage(idKeypair.publicJwk, JSON.stringify(options));
|
|
2140
2311
|
const publicJwk = options.secretMembers ? idKeypair.publicJwk : vaultKeypair.publicJwk; // If secret, encrypt for the owner only
|
|
2141
|
-
const members = this.cipher.encryptMessage(publicJwk,
|
|
2142
|
-
const items = this.cipher.encryptMessage(vaultKeypair.publicJwk,
|
|
2312
|
+
const members = this.cipher.encryptMessage(publicJwk, JSON.stringify({}));
|
|
2313
|
+
const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, JSON.stringify({}));
|
|
2143
2314
|
const sha256 = this.cipher.hashJSON({});
|
|
2144
2315
|
const vault = {
|
|
2145
2316
|
version,
|
|
@@ -2170,6 +2341,9 @@ export default class Keymaster {
|
|
|
2170
2341
|
return false;
|
|
2171
2342
|
}
|
|
2172
2343
|
}
|
|
2344
|
+
generateBallotKey(vault, memberDID) {
|
|
2345
|
+
return this.generateSaltedId(vault, memberDID).slice(0, this.maxAliasLength);
|
|
2346
|
+
}
|
|
2173
2347
|
generateSaltedId(vault, memberDID) {
|
|
2174
2348
|
if (!vault.version) {
|
|
2175
2349
|
return this.cipher.hashMessage(vault.salt + memberDID);
|
|
@@ -2208,13 +2382,13 @@ export default class Keymaster {
|
|
|
2208
2382
|
}
|
|
2209
2383
|
else {
|
|
2210
2384
|
try {
|
|
2211
|
-
const membersJSON = this.cipher.decryptMessage(vault.
|
|
2385
|
+
const membersJSON = this.cipher.decryptMessage(privateJwk, vault.members, vault.publicJwk);
|
|
2212
2386
|
members = JSON.parse(membersJSON);
|
|
2213
2387
|
}
|
|
2214
2388
|
catch (error) {
|
|
2215
2389
|
}
|
|
2216
2390
|
}
|
|
2217
|
-
const itemsJSON = this.cipher.decryptMessage(vault.
|
|
2391
|
+
const itemsJSON = this.cipher.decryptMessage(privateJwk, vault.items, vault.publicJwk);
|
|
2218
2392
|
const items = JSON.parse(itemsJSON);
|
|
2219
2393
|
return {
|
|
2220
2394
|
isOwner,
|
|
@@ -2236,7 +2410,7 @@ export default class Keymaster {
|
|
|
2236
2410
|
async addMemberKey(vault, memberDID, privateJwk) {
|
|
2237
2411
|
const memberDoc = await this.resolveDID(memberDID, { confirm: true });
|
|
2238
2412
|
const memberPublicJwk = this.getPublicKeyJwk(memberDoc);
|
|
2239
|
-
const memberKey = this.cipher.encryptMessage(memberPublicJwk,
|
|
2413
|
+
const memberKey = this.cipher.encryptMessage(memberPublicJwk, JSON.stringify(privateJwk));
|
|
2240
2414
|
const memberKeyId = this.generateSaltedId(vault, memberDID);
|
|
2241
2415
|
vault.keys[memberKeyId] = memberKey;
|
|
2242
2416
|
}
|
|
@@ -2281,7 +2455,7 @@ export default class Keymaster {
|
|
|
2281
2455
|
}
|
|
2282
2456
|
members[memberDID] = { added: new Date().toISOString() };
|
|
2283
2457
|
const publicJwk = config.secretMembers ? idKeypair.publicJwk : vault.publicJwk;
|
|
2284
|
-
vault.members = this.cipher.encryptMessage(publicJwk,
|
|
2458
|
+
vault.members = this.cipher.encryptMessage(publicJwk, JSON.stringify(members));
|
|
2285
2459
|
await this.addMemberKey(vault, memberDID, privateJwk);
|
|
2286
2460
|
return this.mergeData(vaultId, { vault });
|
|
2287
2461
|
}
|
|
@@ -2289,7 +2463,7 @@ export default class Keymaster {
|
|
|
2289
2463
|
const owner = await this.checkVaultOwner(vaultId);
|
|
2290
2464
|
const idKeypair = await this.fetchKeyPair();
|
|
2291
2465
|
const vault = await this.getVault(vaultId);
|
|
2292
|
-
const {
|
|
2466
|
+
const { config, members } = await this.decryptVault(vault);
|
|
2293
2467
|
const memberDoc = await this.resolveDID(memberId, { confirm: true });
|
|
2294
2468
|
const memberDID = this.getAgentDID(memberDoc);
|
|
2295
2469
|
// Don't allow removing the vault owner
|
|
@@ -2298,7 +2472,7 @@ export default class Keymaster {
|
|
|
2298
2472
|
}
|
|
2299
2473
|
delete members[memberDID];
|
|
2300
2474
|
const publicJwk = config.secretMembers ? idKeypair.publicJwk : vault.publicJwk;
|
|
2301
|
-
vault.members = this.cipher.encryptMessage(publicJwk,
|
|
2475
|
+
vault.members = this.cipher.encryptMessage(publicJwk, JSON.stringify(members));
|
|
2302
2476
|
const memberKeyId = this.generateSaltedId(vault, memberDID);
|
|
2303
2477
|
delete vault.keys[memberKeyId];
|
|
2304
2478
|
return this.mergeData(vaultId, { vault });
|
|
@@ -2314,9 +2488,9 @@ export default class Keymaster {
|
|
|
2314
2488
|
async addVaultItem(vaultId, name, buffer) {
|
|
2315
2489
|
await this.checkVaultOwner(vaultId);
|
|
2316
2490
|
const vault = await this.getVault(vaultId);
|
|
2317
|
-
const {
|
|
2491
|
+
const { items } = await this.decryptVault(vault);
|
|
2318
2492
|
const validName = this.validateAlias(name);
|
|
2319
|
-
const encryptedData = this.cipher.encryptBytes(vault.publicJwk,
|
|
2493
|
+
const encryptedData = this.cipher.encryptBytes(vault.publicJwk, buffer);
|
|
2320
2494
|
const cid = await this.gatekeeper.addText(encryptedData);
|
|
2321
2495
|
const sha256 = this.cipher.hashMessage(buffer);
|
|
2322
2496
|
const type = await this.getMimeType(buffer);
|
|
@@ -2329,16 +2503,16 @@ export default class Keymaster {
|
|
|
2329
2503
|
added: new Date().toISOString(),
|
|
2330
2504
|
data,
|
|
2331
2505
|
};
|
|
2332
|
-
vault.items = this.cipher.encryptMessage(vault.publicJwk,
|
|
2506
|
+
vault.items = this.cipher.encryptMessage(vault.publicJwk, JSON.stringify(items));
|
|
2333
2507
|
vault.sha256 = this.cipher.hashJSON(items);
|
|
2334
2508
|
return this.mergeData(vaultId, { vault });
|
|
2335
2509
|
}
|
|
2336
2510
|
async removeVaultItem(vaultId, name) {
|
|
2337
2511
|
await this.checkVaultOwner(vaultId);
|
|
2338
2512
|
const vault = await this.getVault(vaultId);
|
|
2339
|
-
const {
|
|
2513
|
+
const { items } = await this.decryptVault(vault);
|
|
2340
2514
|
delete items[name];
|
|
2341
|
-
vault.items = this.cipher.encryptMessage(vault.publicJwk,
|
|
2515
|
+
vault.items = this.cipher.encryptMessage(vault.publicJwk, JSON.stringify(items));
|
|
2342
2516
|
vault.sha256 = this.cipher.hashJSON(items);
|
|
2343
2517
|
return this.mergeData(vaultId, { vault });
|
|
2344
2518
|
}
|
|
@@ -2357,7 +2531,7 @@ export default class Keymaster {
|
|
|
2357
2531
|
if (!encryptedData) {
|
|
2358
2532
|
throw new KeymasterError(`Failed to retrieve data for item '${name}' (CID: ${items[name].cid})`);
|
|
2359
2533
|
}
|
|
2360
|
-
const bytes = this.cipher.decryptBytes(
|
|
2534
|
+
const bytes = this.cipher.decryptBytes(privateJwk, encryptedData, vault.publicJwk);
|
|
2361
2535
|
return Buffer.from(bytes);
|
|
2362
2536
|
}
|
|
2363
2537
|
async listDmail() {
|
|
@@ -2641,7 +2815,7 @@ export default class Keymaster {
|
|
|
2641
2815
|
if (poll) {
|
|
2642
2816
|
const names = await this.listAliases();
|
|
2643
2817
|
if (!Object.values(names).includes(noticeDID)) {
|
|
2644
|
-
await this.addUnaliasedPoll(noticeDID);
|
|
2818
|
+
await this.addUnaliasedPoll(noticeDID, poll.name);
|
|
2645
2819
|
}
|
|
2646
2820
|
await this.addToNotices(did, [NoticeTags.POLL]);
|
|
2647
2821
|
continue;
|
|
@@ -2725,10 +2899,20 @@ export default class Keymaster {
|
|
|
2725
2899
|
}
|
|
2726
2900
|
return payload && typeof payload.poll === "string" && typeof payload.vote === "number";
|
|
2727
2901
|
}
|
|
2728
|
-
async addUnaliasedPoll(did) {
|
|
2729
|
-
const
|
|
2902
|
+
async addUnaliasedPoll(did, name) {
|
|
2903
|
+
const baseName = name || did.slice(-32);
|
|
2904
|
+
const aliases = await this.listAliases();
|
|
2905
|
+
let candidate = baseName;
|
|
2906
|
+
let suffix = 2;
|
|
2907
|
+
while (candidate in aliases) {
|
|
2908
|
+
if (aliases[candidate] === did) {
|
|
2909
|
+
return; // Already aliased to this DID
|
|
2910
|
+
}
|
|
2911
|
+
candidate = `${baseName}-${suffix}`;
|
|
2912
|
+
suffix++;
|
|
2913
|
+
}
|
|
2730
2914
|
try {
|
|
2731
|
-
await this.addAlias(
|
|
2915
|
+
await this.addAlias(candidate, did);
|
|
2732
2916
|
}
|
|
2733
2917
|
catch { }
|
|
2734
2918
|
}
|
|
@@ -2744,26 +2928,27 @@ export default class Keymaster {
|
|
|
2744
2928
|
const { version, seed, ...rest } = decrypted;
|
|
2745
2929
|
const safeSeed = { mnemonicEnc: seed.mnemonicEnc };
|
|
2746
2930
|
const hdkey = await this.getHDKeyFromCacheOrMnemonic(decrypted);
|
|
2747
|
-
const { publicJwk
|
|
2931
|
+
const { publicJwk } = this.cipher.generateJwk(hdkey.privateKey);
|
|
2748
2932
|
const plaintext = JSON.stringify(rest);
|
|
2749
|
-
const enc = this.cipher.encryptMessage(publicJwk,
|
|
2933
|
+
const enc = this.cipher.encryptMessage(publicJwk, plaintext);
|
|
2750
2934
|
return { version: version, seed: safeSeed, enc };
|
|
2751
2935
|
}
|
|
2752
2936
|
async decryptWalletFromStorage(stored) {
|
|
2753
2937
|
let mnemonic;
|
|
2754
2938
|
try {
|
|
2755
|
-
mnemonic = await
|
|
2939
|
+
mnemonic = await decryptWithPassphrase(stored.seed.mnemonicEnc, this.passphrase);
|
|
2756
2940
|
}
|
|
2757
2941
|
catch (error) {
|
|
2758
|
-
|
|
2759
|
-
|
|
2942
|
+
const msg = error?.message || '';
|
|
2943
|
+
// OperationError: Web Crypto API (legacy); 'invalid ghash tag': @noble/ciphers
|
|
2944
|
+
if (error?.name === 'OperationError' || msg.includes('invalid ghash tag')) {
|
|
2760
2945
|
throw new KeymasterError('Incorrect passphrase.');
|
|
2761
2946
|
}
|
|
2762
2947
|
throw error;
|
|
2763
2948
|
}
|
|
2764
2949
|
this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
|
|
2765
2950
|
const { publicJwk, privateJwk } = this.cipher.generateJwk(this._hdkeyCache.privateKey);
|
|
2766
|
-
const plaintext = this.cipher.decryptMessage(
|
|
2951
|
+
const plaintext = this.cipher.decryptMessage(privateJwk, stored.enc, publicJwk);
|
|
2767
2952
|
const data = JSON.parse(plaintext);
|
|
2768
2953
|
const wallet = { version: stored.version, seed: stored.seed, ...data };
|
|
2769
2954
|
return wallet;
|