@cardanowall/crypto-core 0.2.0 → 0.3.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.
@@ -49,6 +49,15 @@ var MLKEM768X25519_PUBLIC_KEY_LENGTH = 1216;
49
49
  var MLKEM768X25519_ENC_LENGTH = 1120;
50
50
  var MLKEM768X25519_SEED_LENGTH = 32;
51
51
  var MLKEM768X25519_ESEED_LENGTH = 64;
52
+ function mlkem768x25519Keygen(seed) {
53
+ if (seed.length !== MLKEM768X25519_SEED_LENGTH) {
54
+ throw new Error(
55
+ `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${seed.length}`
56
+ );
57
+ }
58
+ const { secretKey, publicKey } = hybrid_js.XWing.keygen(seed);
59
+ return { secretSeed: secretKey, publicKey };
60
+ }
52
61
  function mlkem768x25519Encapsulate(opts) {
53
62
  if (opts.publicKey.length !== MLKEM768X25519_PUBLIC_KEY_LENGTH) {
54
63
  throw new Error(
@@ -107,14 +116,6 @@ var EciesSealedPoeError = class extends Error {
107
116
  this.code = code;
108
117
  }
109
118
  };
110
- function encodeCanonicalCbor(value) {
111
- return cbor2.encode(value, {
112
- cde: true,
113
- collapseBigInts: true,
114
- rejectDuplicateKeys: true,
115
- sortKeys: sorts.sortCoreDeterministic
116
- });
117
- }
118
119
 
119
120
  // src/sealed-poe/slots-codec.ts
120
121
  var CHUNK_MAX_BYTES = 64;
@@ -139,27 +140,151 @@ function joinKemCt(chunks) {
139
140
  }
140
141
  return out;
141
142
  }
142
- function slotsToMacCbor(slots, kem) {
143
- let value;
143
+ function canonicalizeSlots(slots, kem) {
144
144
  if (kem === "x25519") {
145
- value = slots.map((s) => ({ epk: s.epk, wrap: s.wrap }));
146
- } else {
147
- value = slots.map((s) => ({
148
- // Canonicalize the chunk boundaries before the MAC commits to them:
149
- // reassemble the logical ciphertext and re-split into canonical ≤ 64-byte
150
- // chunks. The on-wire `kem_ct` array is a transport detail (the Cardano
151
- // ledger's 64-byte metadatum cap), and a hostile or non-canonical chunking
152
- // ([1, 63, …] instead of [64, …]) reassembles to the SAME bytes — so the
153
- // MAC must be invariant to it. Committing to the verbatim wire chunks would
154
- // let an attacker re-chunk an honest envelope and break the slots_mac match
155
- // for an honest recipient. Honest (already-64B-chunked) records are
156
- // unchanged; a real byte flip still changes the reassembled bytes and is
157
- // still rejected.
158
- kem_ct: chunkKemCt(joinKemCt(s.kem_ct)),
159
- wrap: s.wrap
160
- }));
161
- }
162
- return encodeCanonicalCbor(value);
145
+ return slots.map((s) => ({ epk: s.epk, wrap: s.wrap }));
146
+ }
147
+ return slots.map((s) => ({
148
+ kem_ct: chunkKemCt(joinKemCt(s.kem_ct)),
149
+ wrap: s.wrap
150
+ }));
151
+ }
152
+ function encodeCanonicalCbor(value) {
153
+ return cbor2.encode(value, {
154
+ cde: true,
155
+ collapseBigInts: true,
156
+ rejectDuplicateKeys: true,
157
+ sortKeys: sorts.sortCoreDeterministic
158
+ });
159
+ }
160
+
161
+ // src/sealed-poe/transcript.ts
162
+ var CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX = new TextEncoder().encode(
163
+ "cardano-poe-slots-transcript-v1"
164
+ );
165
+ var CARDANO_POE_HKDF_INFO_PAYLOAD = new TextEncoder().encode(
166
+ "cardano-poe-payload-v1"
167
+ );
168
+ var CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE = new TextEncoder().encode(
169
+ "cardano-poe-payload-passphrase-v1"
170
+ );
171
+ var CARDANO_POE_XWING_KEK_SALT_PREFIX = new TextEncoder().encode(
172
+ "cardano-poe-xwing-kek-salt-v1"
173
+ );
174
+ if (CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length !== 31) {
175
+ throw new Error(
176
+ "CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX byte-length invariant violated (expected 31)"
177
+ );
178
+ }
179
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD.length !== 22) {
180
+ throw new Error("CARDANO_POE_HKDF_INFO_PAYLOAD byte-length invariant violated (expected 22)");
181
+ }
182
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE.length !== 33) {
183
+ throw new Error(
184
+ "CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE byte-length invariant violated (expected 33)"
185
+ );
186
+ }
187
+ if (CARDANO_POE_XWING_KEK_SALT_PREFIX.length !== 29) {
188
+ throw new Error("CARDANO_POE_XWING_KEK_SALT_PREFIX byte-length invariant violated (expected 29)");
189
+ }
190
+ var CARDANO_POE_PW_NORM_PROFILE = "cardano-poe-pw-norm-v1";
191
+ var MAX_SLOTS = 1024;
192
+ var MAX_DECODED_ENVELOPE_BYTES = 65536;
193
+ var MAX_SEALED_PLAINTEXT = 274877906880;
194
+ var MAX_SEALED_CIPHERTEXT = MAX_SEALED_PLAINTEXT + 16;
195
+ function assertPlaintextWithinBound(plaintextLength) {
196
+ if (plaintextLength >= MAX_SEALED_PLAINTEXT) {
197
+ throw new SealedPayloadTooLargeError(
198
+ `plaintext length ${plaintextLength} is at or above the maximum sealed payload size ${MAX_SEALED_PLAINTEXT}`
199
+ );
200
+ }
201
+ }
202
+ function assertCiphertextWithinBound(ciphertextLength) {
203
+ if (ciphertextLength >= MAX_SEALED_CIPHERTEXT) {
204
+ throw new SealedPayloadTooLargeError(
205
+ `ciphertext length ${ciphertextLength} is at or above the maximum sealed ciphertext size ${MAX_SEALED_CIPHERTEXT}`
206
+ );
207
+ }
208
+ }
209
+ var SealedPayloadTooLargeError = class extends Error {
210
+ constructor(message) {
211
+ super(message);
212
+ this.name = "SealedPayloadTooLargeError";
213
+ }
214
+ };
215
+ function computeSlotsHash(args) {
216
+ const transcript = {
217
+ scheme: 1,
218
+ path: "slots",
219
+ aead: "xchacha20-poly1305",
220
+ kem: args.kem,
221
+ nonce: args.nonce,
222
+ slots: canonicalizeSlots(args.slots, args.kem)
223
+ };
224
+ const encoded = encodeCanonicalCbor(transcript);
225
+ const message = new Uint8Array(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length + encoded.length);
226
+ message.set(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX, 0);
227
+ message.set(encoded, CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length);
228
+ return sha2_js.sha256(message);
229
+ }
230
+ function adContentSlots(args) {
231
+ const ad = {
232
+ scheme: 1,
233
+ path: "slots",
234
+ aead: "xchacha20-poly1305",
235
+ kem: args.kem,
236
+ nonce: args.nonce,
237
+ slots_hash: args.slotsHash,
238
+ slots_mac: args.slotsMac
239
+ };
240
+ return encodeCanonicalCbor(ad);
241
+ }
242
+ function adContentPassphrase(args) {
243
+ const ad = {
244
+ scheme: 1,
245
+ path: "passphrase",
246
+ aead: "xchacha20-poly1305",
247
+ nonce: args.nonce,
248
+ passphrase: {
249
+ alg: args.passphrase.alg,
250
+ salt: args.passphrase.salt,
251
+ params: {
252
+ m: args.passphrase.params.m,
253
+ t: args.passphrase.params.t,
254
+ p: args.passphrase.params.p
255
+ },
256
+ normalization: CARDANO_POE_PW_NORM_PROFILE
257
+ }
258
+ };
259
+ return encodeCanonicalCbor(ad);
260
+ }
261
+ function slotsPayloadKey(args) {
262
+ return hkdfSha256({
263
+ ikm: args.cek,
264
+ salt: args.nonce,
265
+ info: CARDANO_POE_HKDF_INFO_PAYLOAD,
266
+ length: 32
267
+ });
268
+ }
269
+ function passphrasePayloadKey(args) {
270
+ return hkdfSha256({
271
+ ikm: args.cek,
272
+ salt: args.nonce,
273
+ info: CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE,
274
+ length: 32
275
+ });
276
+ }
277
+ function xwingKekSalt(args) {
278
+ const message = new Uint8Array(
279
+ CARDANO_POE_XWING_KEK_SALT_PREFIX.length + args.kemCt.length + args.pubR.length
280
+ );
281
+ let offset = 0;
282
+ message.set(CARDANO_POE_XWING_KEK_SALT_PREFIX, offset);
283
+ offset += CARDANO_POE_XWING_KEK_SALT_PREFIX.length;
284
+ message.set(args.kemCt, offset);
285
+ offset += args.kemCt.length;
286
+ message.set(args.pubR, offset);
287
+ return sha2_js.sha256(message);
163
288
  }
164
289
 
165
290
  // src/sealed-poe/wrap.ts
@@ -253,7 +378,7 @@ function wrapSlotMlkem768X25519(args) {
253
378
  }
254
379
  const kek = hkdfSha256({
255
380
  ikm: ss,
256
- salt: EMPTY_SALT,
381
+ salt: xwingKekSalt({ kemCt: enc, pubR: args.pubR }),
257
382
  info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
258
383
  length: 32
259
384
  });
@@ -272,6 +397,7 @@ function eciesSealedPoeWrap(args) {
272
397
  const { plaintext, recipientPublicKeys } = args;
273
398
  const kem = args.kem ?? "x25519";
274
399
  const n = recipientPublicKeys.length;
400
+ assertPlaintextWithinBound(plaintext.length);
275
401
  if (n < 1) {
276
402
  throw new EciesSealedPoeError(
277
403
  "ENC_SLOTS_EMPTY",
@@ -341,6 +467,7 @@ function eciesSealedPoeWrap(args) {
341
467
  );
342
468
  }
343
469
  let envelope;
470
+ let slotsHash;
344
471
  if (kem === "x25519") {
345
472
  const slots = [];
346
473
  for (let i = 0; i < n; i++) {
@@ -356,14 +483,14 @@ function eciesSealedPoeWrap(args) {
356
483
  if (args.skipShuffle !== true) {
357
484
  csprngShuffle(slots);
358
485
  }
359
- const slotsMac = computeSlotsMac(cek, slots, "x25519");
486
+ slotsHash = computeSlotsHash({ kem: "x25519", nonce, slots });
360
487
  envelope = {
361
488
  scheme: 1,
362
489
  aead: "xchacha20-poly1305",
363
490
  kem: "x25519",
364
491
  nonce,
365
492
  slots,
366
- slots_mac: slotsMac
493
+ slots_mac: computeSlotsMac(cek, slotsHash)
367
494
  };
368
495
  } else {
369
496
  const slots = [];
@@ -379,34 +506,39 @@ function eciesSealedPoeWrap(args) {
379
506
  if (args.skipShuffle !== true) {
380
507
  csprngShuffle(slots);
381
508
  }
382
- const slotsMac = computeSlotsMac(cek, slots, "mlkem768x25519");
509
+ slotsHash = computeSlotsHash({ kem: "mlkem768x25519", nonce, slots });
383
510
  envelope = {
384
511
  scheme: 1,
385
512
  aead: "xchacha20-poly1305",
386
513
  kem: "mlkem768x25519",
387
514
  nonce,
388
515
  slots,
389
- slots_mac: slotsMac
516
+ slots_mac: computeSlotsMac(cek, slotsHash)
390
517
  };
391
518
  }
392
- const adContent = concat(nonce, envelope.slots_mac);
519
+ const payloadKey = slotsPayloadKey({ cek, nonce });
520
+ const adContent = adContentSlots({
521
+ kem: envelope.kem,
522
+ nonce,
523
+ slotsHash,
524
+ slotsMac: envelope.slots_mac
525
+ });
393
526
  const ciphertext = xchacha20Poly1305Encrypt({
394
- key: cek,
527
+ key: payloadKey,
395
528
  nonce,
396
529
  aad: adContent,
397
530
  plaintext
398
531
  });
399
532
  return { envelope, ciphertext };
400
533
  }
401
- function computeSlotsMac(cek, slots, kem) {
534
+ function computeSlotsMac(cek, slotsHash) {
402
535
  const hmacKey = hkdfSha256({
403
536
  ikm: cek,
404
537
  salt: EMPTY_SALT,
405
538
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
406
539
  length: 32
407
540
  });
408
- const slotsCbor = slotsToMacCbor(slots, kem);
409
- const slotsMac = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
541
+ const slotsMac = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsHash);
410
542
  if (slotsMac.length !== SLOTS_MAC_LENGTH) {
411
543
  throw new Error(`internal: slots_mac.length=${slotsMac.length}, expected ${SLOTS_MAC_LENGTH}`);
412
544
  }
@@ -438,6 +570,13 @@ function concat2(a, b) {
438
570
  out.set(b, a.length);
439
571
  return out;
440
572
  }
573
+ function bytesKey(bytes) {
574
+ let s = "";
575
+ for (let i = 0; i < bytes.length; i++) {
576
+ s += String.fromCharCode(bytes[i]);
577
+ }
578
+ return s;
579
+ }
441
580
  function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
442
581
  if (envelope.scheme !== 1) {
443
582
  throw new EciesSealedPoeError(
@@ -461,6 +600,12 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
461
600
  if (n < 1) {
462
601
  throw new EciesSealedPoeError("ENC_SLOTS_EMPTY", `envelope.slots.length=${n} must be >= 1`);
463
602
  }
603
+ if (n > MAX_SLOTS) {
604
+ throw new EciesSealedPoeError(
605
+ "ENC_SLOTS_TOO_MANY",
606
+ `envelope.slots.length=${n} exceeds MAX_SLOTS=${MAX_SLOTS}`
607
+ );
608
+ }
464
609
  if (envelope.nonce.length !== NONCE_LENGTH2) {
465
610
  throw new EciesSealedPoeError(
466
611
  "NONCE_LENGTH_MISMATCH",
@@ -473,6 +618,7 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
473
618
  `envelope.slots_mac MUST be exactly ${SLOTS_MAC_LENGTH2} bytes, got ${envelope.slots_mac.length}`
474
619
  );
475
620
  }
621
+ const seenKemMaterial = /* @__PURE__ */ new Set();
476
622
  if (envelope.kem === "x25519") {
477
623
  for (let i = 0; i < n; i++) {
478
624
  const slot = envelope.slots[i];
@@ -488,6 +634,14 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
488
634
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
489
635
  );
490
636
  }
637
+ const key = bytesKey(slot.epk);
638
+ if (seenKemMaterial.has(key)) {
639
+ throw new EciesSealedPoeError(
640
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
641
+ `envelope.slots[${i}].epk duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
642
+ );
643
+ }
644
+ seenKemMaterial.add(key);
491
645
  }
492
646
  } else {
493
647
  for (let i = 0; i < n; i++) {
@@ -505,8 +659,24 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
505
659
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
506
660
  );
507
661
  }
662
+ const key = bytesKey(enc);
663
+ if (seenKemMaterial.has(key)) {
664
+ throw new EciesSealedPoeError(
665
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
666
+ `envelope.slots[${i}].kem_ct duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
667
+ );
668
+ }
669
+ seenKemMaterial.add(key);
508
670
  }
509
671
  }
672
+ const perSlotBytes = envelope.kem === "x25519" ? X25519_PUBLIC_KEY_LENGTH2 + WRAP_LENGTH2 : MLKEM768X25519_ENC_LENGTH + WRAP_LENGTH2;
673
+ const decodedEnvelopeBytes = NONCE_LENGTH2 + SLOTS_MAC_LENGTH2 + n * perSlotBytes;
674
+ if (decodedEnvelopeBytes > MAX_DECODED_ENVELOPE_BYTES) {
675
+ throw new EciesSealedPoeError(
676
+ "ENC_ENVELOPE_TOO_LARGE",
677
+ `decoded envelope size ${decodedEnvelopeBytes} exceeds MAX_DECODED_ENVELOPE_BYTES=${MAX_DECODED_ENVELOPE_BYTES}`
678
+ );
679
+ }
510
680
  if (multiPrivKeys !== void 0) {
511
681
  for (let i = 0; i < multiPrivKeys.length; i++) {
512
682
  if (multiPrivKeys[i].length !== X25519_SECRET_KEY_LENGTH2) {
@@ -525,60 +695,42 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
525
695
  }
526
696
  }
527
697
  }
698
+ var ZERO_IKM_32 = new Uint8Array(32);
528
699
  function tryX25519Slot(args) {
529
- if (args.liveSlot) {
530
- try {
531
- const shared = x25519Ecdh({
532
- secretKey: args.recipientSecretKey,
533
- theirPublicKey: args.slot.epk
534
- });
535
- const kek = hkdfSha256({
536
- ikm: shared,
537
- salt: concat2(args.slot.epk, args.pubRLocal),
538
- info: CARDANO_POE_HKDF_INFO_KEK,
539
- length: 32
540
- });
541
- return chacha20Poly1305Decrypt({
542
- key: kek,
543
- nonce: ZERO_NONCE_122,
544
- aad: CARDANO_POE_HKDF_INFO_KEK,
545
- ciphertext: args.slot.wrap
546
- });
547
- } catch (e) {
548
- if (!(e instanceof AeadVerificationError) && !(e instanceof X25519LowOrderPointError)) {
549
- throw e;
550
- }
551
- return null;
552
- }
553
- }
700
+ const salt = concat2(args.slot.epk, args.pubRLocal);
701
+ let shared;
554
702
  try {
555
- const shared = x25519Ecdh({
703
+ shared = x25519Ecdh({
556
704
  secretKey: args.recipientSecretKey,
557
705
  theirPublicKey: args.slot.epk
558
706
  });
559
- hkdfSha256({
560
- ikm: shared,
561
- salt: concat2(args.slot.epk, args.pubRLocal),
562
- info: CARDANO_POE_HKDF_INFO_KEK,
563
- length: 32
564
- });
565
707
  } catch (e) {
566
708
  if (!(e instanceof X25519LowOrderPointError)) throw e;
709
+ hkdfSha256({ ikm: ZERO_IKM_32, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
710
+ return null;
711
+ }
712
+ const kek = hkdfSha256({ ikm: shared, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
713
+ try {
714
+ return chacha20Poly1305Decrypt({
715
+ key: kek,
716
+ nonce: ZERO_NONCE_122,
717
+ aad: CARDANO_POE_HKDF_INFO_KEK,
718
+ ciphertext: args.slot.wrap
719
+ });
720
+ } catch (e) {
721
+ if (!(e instanceof AeadVerificationError)) throw e;
722
+ return null;
567
723
  }
568
- return null;
569
724
  }
570
725
  function tryMlkem768X25519Slot(args) {
571
726
  const enc = joinKemCt(args.slot.kem_ct);
572
727
  const ss = mlkem768x25519Decapsulate({ secretSeed: args.recipientSecretKey, enc });
573
728
  const kek = hkdfSha256({
574
729
  ikm: ss,
575
- salt: EMPTY_SALT2,
730
+ salt: xwingKekSalt({ kemCt: enc, pubR: args.pubR }),
576
731
  info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
577
732
  length: 32
578
733
  });
579
- if (!args.liveSlot) {
580
- return null;
581
- }
582
734
  try {
583
735
  return chacha20Poly1305Decrypt({
584
736
  key: kek,
@@ -595,51 +747,43 @@ function tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN,
595
747
  const n = envelope.slots.length;
596
748
  let cek = null;
597
749
  let matchedSlotIdx = -1;
750
+ let cekConflict = false;
751
+ const recordMatch = (candidate, i) => {
752
+ if (candidate === null) return;
753
+ if (cek === null) {
754
+ cek = candidate;
755
+ matchedSlotIdx = i;
756
+ } else if (!compareCt(candidate, cek)) {
757
+ cekConflict = true;
758
+ }
759
+ };
598
760
  if (envelope.kem === "x25519") {
599
761
  const pubRLocal = x25519PublicKey({ secretKey: recipientSecretKey });
600
762
  for (let i = 0; i < n; i++) {
601
763
  if (slotsAttemptedOut !== void 0) {
602
764
  slotsAttemptedOut.count = i + 1;
603
765
  }
604
- const candidate = tryX25519Slot({
605
- slot: envelope.slots[i],
606
- recipientSecretKey,
607
- pubRLocal,
608
- liveSlot: cek === null
609
- });
610
- if (cek === null && candidate !== null) {
611
- cek = candidate;
612
- matchedSlotIdx = i;
613
- }
766
+ recordMatch(tryX25519Slot({ slot: envelope.slots[i], recipientSecretKey, pubRLocal }), i);
614
767
  if (cek !== null && !constantTimeN) break;
615
768
  }
616
769
  } else {
770
+ const pubR = mlkem768x25519Keygen(recipientSecretKey).publicKey;
617
771
  for (let i = 0; i < n; i++) {
618
772
  if (slotsAttemptedOut !== void 0) {
619
773
  slotsAttemptedOut.count = i + 1;
620
774
  }
621
- const candidate = tryMlkem768X25519Slot({
622
- slot: envelope.slots[i],
623
- recipientSecretKey,
624
- liveSlot: cek === null
625
- });
626
- if (cek === null && candidate !== null) {
627
- cek = candidate;
628
- matchedSlotIdx = i;
629
- }
775
+ recordMatch(tryMlkem768X25519Slot({ slot: envelope.slots[i], recipientSecretKey, pubR }), i);
630
776
  if (cek !== null && !constantTimeN) break;
631
777
  }
632
778
  }
633
- return cek === null ? null : { cek, slotIdx: matchedSlotIdx };
634
- }
635
- function tryRecipientUnwrap(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut) {
636
- return tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut)?.cek ?? null;
779
+ return cek === null ? null : { cek, slotIdx: matchedSlotIdx, cekConflict };
637
780
  }
638
- function slotsMacCborBytes(envelope) {
639
- return slotsToMacCbor(
640
- envelope.slots,
641
- envelope.kem
642
- );
781
+ function slotsHashBytes(envelope) {
782
+ return computeSlotsHash({
783
+ kem: envelope.kem,
784
+ nonce: envelope.nonce,
785
+ slots: envelope.slots
786
+ });
643
787
  }
644
788
  function eciesSealedPoeUnwrap(args) {
645
789
  const { envelope, ciphertext } = args;
@@ -668,34 +812,38 @@ function eciesSealedPoeUnwrap(args) {
668
812
  } else {
669
813
  assertEnvelopeStructure(envelope, void 0, args.recipientSecretKey);
670
814
  }
815
+ assertCiphertextWithinBound(ciphertext.length);
816
+ const slotsHash = slotsHashBytes(envelope);
671
817
  let matchedCek = null;
672
818
  let anyCandidateRecovered = false;
673
819
  if (hasSingle) {
674
820
  const recipientSecretKey = args.recipientSecretKey;
675
- const cek = tryRecipientUnwrap(
821
+ const candidate = tryRecipientUnwrapWithIdx(
676
822
  envelope,
677
823
  recipientSecretKey,
678
824
  constantTimeN,
679
825
  args._slotsAttemptedOut
680
826
  );
681
- if (cek === null) {
827
+ if (candidate === null) {
682
828
  return { matched: false, reason: "WRONG_RECIPIENT_KEY" };
683
829
  }
684
- const slotsCbor = slotsMacCborBytes(envelope);
830
+ if (candidate.cekConflict) {
831
+ return { matched: false, reason: "TAMPERED_HEADER" };
832
+ }
685
833
  const hmacKey = hkdfSha256({
686
- ikm: cek,
834
+ ikm: candidate.cek,
687
835
  salt: EMPTY_SALT2,
688
836
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
689
837
  length: 32
690
838
  });
691
- const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
839
+ const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsHash);
692
840
  if (!compareCt(slotsMacCalc, envelope.slots_mac)) {
693
841
  return { matched: false, reason: "TAMPERED_HEADER" };
694
842
  }
695
- matchedCek = cek;
843
+ matchedCek = candidate.cek;
696
844
  } else {
697
- const slotsCbor = slotsMacCborBytes(envelope);
698
845
  const recipientSecretKeys = multiPrivKeys;
846
+ let cekConflict = false;
699
847
  for (let k = 0; k < recipientSecretKeys.length; k++) {
700
848
  if (args._privsAttemptedOut !== void 0) {
701
849
  args._privsAttemptedOut.count = k + 1;
@@ -703,7 +851,7 @@ function eciesSealedPoeUnwrap(args) {
703
851
  if (args._slotsAttemptedOut !== void 0) {
704
852
  args._slotsAttemptedOut.count = 0;
705
853
  }
706
- const cek = tryRecipientUnwrap(
854
+ const candidate = tryRecipientUnwrapWithIdx(
707
855
  envelope,
708
856
  recipientSecretKeys[k],
709
857
  constantTimeN,
@@ -712,20 +860,25 @@ function eciesSealedPoeUnwrap(args) {
712
860
  if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
713
861
  args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
714
862
  }
715
- if (cek === null) continue;
863
+ if (candidate === null) continue;
864
+ if (candidate.cekConflict) cekConflict = true;
865
+ const cek = candidate.cek;
716
866
  const hmacKey = hkdfSha256({
717
867
  ikm: cek,
718
868
  salt: EMPTY_SALT2,
719
869
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
720
870
  length: 32
721
871
  });
722
- const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
872
+ const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsHash);
723
873
  if (compareCt(slotsMacCalc, envelope.slots_mac)) {
724
874
  matchedCek = cek;
725
875
  break;
726
876
  }
727
877
  anyCandidateRecovered = true;
728
878
  }
879
+ if (matchedCek !== null && cekConflict) {
880
+ return { matched: false, reason: "TAMPERED_HEADER" };
881
+ }
729
882
  if (matchedCek === null) {
730
883
  return {
731
884
  matched: false,
@@ -733,10 +886,16 @@ function eciesSealedPoeUnwrap(args) {
733
886
  };
734
887
  }
735
888
  }
736
- const adContent = concat2(envelope.nonce, envelope.slots_mac);
889
+ const payloadKey = slotsPayloadKey({ cek: matchedCek, nonce: envelope.nonce });
890
+ const adContent = adContentSlots({
891
+ kem: envelope.kem,
892
+ nonce: envelope.nonce,
893
+ slotsHash,
894
+ slotsMac: envelope.slots_mac
895
+ });
737
896
  try {
738
897
  const plaintext = xchacha20Poly1305Decrypt({
739
- key: matchedCek,
898
+ key: payloadKey,
740
899
  nonce: envelope.nonce,
741
900
  aad: adContent,
742
901
  ciphertext
@@ -762,7 +921,7 @@ function eciesSealedPoeTrialDecrypt(args) {
762
921
  );
763
922
  }
764
923
  assertEnvelopeStructure(envelope, recipientSecretKeys, void 0);
765
- const slotsCbor = slotsMacCborBytes(envelope);
924
+ const slotsHash = slotsHashBytes(envelope);
766
925
  let anyCandidateRecovered = false;
767
926
  for (let k = 0; k < recipientSecretKeys.length; k++) {
768
927
  if (args._privsAttemptedOut !== void 0) {
@@ -781,13 +940,17 @@ function eciesSealedPoeTrialDecrypt(args) {
781
940
  args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
782
941
  }
783
942
  if (candidate === null) continue;
943
+ if (candidate.cekConflict) {
944
+ anyCandidateRecovered = true;
945
+ continue;
946
+ }
784
947
  const hmacKey = hkdfSha256({
785
948
  ikm: candidate.cek,
786
949
  salt: EMPTY_SALT2,
787
950
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
788
951
  length: 32
789
952
  });
790
- const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
953
+ const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsHash);
791
954
  if (compareCt(slotsMacCalc, envelope.slots_mac)) {
792
955
  return { kind: "match", slotIdx: candidate.slotIdx, cek: candidate.cek };
793
956
  }
@@ -837,15 +1000,33 @@ function sealedEnvelopeFromParsed(enc) {
837
1000
 
838
1001
  exports.CARDANO_POE_HKDF_INFO_KEK = CARDANO_POE_HKDF_INFO_KEK;
839
1002
  exports.CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 = CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519;
1003
+ exports.CARDANO_POE_HKDF_INFO_PAYLOAD = CARDANO_POE_HKDF_INFO_PAYLOAD;
1004
+ exports.CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE = CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE;
840
1005
  exports.CARDANO_POE_HKDF_INFO_SLOTS_MAC = CARDANO_POE_HKDF_INFO_SLOTS_MAC;
1006
+ exports.CARDANO_POE_PW_NORM_PROFILE = CARDANO_POE_PW_NORM_PROFILE;
1007
+ exports.CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX = CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX;
1008
+ exports.CARDANO_POE_XWING_KEK_SALT_PREFIX = CARDANO_POE_XWING_KEK_SALT_PREFIX;
841
1009
  exports.EciesSealedPoeError = EciesSealedPoeError;
1010
+ exports.MAX_DECODED_ENVELOPE_BYTES = MAX_DECODED_ENVELOPE_BYTES;
1011
+ exports.MAX_SEALED_CIPHERTEXT = MAX_SEALED_CIPHERTEXT;
1012
+ exports.MAX_SEALED_PLAINTEXT = MAX_SEALED_PLAINTEXT;
1013
+ exports.MAX_SLOTS = MAX_SLOTS;
1014
+ exports.SealedPayloadTooLargeError = SealedPayloadTooLargeError;
1015
+ exports.adContentPassphrase = adContentPassphrase;
1016
+ exports.adContentSlots = adContentSlots;
1017
+ exports.assertCiphertextWithinBound = assertCiphertextWithinBound;
1018
+ exports.assertPlaintextWithinBound = assertPlaintextWithinBound;
1019
+ exports.canonicalizeSlots = canonicalizeSlots;
842
1020
  exports.chunkKemCt = chunkKemCt;
1021
+ exports.computeSlotsHash = computeSlotsHash;
843
1022
  exports.eciesSealedPoeTrialDecrypt = eciesSealedPoeTrialDecrypt;
844
1023
  exports.eciesSealedPoeUnwrap = eciesSealedPoeUnwrap;
845
1024
  exports.eciesSealedPoeWrap = eciesSealedPoeWrap;
846
1025
  exports.joinKemCt = joinKemCt;
1026
+ exports.passphrasePayloadKey = passphrasePayloadKey;
847
1027
  exports.sealedEnvelopeFromParsed = sealedEnvelopeFromParsed;
848
- exports.slotsToMacCbor = slotsToMacCbor;
1028
+ exports.slotsPayloadKey = slotsPayloadKey;
849
1029
  exports.uniformIndexBelow = uniformIndexBelow;
1030
+ exports.xwingKekSalt = xwingKekSalt;
850
1031
  //# sourceMappingURL=sealed-poe.cjs.map
851
1032
  //# sourceMappingURL=sealed-poe.cjs.map