@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.
@@ -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,7 @@ 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);
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, keypair.privateJwk, msg);
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.publicJwk, keypair.privateJwk, castData.backup);
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 encMnemonic(mnemonic, this.passphrase);
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, senderKeypair.privateJwk, msg) : null;
708
- const cipher_receiver = this.cipher.encryptMessage(receivePublicJwk, senderKeypair.privateJwk, msg);
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(senderPublicJwk, receiverKeypair.privateJwk, ciphertext);
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 asset = await this.resolveAsset(did);
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
- 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;
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, keypair.privateJwk, msg);
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.publicJwk, keypair.privateJwk, backupStore.backup);
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
- const subjectDID = await this.lookupDID(subjectId);
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: subjectDID,
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: subjectDID,
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
- return this.encryptJSON(signed, credential.credentialSubject.id, { ...options, includeHash: true });
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: [vc.credentialSubject.id],
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
- 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
- };
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
- type: 'poll',
1859
- version: 1,
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(poll, options = {}) {
1867
- if (poll.type !== 'poll') {
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 (!poll.description) {
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 (!poll.options || !Array.isArray(poll.options) || poll.options.length < 2 || poll.options.length > 10) {
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 (!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
1910
+ if (!config.deadline) {
1894
1911
  throw new InvalidParameterError('poll.deadline');
1895
1912
  }
1896
- const deadline = new Date(poll.deadline);
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
- return this.createAsset({ poll }, options);
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 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;
1926
+ const isVault = await this.testVault(id);
1927
+ if (!isVault) {
1928
+ return null;
1911
1929
  }
1912
- const castAsset = asset;
1913
- if (!castAsset.poll) {
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 poll = await this.getPoll(id);
1921
- return poll !== null;
1944
+ const config = await this.getPoll(id);
1945
+ return config !== null;
1922
1946
  }
1923
- catch (error) {
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 poll = await this.getPoll(pollId);
1941
- if (!poll) {
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
- if (poll.ballots) {
1946
- hasVoted = !!poll.ballots[id.did];
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: 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,
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 (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,
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
- 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;
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
- return view;
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 poll = await this.getPoll(pollId);
2011
- if (!poll) {
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 mising from poll');
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 (!eligible) {
2021
- throw new InvalidParameterError('voter not in roster');
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
- let ballot;
2027
- if (spoil) {
2028
- ballot = {
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
- else {
2034
- const max = poll.options.length;
2035
- if (!Number.isInteger(vote) || vote < 1 || vote > max) {
2036
- throw new InvalidParameterError('vote');
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
- ballot = {
2039
- poll: didPoll,
2040
- vote: vote,
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
- // Encrypt for receiver only
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 || !dataBallot.vote) {
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 poll = await this.getPoll(didPoll);
2065
- if (!poll) {
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
- const eligible = await this.testGroup(poll.roster, didVoter);
2072
- if (!eligible) {
2073
- throw new InvalidParameterError('voter not in roster');
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(poll.deadline).getTime();
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 = poll.options.length;
2237
+ const max = config.options.length;
2080
2238
  const vote = dataBallot.vote;
2081
- if (!vote || vote < 0 || vote > max) {
2239
+ if (vote < 0 || vote > max) {
2082
2240
  throw new InvalidParameterError('ballot.vote');
2083
2241
  }
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 });
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 view = await this.viewPoll(pollId);
2102
- if (!view.results?.final) {
2103
- throw new InvalidParameterError('poll not final');
2255
+ const config = await this.getPoll(pollId);
2256
+ if (!config) {
2257
+ throw new InvalidParameterError(pollId);
2104
2258
  }
2105
- if (!reveal && view.results.ballots) {
2106
- delete view.results.ballots;
2259
+ const results = await this.computePollResults(pollId, config);
2260
+ if (!results.final) {
2261
+ throw new InvalidParameterError('poll not final');
2107
2262
  }
2108
- const poll = await this.getPoll(pollId);
2109
- if (!poll) {
2110
- throw new InvalidParameterError(pollId);
2263
+ if (!reveal) {
2264
+ delete results.ballots;
2111
2265
  }
2112
- poll.results = view.results;
2113
- return this.mergeData(pollId, { poll });
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 poll = await this.getPoll(pollId);
2123
- if (!poll) {
2277
+ const config = await this.getPoll(pollId);
2278
+ if (!config) {
2124
2279
  throw new InvalidParameterError(pollId);
2125
2280
  }
2126
- delete poll.results;
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, vaultKeypair.privateJwk, JSON.stringify(options));
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, vaultKeypair.privateJwk, JSON.stringify({}));
2142
- const items = this.cipher.encryptMessage(vaultKeypair.publicJwk, vaultKeypair.privateJwk, JSON.stringify({}));
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.publicJwk, privateJwk, vault.members);
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.publicJwk, privateJwk, vault.items);
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, privateJwk, JSON.stringify(privateJwk));
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, privateJwk, JSON.stringify(members));
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 { privateJwk, config, members } = await this.decryptVault(vault);
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, privateJwk, JSON.stringify(members));
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 { privateJwk, items } = await this.decryptVault(vault);
2474
+ const { items } = await this.decryptVault(vault);
2318
2475
  const validName = this.validateAlias(name);
2319
- const encryptedData = this.cipher.encryptBytes(vault.publicJwk, privateJwk, buffer);
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, privateJwk, JSON.stringify(items));
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 { privateJwk, items } = await this.decryptVault(vault);
2496
+ const { items } = await this.decryptVault(vault);
2340
2497
  delete items[name];
2341
- vault.items = this.cipher.encryptMessage(vault.publicJwk, privateJwk, JSON.stringify(items));
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(vault.publicJwk, privateJwk, encryptedData);
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 fallbackName = did.slice(-32);
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(fallbackName, did);
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, privateJwk } = this.cipher.generateJwk(hdkey.privateKey);
2914
+ const { publicJwk } = this.cipher.generateJwk(hdkey.privateKey);
2748
2915
  const plaintext = JSON.stringify(rest);
2749
- const enc = this.cipher.encryptMessage(publicJwk, privateJwk, plaintext);
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 decMnemonic(stored.seed.mnemonicEnc, this.passphrase);
2922
+ mnemonic = await decryptWithPassphrase(stored.seed.mnemonicEnc, this.passphrase);
2756
2923
  }
2757
2924
  catch (error) {
2758
- // OperationError is thrown by crypto.subtle.decrypt when the passphrase is wrong
2759
- if (error?.name === 'OperationError') {
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(publicJwk, privateJwk, stored.enc);
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;