@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.
@@ -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 { decMnemonic, encMnemonic } from "./encryption.js";
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 encMnemonic(mnemonic, this.passphrase);
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 decMnemonic(wallet.seed.mnemonicEnc, this.passphrase);
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, keypair.privateJwk, msg);
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.publicJwk, keypair.privateJwk, castData.backup);
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 encMnemonic(mnemonic, this.passphrase);
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, senderKeypair.privateJwk, msg) : null;
708
- const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
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(senderPublicJwk, receiverKeypair.privateJwk, ciphertext);
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 asset = await this.resolveAsset(did);
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
- const doc = await this.resolveDID(crypt.sender, { confirm: true, versionTime: crypt.created });
749
- const senderPublicJwk = this.getPublicKeyJwk(doc);
750
- const ciphertext = (crypt.sender === id.did && crypt.cipher_sender) ? crypt.cipher_sender : crypt.cipher_receiver;
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, keypair.privateJwk, msg);
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.publicJwk, keypair.privateJwk, backupStore.backup);
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
- const subjectDID = await this.lookupDID(subjectId);
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: subjectDID,
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: subjectDID,
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
- return this.encryptJSON(signed, credential.credentialSubject.id, { ...options, includeHash: true });
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: [vc.credentialSubject.id],
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
- const encrypted = {
1357
- sender: id.did,
1358
- created: new Date().toISOString(),
1359
- cipher_hash: msgHash,
1360
- cipher_sender: cipher_sender,
1361
- cipher_receiver: cipher_receiver,
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
- type: 'poll',
1859
- version: 1,
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(poll, options = {}) {
1867
- if (poll.type !== 'poll') {
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 (!poll.description) {
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 (!poll.options || !Array.isArray(poll.options) || poll.options.length < 2 || poll.options.length > 10) {
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 (!poll.roster) {
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(poll.deadline);
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
- return this.createAsset({ poll }, options);
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 asset = await this.resolveAsset(id);
1907
- // TEMP during did:cid, return old version poll
1908
- const castOldAsset = asset;
1909
- if (castOldAsset.options) {
1910
- return castOldAsset;
1943
+ const isVault = await this.testVault(id);
1944
+ if (!isVault) {
1945
+ return null;
1911
1946
  }
1912
- const castAsset = asset;
1913
- if (!castAsset.poll) {
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 poll = await this.getPoll(id);
1921
- return poll !== null;
1961
+ const config = await this.getPoll(id);
1962
+ return config !== null;
1922
1963
  }
1923
- catch (error) {
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 poll = await this.getPoll(pollId);
1941
- if (!poll) {
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
- if (poll.ballots) {
1946
- hasVoted = !!poll.ballots[id.did];
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: poll.description,
1953
- options: poll.options,
1954
- deadline: poll.deadline,
1955
- isOwner: (doc.didDocument?.controller === id.did),
1956
- isEligible: isEligible,
1957
- voteExpired: voteExpired,
1958
- hasVoted: 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 (id.did === doc.didDocument?.controller) {
1961
- let voted = 0;
1962
- const results = {
1963
- tally: [],
1964
- ballots: [],
1965
- };
1966
- results.tally.push({
1967
- vote: 0,
1968
- option: 'spoil',
1969
- count: 0,
1970
- });
1971
- for (let i = 0; i < poll.options.length; i++) {
1972
- results.tally.push({
1973
- vote: i + 1,
1974
- option: poll.options[i],
1975
- count: 0,
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
- for (let voter in poll.ballots) {
1979
- const ballot = poll.ballots[voter];
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
- return view;
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 poll = await this.getPoll(pollId);
2011
- if (!poll) {
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 mising from poll');
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 (!eligible) {
2021
- throw new InvalidParameterError('voter not in roster');
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
- let ballot;
2027
- if (spoil) {
2028
- ballot = {
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
- else {
2034
- const max = poll.options.length;
2035
- if (!Number.isInteger(vote) || vote < 1 || vote > max) {
2036
- throw new InvalidParameterError('vote');
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
- ballot = {
2039
- poll: didPoll,
2040
- vote: vote,
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
- // Encrypt for receiver only
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 || !dataBallot.vote) {
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 poll = await this.getPoll(didPoll);
2065
- if (!poll) {
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
- const eligible = await this.testGroup(poll.roster, didVoter);
2072
- if (!eligible) {
2073
- throw new InvalidParameterError('voter not in roster');
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(poll.deadline).getTime();
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 = poll.options.length;
2254
+ const max = config.options.length;
2080
2255
  const vote = dataBallot.vote;
2081
- if (!vote || vote < 0 || vote > max) {
2256
+ if (vote < 0 || vote > max) {
2082
2257
  throw new InvalidParameterError('ballot.vote');
2083
2258
  }
2084
- if (!poll.ballots) {
2085
- poll.ballots = {};
2086
- }
2087
- poll.ballots[didVoter] = {
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 view = await this.viewPoll(pollId);
2102
- if (!view.results?.final) {
2103
- throw new InvalidParameterError('poll not final');
2272
+ const config = await this.getPoll(pollId);
2273
+ if (!config) {
2274
+ throw new InvalidParameterError(pollId);
2104
2275
  }
2105
- if (!reveal && view.results.ballots) {
2106
- delete view.results.ballots;
2276
+ const results = await this.computePollResults(pollId, config);
2277
+ if (!results.final) {
2278
+ throw new InvalidParameterError('poll not final');
2107
2279
  }
2108
- const poll = await this.getPoll(pollId);
2109
- if (!poll) {
2110
- throw new InvalidParameterError(pollId);
2280
+ if (!reveal) {
2281
+ delete results.ballots;
2111
2282
  }
2112
- poll.results = view.results;
2113
- return this.mergeData(pollId, { poll });
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 poll = await this.getPoll(pollId);
2123
- if (!poll) {
2294
+ const config = await this.getPoll(pollId);
2295
+ if (!config) {
2124
2296
  throw new InvalidParameterError(pollId);
2125
2297
  }
2126
- delete poll.results;
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, vaultKeypair.privateJwk, JSON.stringify(options));
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, vaultKeypair.privateJwk, JSON.stringify({}));
2142
- const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify({}));
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.publicJwk, privateJwk, vault.members);
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.publicJwk, privateJwk, vault.items);
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, privateJwk, JSON.stringify(privateJwk));
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, privateJwk, JSON.stringify(members));
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 { privateJwk, config, members } = await this.decryptVault(vault);
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, privateJwk, JSON.stringify(members));
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 { privateJwk, items } = await this.decryptVault(vault);
2491
+ const { items } = await this.decryptVault(vault);
2318
2492
  const validName = this.validateAlias(name);
2319
- const encryptedData = this.cipher.encryptBytes(vault.publicJwk, privateJwk, buffer);
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, privateJwk, JSON.stringify(items));
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 { privateJwk, items } = await this.decryptVault(vault);
2513
+ const { items } = await this.decryptVault(vault);
2340
2514
  delete items[name];
2341
- vault.items = this.cipher.encryptMessage(vault.publicJwk, privateJwk, JSON.stringify(items));
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(vault.publicJwk, privateJwk, encryptedData);
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 fallbackName = did.slice(-32);
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(fallbackName, did);
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, privateJwk } = this.cipher.generateJwk(hdkey.privateKey);
2931
+ const { publicJwk } = this.cipher.generateJwk(hdkey.privateKey);
2748
2932
  const plaintext = JSON.stringify(rest);
2749
- const enc = this.cipher.encryptMessage(publicJwk, privateJwk, plaintext);
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 decMnemonic(stored.seed.mnemonicEnc, this.passphrase);
2939
+ mnemonic = await decryptWithPassphrase(stored.seed.mnemonicEnc, this.passphrase);
2756
2940
  }
2757
2941
  catch (error) {
2758
- // OperationError is thrown by crypto.subtle.decrypt when the passphrase is wrong
2759
- if (error?.name === 'OperationError') {
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(publicJwk, privateJwk, stored.enc);
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;