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