@didcid/keymaster 0.3.10 → 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/dist/cjs/index.cjs +0 -1
- package/dist/cjs/keymaster-client.cjs +74 -2
- package/dist/cjs/keymaster.cjs +1949 -206
- package/dist/cjs/node.cjs +0 -1
- package/dist/esm/cli.js +111 -7
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/keymaster-client.js +74 -2
- package/dist/esm/keymaster-client.js.map +1 -1
- package/dist/esm/keymaster.js +374 -206
- package/dist/esm/keymaster.js.map +1 -1
- package/dist/types/keymaster-client.d.ts +12 -5
- package/dist/types/keymaster.d.ts +17 -5
- package/dist/types/types.d.ts +24 -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,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
|
|
@@ -697,19 +702,16 @@ export default class Keymaster {
|
|
|
697
702
|
}
|
|
698
703
|
async encryptMessage(msg, receiver, options = {}) {
|
|
699
704
|
const { encryptForSender = true, includeHash = false, } = options;
|
|
700
|
-
const id = await this.fetchIdInfo();
|
|
701
705
|
const senderKeypair = await this.fetchKeyPair();
|
|
702
706
|
if (!senderKeypair) {
|
|
703
707
|
throw new KeymasterError('No valid sender keypair');
|
|
704
708
|
}
|
|
705
709
|
const doc = await this.resolveDID(receiver, { confirm: true });
|
|
706
710
|
const receivePublicJwk = this.getPublicKeyJwk(doc);
|
|
707
|
-
const cipher_sender = encryptForSender ? this.cipher.encryptMessage(senderKeypair.publicJwk,
|
|
708
|
-
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);
|
|
709
713
|
const cipher_hash = includeHash ? this.cipher.hashMessage(msg) : null;
|
|
710
714
|
const encrypted = {
|
|
711
|
-
sender: id.did,
|
|
712
|
-
created: new Date().toISOString(),
|
|
713
715
|
cipher_hash,
|
|
714
716
|
cipher_sender,
|
|
715
717
|
cipher_receiver,
|
|
@@ -725,7 +727,7 @@ export default class Keymaster {
|
|
|
725
727
|
const didkey = hdkey.derive(path);
|
|
726
728
|
const receiverKeypair = this.cipher.generateJwk(didkey.privateKey);
|
|
727
729
|
try {
|
|
728
|
-
return this.cipher.decryptMessage(
|
|
730
|
+
return this.cipher.decryptMessage(receiverKeypair.privateJwk, ciphertext, senderPublicJwk);
|
|
729
731
|
}
|
|
730
732
|
catch (error) {
|
|
731
733
|
index -= 1;
|
|
@@ -736,7 +738,8 @@ export default class Keymaster {
|
|
|
736
738
|
async decryptMessage(did) {
|
|
737
739
|
const wallet = await this.loadWallet();
|
|
738
740
|
const id = await this.fetchIdInfo();
|
|
739
|
-
const
|
|
741
|
+
const msgDoc = await this.resolveDID(did);
|
|
742
|
+
const asset = msgDoc.didDocumentData;
|
|
740
743
|
if (!asset) {
|
|
741
744
|
throw new InvalidParameterError('did not encrypted');
|
|
742
745
|
}
|
|
@@ -745,9 +748,16 @@ export default class Keymaster {
|
|
|
745
748
|
throw new InvalidParameterError('did not encrypted');
|
|
746
749
|
}
|
|
747
750
|
const crypt = (castAsset.encrypted ? castAsset.encrypted : castAsset);
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
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;
|
|
751
761
|
return await this.decryptWithDerivedKeys(wallet, id, senderPublicJwk, ciphertext);
|
|
752
762
|
}
|
|
753
763
|
async encryptJSON(json, did, options = {}) {
|
|
@@ -1128,7 +1138,7 @@ export default class Keymaster {
|
|
|
1128
1138
|
id: idInfo,
|
|
1129
1139
|
};
|
|
1130
1140
|
const msg = JSON.stringify(data);
|
|
1131
|
-
const backup = this.cipher.encryptMessage(keypair.publicJwk,
|
|
1141
|
+
const backup = this.cipher.encryptMessage(keypair.publicJwk, msg);
|
|
1132
1142
|
const doc = await this.resolveDID(idInfo.did);
|
|
1133
1143
|
const registry = doc.didDocumentRegistration?.registry;
|
|
1134
1144
|
if (!registry) {
|
|
@@ -1154,7 +1164,7 @@ export default class Keymaster {
|
|
|
1154
1164
|
if (typeof backupStore.backup !== 'string') {
|
|
1155
1165
|
throw new InvalidDIDError('backup not found in backupStore');
|
|
1156
1166
|
}
|
|
1157
|
-
const backup = this.cipher.decryptMessage(keypair.
|
|
1167
|
+
const backup = this.cipher.decryptMessage(keypair.privateJwk, backupStore.backup, keypair.publicJwk);
|
|
1158
1168
|
const data = JSON.parse(backup);
|
|
1159
1169
|
await this.mutateWallet((wallet) => {
|
|
1160
1170
|
if (wallet.ids[data.name]) {
|
|
@@ -1255,7 +1265,13 @@ export default class Keymaster {
|
|
|
1255
1265
|
validFrom = new Date().toISOString();
|
|
1256
1266
|
}
|
|
1257
1267
|
const id = await this.fetchIdInfo();
|
|
1258
|
-
|
|
1268
|
+
let subjectURI;
|
|
1269
|
+
try {
|
|
1270
|
+
subjectURI = await this.lookupDID(subjectId);
|
|
1271
|
+
}
|
|
1272
|
+
catch {
|
|
1273
|
+
subjectURI = subjectId;
|
|
1274
|
+
}
|
|
1259
1275
|
const vc = {
|
|
1260
1276
|
"@context": [
|
|
1261
1277
|
"https://www.w3.org/ns/credentials/v2",
|
|
@@ -1266,7 +1282,7 @@ export default class Keymaster {
|
|
|
1266
1282
|
validFrom,
|
|
1267
1283
|
validUntil,
|
|
1268
1284
|
credentialSubject: {
|
|
1269
|
-
id:
|
|
1285
|
+
id: subjectURI,
|
|
1270
1286
|
},
|
|
1271
1287
|
};
|
|
1272
1288
|
// If schema provided, add credentialSchema and generate claims from schema
|
|
@@ -1291,7 +1307,7 @@ export default class Keymaster {
|
|
|
1291
1307
|
}
|
|
1292
1308
|
if (claims && Object.keys(claims).length) {
|
|
1293
1309
|
vc.credentialSubject = {
|
|
1294
|
-
id:
|
|
1310
|
+
id: subjectURI,
|
|
1295
1311
|
...claims,
|
|
1296
1312
|
};
|
|
1297
1313
|
}
|
|
@@ -1306,21 +1322,32 @@ export default class Keymaster {
|
|
|
1306
1322
|
throw new InvalidParameterError('credential.issuer');
|
|
1307
1323
|
}
|
|
1308
1324
|
const signed = await this.addProof(credential);
|
|
1309
|
-
|
|
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 });
|
|
1310
1330
|
}
|
|
1311
1331
|
async sendCredential(did, options = {}) {
|
|
1312
1332
|
const vc = await this.getCredential(did);
|
|
1313
1333
|
if (!vc) {
|
|
1314
1334
|
return null;
|
|
1315
1335
|
}
|
|
1336
|
+
const subjectId = vc.credentialSubject.id;
|
|
1337
|
+
if (!this.isManagedDID(subjectId)) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1316
1340
|
const registry = this.ephemeralRegistry;
|
|
1317
1341
|
const validUntil = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Default to 7 days
|
|
1318
1342
|
const message = {
|
|
1319
|
-
to: [
|
|
1343
|
+
to: [subjectId],
|
|
1320
1344
|
dids: [did],
|
|
1321
1345
|
};
|
|
1322
1346
|
return this.createNotice(message, { registry, validUntil, ...options });
|
|
1323
1347
|
}
|
|
1348
|
+
isManagedDID(value) {
|
|
1349
|
+
return value.startsWith('did:cid:');
|
|
1350
|
+
}
|
|
1324
1351
|
isVerifiableCredential(obj) {
|
|
1325
1352
|
if (typeof obj !== 'object' || !obj) {
|
|
1326
1353
|
return false;
|
|
@@ -1342,24 +1369,29 @@ export default class Keymaster {
|
|
|
1342
1369
|
delete credential.proof;
|
|
1343
1370
|
const signed = await this.addProof(credential);
|
|
1344
1371
|
const msg = JSON.stringify(signed);
|
|
1345
|
-
const id = await this.fetchIdInfo();
|
|
1346
1372
|
const senderKeypair = await this.fetchKeyPair();
|
|
1347
1373
|
if (!senderKeypair) {
|
|
1348
1374
|
throw new KeymasterError('No valid sender keypair');
|
|
1349
1375
|
}
|
|
1350
1376
|
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
1377
|
const msgHash = this.cipher.hashMessage(msg);
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
+
}
|
|
1363
1395
|
return this.updateDID(did, { didDocumentData: { encrypted } });
|
|
1364
1396
|
}
|
|
1365
1397
|
async revokeCredential(credential) {
|
|
@@ -1855,72 +1887,64 @@ export default class Keymaster {
|
|
|
1855
1887
|
const nextWeek = new Date();
|
|
1856
1888
|
nextWeek.setDate(now.getDate() + 7);
|
|
1857
1889
|
return {
|
|
1858
|
-
|
|
1859
|
-
|
|
1890
|
+
version: 2,
|
|
1891
|
+
name: 'poll-name',
|
|
1860
1892
|
description: 'What is this poll about?',
|
|
1861
|
-
roster: 'DID of the eligible voter group',
|
|
1862
1893
|
options: ['yes', 'no', 'abstain'],
|
|
1863
1894
|
deadline: nextWeek.toISOString(),
|
|
1864
1895
|
};
|
|
1865
1896
|
}
|
|
1866
|
-
async createPoll(
|
|
1867
|
-
if (
|
|
1868
|
-
throw new InvalidParameterError('poll');
|
|
1869
|
-
}
|
|
1870
|
-
if (poll.version !== 1) {
|
|
1897
|
+
async createPoll(config, options = {}) {
|
|
1898
|
+
if (config.version !== 2) {
|
|
1871
1899
|
throw new InvalidParameterError('poll.version');
|
|
1872
1900
|
}
|
|
1873
|
-
if (!
|
|
1901
|
+
if (!config.name) {
|
|
1902
|
+
throw new InvalidParameterError('poll.name');
|
|
1903
|
+
}
|
|
1904
|
+
if (!config.description) {
|
|
1874
1905
|
throw new InvalidParameterError('poll.description');
|
|
1875
1906
|
}
|
|
1876
|
-
if (!
|
|
1907
|
+
if (!config.options || !Array.isArray(config.options) || config.options.length < 2 || config.options.length > 10) {
|
|
1877
1908
|
throw new InvalidParameterError('poll.options');
|
|
1878
1909
|
}
|
|
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
|
|
1910
|
+
if (!config.deadline) {
|
|
1894
1911
|
throw new InvalidParameterError('poll.deadline');
|
|
1895
1912
|
}
|
|
1896
|
-
const deadline = new Date(
|
|
1913
|
+
const deadline = new Date(config.deadline);
|
|
1897
1914
|
if (isNaN(deadline.getTime())) {
|
|
1898
1915
|
throw new InvalidParameterError('poll.deadline');
|
|
1899
1916
|
}
|
|
1900
1917
|
if (deadline < new Date()) {
|
|
1901
1918
|
throw new InvalidParameterError('poll.deadline');
|
|
1902
1919
|
}
|
|
1903
|
-
|
|
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;
|
|
1904
1924
|
}
|
|
1905
1925
|
async getPoll(id) {
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
if (castOldAsset.options) {
|
|
1910
|
-
return castOldAsset;
|
|
1926
|
+
const isVault = await this.testVault(id);
|
|
1927
|
+
if (!isVault) {
|
|
1928
|
+
return null;
|
|
1911
1929
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
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 {
|
|
1914
1939
|
return null;
|
|
1915
1940
|
}
|
|
1916
|
-
return castAsset.poll;
|
|
1917
1941
|
}
|
|
1918
1942
|
async testPoll(id) {
|
|
1919
1943
|
try {
|
|
1920
|
-
const
|
|
1921
|
-
return
|
|
1944
|
+
const config = await this.getPoll(id);
|
|
1945
|
+
return config !== null;
|
|
1922
1946
|
}
|
|
1923
|
-
catch
|
|
1947
|
+
catch {
|
|
1924
1948
|
return false;
|
|
1925
1949
|
}
|
|
1926
1950
|
}
|
|
@@ -1935,113 +1959,243 @@ export default class Keymaster {
|
|
|
1935
1959
|
}
|
|
1936
1960
|
return polls;
|
|
1937
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
|
+
}
|
|
1938
1983
|
async viewPoll(pollId) {
|
|
1939
1984
|
const id = await this.fetchIdInfo();
|
|
1940
|
-
const
|
|
1941
|
-
if (!
|
|
1985
|
+
const config = await this.getPoll(pollId);
|
|
1986
|
+
if (!config) {
|
|
1942
1987
|
throw new InvalidParameterError('pollId');
|
|
1943
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;
|
|
1944
1993
|
let hasVoted = false;
|
|
1945
|
-
|
|
1946
|
-
|
|
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;
|
|
1947
2010
|
}
|
|
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
2011
|
const view = {
|
|
1952
|
-
description:
|
|
1953
|
-
options:
|
|
1954
|
-
deadline:
|
|
1955
|
-
isOwner
|
|
1956
|
-
isEligible
|
|
1957
|
-
voteExpired
|
|
1958
|
-
hasVoted
|
|
2012
|
+
description: config.description,
|
|
2013
|
+
options: config.options,
|
|
2014
|
+
deadline: config.deadline,
|
|
2015
|
+
isOwner,
|
|
2016
|
+
isEligible,
|
|
2017
|
+
voteExpired,
|
|
2018
|
+
hasVoted,
|
|
2019
|
+
ballots,
|
|
1959
2020
|
};
|
|
1960
|
-
if (
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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 || '',
|
|
1976
2076
|
});
|
|
1977
2077
|
}
|
|
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;
|
|
2078
|
+
voted += 1;
|
|
2079
|
+
if (vote >= 0 && vote < results.tally.length) {
|
|
1991
2080
|
results.tally[vote].count += 1;
|
|
1992
2081
|
}
|
|
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
2082
|
}
|
|
2003
|
-
|
|
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;
|
|
2004
2092
|
}
|
|
2005
2093
|
async votePoll(pollId, vote, options = {}) {
|
|
2006
|
-
const { spoil = false } = options;
|
|
2007
2094
|
const id = await this.fetchIdInfo();
|
|
2008
2095
|
const didPoll = await this.lookupDID(pollId);
|
|
2009
2096
|
const doc = await this.resolveDID(didPoll);
|
|
2010
|
-
const
|
|
2011
|
-
if (!
|
|
2097
|
+
const config = await this.getPoll(pollId);
|
|
2098
|
+
if (!config) {
|
|
2012
2099
|
throw new InvalidParameterError('pollId');
|
|
2013
2100
|
}
|
|
2014
|
-
const eligible = await this.testGroup(poll.roster, id.did);
|
|
2015
|
-
const expired = Date.now() > new Date(poll.deadline).getTime();
|
|
2016
2101
|
const owner = doc.didDocument?.controller;
|
|
2017
2102
|
if (!owner) {
|
|
2018
|
-
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;
|
|
2109
|
+
}
|
|
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
|
+
}
|
|
2019
2119
|
}
|
|
2020
|
-
if (!
|
|
2021
|
-
throw new InvalidParameterError('voter not
|
|
2120
|
+
if (!isEligible) {
|
|
2121
|
+
throw new InvalidParameterError('voter is not a poll member');
|
|
2022
2122
|
}
|
|
2123
|
+
const expired = Date.now() > new Date(config.deadline).getTime();
|
|
2023
2124
|
if (expired) {
|
|
2024
2125
|
throw new InvalidParameterError('poll has expired');
|
|
2025
2126
|
}
|
|
2026
|
-
|
|
2027
|
-
if (
|
|
2028
|
-
|
|
2029
|
-
poll: didPoll,
|
|
2030
|
-
vote: 0,
|
|
2031
|
-
};
|
|
2127
|
+
const max = config.options.length;
|
|
2128
|
+
if (!Number.isInteger(vote) || vote < 0 || vote > max) {
|
|
2129
|
+
throw new InvalidParameterError('vote');
|
|
2032
2130
|
}
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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];
|
|
2037
2190
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2191
|
+
else if (data.vote === 0) {
|
|
2192
|
+
result.option = 'spoil';
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
catch {
|
|
2196
|
+
// Caller cannot decrypt (not the owner) — return limited info
|
|
2042
2197
|
}
|
|
2043
|
-
|
|
2044
|
-
return await this.encryptJSON(ballot, owner, { ...options, encryptForSender: false });
|
|
2198
|
+
return result;
|
|
2045
2199
|
}
|
|
2046
2200
|
async updatePoll(ballot) {
|
|
2047
2201
|
const id = await this.fetchIdInfo();
|
|
@@ -2051,7 +2205,7 @@ export default class Keymaster {
|
|
|
2051
2205
|
let dataBallot;
|
|
2052
2206
|
try {
|
|
2053
2207
|
dataBallot = await this.decryptJSON(didBallot);
|
|
2054
|
-
if (!dataBallot.poll ||
|
|
2208
|
+
if (!dataBallot.poll || dataBallot.vote === undefined) {
|
|
2055
2209
|
throw new InvalidParameterError('ballot');
|
|
2056
2210
|
}
|
|
2057
2211
|
}
|
|
@@ -2061,34 +2215,34 @@ export default class Keymaster {
|
|
|
2061
2215
|
const didPoll = dataBallot.poll;
|
|
2062
2216
|
const docPoll = await this.resolveDID(didPoll);
|
|
2063
2217
|
const didOwner = docPoll.didDocument.controller;
|
|
2064
|
-
const
|
|
2065
|
-
if (!
|
|
2218
|
+
const config = await this.getPoll(didPoll);
|
|
2219
|
+
if (!config) {
|
|
2066
2220
|
throw new KeymasterError('Cannot find poll related to ballot');
|
|
2067
2221
|
}
|
|
2068
2222
|
if (id.did !== didOwner) {
|
|
2069
2223
|
throw new InvalidParameterError('only owner can update a poll');
|
|
2070
2224
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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');
|
|
2074
2232
|
}
|
|
2075
|
-
const expired = Date.now() > new Date(
|
|
2233
|
+
const expired = Date.now() > new Date(config.deadline).getTime();
|
|
2076
2234
|
if (expired) {
|
|
2077
2235
|
throw new InvalidParameterError('poll has expired');
|
|
2078
2236
|
}
|
|
2079
|
-
const max =
|
|
2237
|
+
const max = config.options.length;
|
|
2080
2238
|
const vote = dataBallot.vote;
|
|
2081
|
-
if (
|
|
2239
|
+
if (vote < 0 || vote > max) {
|
|
2082
2240
|
throw new InvalidParameterError('ballot.vote');
|
|
2083
2241
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
ballot: didBallot,
|
|
2089
|
-
received: new Date().toISOString(),
|
|
2090
|
-
};
|
|
2091
|
-
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;
|
|
2092
2246
|
}
|
|
2093
2247
|
async publishPoll(pollId, options = {}) {
|
|
2094
2248
|
const { reveal = false } = options;
|
|
@@ -2098,19 +2252,20 @@ export default class Keymaster {
|
|
|
2098
2252
|
if (id.did !== owner) {
|
|
2099
2253
|
throw new InvalidParameterError('only owner can publish a poll');
|
|
2100
2254
|
}
|
|
2101
|
-
const
|
|
2102
|
-
if (!
|
|
2103
|
-
throw new InvalidParameterError(
|
|
2255
|
+
const config = await this.getPoll(pollId);
|
|
2256
|
+
if (!config) {
|
|
2257
|
+
throw new InvalidParameterError(pollId);
|
|
2104
2258
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2259
|
+
const results = await this.computePollResults(pollId, config);
|
|
2260
|
+
if (!results.final) {
|
|
2261
|
+
throw new InvalidParameterError('poll not final');
|
|
2107
2262
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
throw new InvalidParameterError(pollId);
|
|
2263
|
+
if (!reveal) {
|
|
2264
|
+
delete results.ballots;
|
|
2111
2265
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2266
|
+
const buffer = Buffer.from(JSON.stringify(results), 'utf-8');
|
|
2267
|
+
await this.addVaultItem(pollId, PollItems.RESULTS, buffer);
|
|
2268
|
+
return true;
|
|
2114
2269
|
}
|
|
2115
2270
|
async unpublishPoll(pollId) {
|
|
2116
2271
|
const id = await this.fetchIdInfo();
|
|
@@ -2119,12 +2274,11 @@ export default class Keymaster {
|
|
|
2119
2274
|
if (id.did !== owner) {
|
|
2120
2275
|
throw new InvalidParameterError(pollId);
|
|
2121
2276
|
}
|
|
2122
|
-
const
|
|
2123
|
-
if (!
|
|
2277
|
+
const config = await this.getPoll(pollId);
|
|
2278
|
+
if (!config) {
|
|
2124
2279
|
throw new InvalidParameterError(pollId);
|
|
2125
2280
|
}
|
|
2126
|
-
|
|
2127
|
-
return this.mergeData(pollId, { poll });
|
|
2281
|
+
return this.removeVaultItem(pollId, PollItems.RESULTS);
|
|
2128
2282
|
}
|
|
2129
2283
|
async createVault(options = {}) {
|
|
2130
2284
|
const id = await this.fetchIdInfo();
|
|
@@ -2136,10 +2290,10 @@ export default class Keymaster {
|
|
|
2136
2290
|
const salt = this.cipher.generateRandomSalt();
|
|
2137
2291
|
const vaultKeypair = this.cipher.generateRandomJwk();
|
|
2138
2292
|
const keys = {};
|
|
2139
|
-
const config = this.cipher.encryptMessage(idKeypair.publicJwk,
|
|
2293
|
+
const config = this.cipher.encryptMessage(idKeypair.publicJwk, JSON.stringify(options));
|
|
2140
2294
|
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,
|
|
2295
|
+
const members = this.cipher.encryptMessage(publicJwk, JSON.stringify({}));
|
|
2296
|
+
const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, JSON.stringify({}));
|
|
2143
2297
|
const sha256 = this.cipher.hashJSON({});
|
|
2144
2298
|
const vault = {
|
|
2145
2299
|
version,
|
|
@@ -2170,6 +2324,9 @@ export default class Keymaster {
|
|
|
2170
2324
|
return false;
|
|
2171
2325
|
}
|
|
2172
2326
|
}
|
|
2327
|
+
generateBallotKey(vault, memberDID) {
|
|
2328
|
+
return this.generateSaltedId(vault, memberDID).slice(0, this.maxAliasLength);
|
|
2329
|
+
}
|
|
2173
2330
|
generateSaltedId(vault, memberDID) {
|
|
2174
2331
|
if (!vault.version) {
|
|
2175
2332
|
return this.cipher.hashMessage(vault.salt + memberDID);
|
|
@@ -2208,13 +2365,13 @@ export default class Keymaster {
|
|
|
2208
2365
|
}
|
|
2209
2366
|
else {
|
|
2210
2367
|
try {
|
|
2211
|
-
const membersJSON = this.cipher.decryptMessage(vault.
|
|
2368
|
+
const membersJSON = this.cipher.decryptMessage(privateJwk, vault.members, vault.publicJwk);
|
|
2212
2369
|
members = JSON.parse(membersJSON);
|
|
2213
2370
|
}
|
|
2214
2371
|
catch (error) {
|
|
2215
2372
|
}
|
|
2216
2373
|
}
|
|
2217
|
-
const itemsJSON = this.cipher.decryptMessage(vault.
|
|
2374
|
+
const itemsJSON = this.cipher.decryptMessage(privateJwk, vault.items, vault.publicJwk);
|
|
2218
2375
|
const items = JSON.parse(itemsJSON);
|
|
2219
2376
|
return {
|
|
2220
2377
|
isOwner,
|
|
@@ -2236,7 +2393,7 @@ export default class Keymaster {
|
|
|
2236
2393
|
async addMemberKey(vault, memberDID, privateJwk) {
|
|
2237
2394
|
const memberDoc = await this.resolveDID(memberDID, { confirm: true });
|
|
2238
2395
|
const memberPublicJwk = this.getPublicKeyJwk(memberDoc);
|
|
2239
|
-
const memberKey = this.cipher.encryptMessage(memberPublicJwk,
|
|
2396
|
+
const memberKey = this.cipher.encryptMessage(memberPublicJwk, JSON.stringify(privateJwk));
|
|
2240
2397
|
const memberKeyId = this.generateSaltedId(vault, memberDID);
|
|
2241
2398
|
vault.keys[memberKeyId] = memberKey;
|
|
2242
2399
|
}
|
|
@@ -2281,7 +2438,7 @@ export default class Keymaster {
|
|
|
2281
2438
|
}
|
|
2282
2439
|
members[memberDID] = { added: new Date().toISOString() };
|
|
2283
2440
|
const publicJwk = config.secretMembers ? idKeypair.publicJwk : vault.publicJwk;
|
|
2284
|
-
vault.members = this.cipher.encryptMessage(publicJwk,
|
|
2441
|
+
vault.members = this.cipher.encryptMessage(publicJwk, JSON.stringify(members));
|
|
2285
2442
|
await this.addMemberKey(vault, memberDID, privateJwk);
|
|
2286
2443
|
return this.mergeData(vaultId, { vault });
|
|
2287
2444
|
}
|
|
@@ -2289,7 +2446,7 @@ export default class Keymaster {
|
|
|
2289
2446
|
const owner = await this.checkVaultOwner(vaultId);
|
|
2290
2447
|
const idKeypair = await this.fetchKeyPair();
|
|
2291
2448
|
const vault = await this.getVault(vaultId);
|
|
2292
|
-
const {
|
|
2449
|
+
const { config, members } = await this.decryptVault(vault);
|
|
2293
2450
|
const memberDoc = await this.resolveDID(memberId, { confirm: true });
|
|
2294
2451
|
const memberDID = this.getAgentDID(memberDoc);
|
|
2295
2452
|
// Don't allow removing the vault owner
|
|
@@ -2298,7 +2455,7 @@ export default class Keymaster {
|
|
|
2298
2455
|
}
|
|
2299
2456
|
delete members[memberDID];
|
|
2300
2457
|
const publicJwk = config.secretMembers ? idKeypair.publicJwk : vault.publicJwk;
|
|
2301
|
-
vault.members = this.cipher.encryptMessage(publicJwk,
|
|
2458
|
+
vault.members = this.cipher.encryptMessage(publicJwk, JSON.stringify(members));
|
|
2302
2459
|
const memberKeyId = this.generateSaltedId(vault, memberDID);
|
|
2303
2460
|
delete vault.keys[memberKeyId];
|
|
2304
2461
|
return this.mergeData(vaultId, { vault });
|
|
@@ -2314,9 +2471,9 @@ export default class Keymaster {
|
|
|
2314
2471
|
async addVaultItem(vaultId, name, buffer) {
|
|
2315
2472
|
await this.checkVaultOwner(vaultId);
|
|
2316
2473
|
const vault = await this.getVault(vaultId);
|
|
2317
|
-
const {
|
|
2474
|
+
const { items } = await this.decryptVault(vault);
|
|
2318
2475
|
const validName = this.validateAlias(name);
|
|
2319
|
-
const encryptedData = this.cipher.encryptBytes(vault.publicJwk,
|
|
2476
|
+
const encryptedData = this.cipher.encryptBytes(vault.publicJwk, buffer);
|
|
2320
2477
|
const cid = await this.gatekeeper.addText(encryptedData);
|
|
2321
2478
|
const sha256 = this.cipher.hashMessage(buffer);
|
|
2322
2479
|
const type = await this.getMimeType(buffer);
|
|
@@ -2329,16 +2486,16 @@ export default class Keymaster {
|
|
|
2329
2486
|
added: new Date().toISOString(),
|
|
2330
2487
|
data,
|
|
2331
2488
|
};
|
|
2332
|
-
vault.items = this.cipher.encryptMessage(vault.publicJwk,
|
|
2489
|
+
vault.items = this.cipher.encryptMessage(vault.publicJwk, JSON.stringify(items));
|
|
2333
2490
|
vault.sha256 = this.cipher.hashJSON(items);
|
|
2334
2491
|
return this.mergeData(vaultId, { vault });
|
|
2335
2492
|
}
|
|
2336
2493
|
async removeVaultItem(vaultId, name) {
|
|
2337
2494
|
await this.checkVaultOwner(vaultId);
|
|
2338
2495
|
const vault = await this.getVault(vaultId);
|
|
2339
|
-
const {
|
|
2496
|
+
const { items } = await this.decryptVault(vault);
|
|
2340
2497
|
delete items[name];
|
|
2341
|
-
vault.items = this.cipher.encryptMessage(vault.publicJwk,
|
|
2498
|
+
vault.items = this.cipher.encryptMessage(vault.publicJwk, JSON.stringify(items));
|
|
2342
2499
|
vault.sha256 = this.cipher.hashJSON(items);
|
|
2343
2500
|
return this.mergeData(vaultId, { vault });
|
|
2344
2501
|
}
|
|
@@ -2357,7 +2514,7 @@ export default class Keymaster {
|
|
|
2357
2514
|
if (!encryptedData) {
|
|
2358
2515
|
throw new KeymasterError(`Failed to retrieve data for item '${name}' (CID: ${items[name].cid})`);
|
|
2359
2516
|
}
|
|
2360
|
-
const bytes = this.cipher.decryptBytes(
|
|
2517
|
+
const bytes = this.cipher.decryptBytes(privateJwk, encryptedData, vault.publicJwk);
|
|
2361
2518
|
return Buffer.from(bytes);
|
|
2362
2519
|
}
|
|
2363
2520
|
async listDmail() {
|
|
@@ -2641,7 +2798,7 @@ export default class Keymaster {
|
|
|
2641
2798
|
if (poll) {
|
|
2642
2799
|
const names = await this.listAliases();
|
|
2643
2800
|
if (!Object.values(names).includes(noticeDID)) {
|
|
2644
|
-
await this.addUnaliasedPoll(noticeDID);
|
|
2801
|
+
await this.addUnaliasedPoll(noticeDID, poll.name);
|
|
2645
2802
|
}
|
|
2646
2803
|
await this.addToNotices(did, [NoticeTags.POLL]);
|
|
2647
2804
|
continue;
|
|
@@ -2725,10 +2882,20 @@ export default class Keymaster {
|
|
|
2725
2882
|
}
|
|
2726
2883
|
return payload && typeof payload.poll === "string" && typeof payload.vote === "number";
|
|
2727
2884
|
}
|
|
2728
|
-
async addUnaliasedPoll(did) {
|
|
2729
|
-
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
|
+
}
|
|
2730
2897
|
try {
|
|
2731
|
-
await this.addAlias(
|
|
2898
|
+
await this.addAlias(candidate, did);
|
|
2732
2899
|
}
|
|
2733
2900
|
catch { }
|
|
2734
2901
|
}
|
|
@@ -2744,26 +2911,27 @@ export default class Keymaster {
|
|
|
2744
2911
|
const { version, seed, ...rest } = decrypted;
|
|
2745
2912
|
const safeSeed = { mnemonicEnc: seed.mnemonicEnc };
|
|
2746
2913
|
const hdkey = await this.getHDKeyFromCacheOrMnemonic(decrypted);
|
|
2747
|
-
const { publicJwk
|
|
2914
|
+
const { publicJwk } = this.cipher.generateJwk(hdkey.privateKey);
|
|
2748
2915
|
const plaintext = JSON.stringify(rest);
|
|
2749
|
-
const enc = this.cipher.encryptMessage(publicJwk,
|
|
2916
|
+
const enc = this.cipher.encryptMessage(publicJwk, plaintext);
|
|
2750
2917
|
return { version: version, seed: safeSeed, enc };
|
|
2751
2918
|
}
|
|
2752
2919
|
async decryptWalletFromStorage(stored) {
|
|
2753
2920
|
let mnemonic;
|
|
2754
2921
|
try {
|
|
2755
|
-
mnemonic = await
|
|
2922
|
+
mnemonic = await decryptWithPassphrase(stored.seed.mnemonicEnc, this.passphrase);
|
|
2756
2923
|
}
|
|
2757
2924
|
catch (error) {
|
|
2758
|
-
|
|
2759
|
-
|
|
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')) {
|
|
2760
2928
|
throw new KeymasterError('Incorrect passphrase.');
|
|
2761
2929
|
}
|
|
2762
2930
|
throw error;
|
|
2763
2931
|
}
|
|
2764
2932
|
this._hdkeyCache = this.cipher.generateHDKey(mnemonic);
|
|
2765
2933
|
const { publicJwk, privateJwk } = this.cipher.generateJwk(this._hdkeyCache.privateKey);
|
|
2766
|
-
const plaintext = this.cipher.decryptMessage(
|
|
2934
|
+
const plaintext = this.cipher.decryptMessage(privateJwk, stored.enc, publicJwk);
|
|
2767
2935
|
const data = JSON.parse(plaintext);
|
|
2768
2936
|
const wallet = { version: stored.version, seed: stored.seed, ...data };
|
|
2769
2937
|
return wallet;
|