@cardanowall/sdk-ts 0.1.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.
Files changed (38) hide show
  1. package/README.md +14 -14
  2. package/dist/client/index.cjs +1621 -1538
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +52 -52
  5. package/dist/client/index.d.ts +52 -52
  6. package/dist/client/index.js +1620 -1537
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/conformance/cli.cjs +2367 -2106
  9. package/dist/conformance/cli.cjs.map +1 -1
  10. package/dist/conformance/cli.js +2367 -2106
  11. package/dist/conformance/cli.js.map +1 -1
  12. package/dist/identity/index.cjs +219 -104
  13. package/dist/identity/index.cjs.map +1 -1
  14. package/dist/identity/index.d.cts +1 -1
  15. package/dist/identity/index.d.ts +1 -1
  16. package/dist/identity/index.js +219 -104
  17. package/dist/identity/index.js.map +1 -1
  18. package/dist/ids/index.cjs.map +1 -1
  19. package/dist/ids/index.js.map +1 -1
  20. package/dist/index.cjs +2808 -2530
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +3 -3
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +2805 -2527
  25. package/dist/index.js.map +1 -1
  26. package/dist/merkle/index.cjs +1 -1
  27. package/dist/merkle/index.cjs.map +1 -1
  28. package/dist/merkle/index.js +1 -1
  29. package/dist/merkle/index.js.map +1 -1
  30. package/dist/{types-BQMtbRCb.d.cts → types-DGsZTMuZ.d.cts} +6 -6
  31. package/dist/{types-BQMtbRCb.d.ts → types-DGsZTMuZ.d.ts} +6 -6
  32. package/dist/verifier/index.cjs +2368 -2107
  33. package/dist/verifier/index.cjs.map +1 -1
  34. package/dist/verifier/index.d.cts +3 -3
  35. package/dist/verifier/index.d.ts +3 -3
  36. package/dist/verifier/index.js +2369 -2108
  37. package/dist/verifier/index.js.map +1 -1
  38. package/package.json +8 -8
@@ -1,6 +1,6 @@
1
1
  import { DerivedEd25519KeyPair, DerivedX25519KeyPair, DerivedMlKem768X25519KeyPair } from '@cardanowall/crypto-core/seed-derive';
2
2
  import { SealedEnvelope, UnwrapResult, RecipientKeyBundle } from '@cardanowall/crypto-core/sealed-poe';
3
- import { S as Signer } from '../types-BQMtbRCb.cjs';
3
+ import { S as Signer } from '../types-DGsZTMuZ.cjs';
4
4
 
5
5
  interface SeedKeys {
6
6
  readonly ed25519: DerivedEd25519KeyPair;
@@ -1,6 +1,6 @@
1
1
  import { DerivedEd25519KeyPair, DerivedX25519KeyPair, DerivedMlKem768X25519KeyPair } from '@cardanowall/crypto-core/seed-derive';
2
2
  import { SealedEnvelope, UnwrapResult, RecipientKeyBundle } from '@cardanowall/crypto-core/sealed-poe';
3
- import { S as Signer } from '../types-BQMtbRCb.js';
3
+ import { S as Signer } from '../types-DGsZTMuZ.js';
4
4
 
5
5
  interface SeedKeys {
6
6
  readonly ed25519: DerivedEd25519KeyPair;
@@ -857,6 +857,15 @@ function hkdfSha2562(opts2) {
857
857
  }
858
858
  var MLKEM768X25519_ENC_LENGTH = 1120;
859
859
  var MLKEM768X25519_SEED_LENGTH2 = 32;
860
+ function mlkem768x25519Keygen2(seed) {
861
+ if (seed.length !== MLKEM768X25519_SEED_LENGTH2) {
862
+ throw new Error(
863
+ `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH2} bytes, got ${seed.length}`
864
+ );
865
+ }
866
+ const { secretKey, publicKey } = XWing.keygen(seed);
867
+ return { secretSeed: secretKey, publicKey };
868
+ }
860
869
  function mlkem768x25519Decapsulate(opts2) {
861
870
  if (opts2.secretSeed.length !== MLKEM768X25519_SEED_LENGTH2) {
862
871
  throw new Error(
@@ -899,14 +908,6 @@ var EciesSealedPoeError = class extends Error {
899
908
  this.code = code;
900
909
  }
901
910
  };
902
- function encodeCanonicalCbor(value) {
903
- return encode(value, {
904
- cde: true,
905
- collapseBigInts: true,
906
- rejectDuplicateKeys: true,
907
- sortKeys: sortCoreDeterministic
908
- });
909
- }
910
911
  var CHUNK_MAX_BYTES = 64;
911
912
  function chunkKemCt(value) {
912
913
  if (value.length === 0) {
@@ -929,27 +930,114 @@ function joinKemCt(chunks) {
929
930
  }
930
931
  return out;
931
932
  }
932
- function slotsToMacCbor(slots, kem) {
933
- let value;
933
+ function canonicalizeSlots(slots, kem) {
934
934
  if (kem === "x25519") {
935
- value = slots.map((s) => ({ epk: s.epk, wrap: s.wrap }));
936
- } else {
937
- value = slots.map((s) => ({
938
- // Canonicalize the chunk boundaries before the MAC commits to them:
939
- // reassemble the logical ciphertext and re-split into canonical ≤ 64-byte
940
- // chunks. The on-wire `kem_ct` array is a transport detail (the Cardano
941
- // ledger's 64-byte metadatum cap), and a hostile or non-canonical chunking
942
- // ([1, 63, …] instead of [64, …]) reassembles to the SAME bytes — so the
943
- // MAC must be invariant to it. Committing to the verbatim wire chunks would
944
- // let an attacker re-chunk an honest envelope and break the slots_mac match
945
- // for an honest recipient. Honest (already-64B-chunked) records are
946
- // unchanged; a real byte flip still changes the reassembled bytes and is
947
- // still rejected.
948
- kem_ct: chunkKemCt(joinKemCt(s.kem_ct)),
949
- wrap: s.wrap
950
- }));
935
+ return slots.map((s) => ({ epk: s.epk, wrap: s.wrap }));
951
936
  }
952
- return encodeCanonicalCbor(value);
937
+ return slots.map((s) => ({
938
+ kem_ct: chunkKemCt(joinKemCt(s.kem_ct)),
939
+ wrap: s.wrap
940
+ }));
941
+ }
942
+ function encodeCanonicalCbor(value) {
943
+ return encode(value, {
944
+ cde: true,
945
+ collapseBigInts: true,
946
+ rejectDuplicateKeys: true,
947
+ sortKeys: sortCoreDeterministic
948
+ });
949
+ }
950
+ var CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX = new TextEncoder().encode(
951
+ "cardano-poe-slots-transcript-v1"
952
+ );
953
+ var CARDANO_POE_HKDF_INFO_PAYLOAD = new TextEncoder().encode(
954
+ "cardano-poe-payload-v1"
955
+ );
956
+ var CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE = new TextEncoder().encode(
957
+ "cardano-poe-payload-passphrase-v1"
958
+ );
959
+ var CARDANO_POE_XWING_KEK_SALT_PREFIX = new TextEncoder().encode(
960
+ "cardano-poe-xwing-kek-salt-v1"
961
+ );
962
+ if (CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length !== 31) {
963
+ throw new Error(
964
+ "CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX byte-length invariant violated (expected 31)"
965
+ );
966
+ }
967
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD.length !== 22) {
968
+ throw new Error("CARDANO_POE_HKDF_INFO_PAYLOAD byte-length invariant violated (expected 22)");
969
+ }
970
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE.length !== 33) {
971
+ throw new Error(
972
+ "CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE byte-length invariant violated (expected 33)"
973
+ );
974
+ }
975
+ if (CARDANO_POE_XWING_KEK_SALT_PREFIX.length !== 29) {
976
+ throw new Error("CARDANO_POE_XWING_KEK_SALT_PREFIX byte-length invariant violated (expected 29)");
977
+ }
978
+ var MAX_SLOTS = 1024;
979
+ var MAX_DECODED_ENVELOPE_BYTES = 65536;
980
+ var MAX_SEALED_PLAINTEXT = 274877906880;
981
+ var MAX_SEALED_CIPHERTEXT = MAX_SEALED_PLAINTEXT + 16;
982
+ function assertCiphertextWithinBound(ciphertextLength) {
983
+ if (ciphertextLength >= MAX_SEALED_CIPHERTEXT) {
984
+ throw new SealedPayloadTooLargeError(
985
+ `ciphertext length ${ciphertextLength} is at or above the maximum sealed ciphertext size ${MAX_SEALED_CIPHERTEXT}`
986
+ );
987
+ }
988
+ }
989
+ var SealedPayloadTooLargeError = class extends Error {
990
+ constructor(message) {
991
+ super(message);
992
+ this.name = "SealedPayloadTooLargeError";
993
+ }
994
+ };
995
+ function computeSlotsHash(args) {
996
+ const transcript = {
997
+ scheme: 1,
998
+ path: "slots",
999
+ aead: "xchacha20-poly1305",
1000
+ kem: args.kem,
1001
+ nonce: args.nonce,
1002
+ slots: canonicalizeSlots(args.slots, args.kem)
1003
+ };
1004
+ const encoded = encodeCanonicalCbor(transcript);
1005
+ const message = new Uint8Array(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length + encoded.length);
1006
+ message.set(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX, 0);
1007
+ message.set(encoded, CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length);
1008
+ return sha256(message);
1009
+ }
1010
+ function adContentSlots(args) {
1011
+ const ad = {
1012
+ scheme: 1,
1013
+ path: "slots",
1014
+ aead: "xchacha20-poly1305",
1015
+ kem: args.kem,
1016
+ nonce: args.nonce,
1017
+ slots_hash: args.slotsHash,
1018
+ slots_mac: args.slotsMac
1019
+ };
1020
+ return encodeCanonicalCbor(ad);
1021
+ }
1022
+ function slotsPayloadKey(args) {
1023
+ return hkdfSha2562({
1024
+ ikm: args.cek,
1025
+ salt: args.nonce,
1026
+ info: CARDANO_POE_HKDF_INFO_PAYLOAD,
1027
+ length: 32
1028
+ });
1029
+ }
1030
+ function xwingKekSalt(args) {
1031
+ const message = new Uint8Array(
1032
+ CARDANO_POE_XWING_KEK_SALT_PREFIX.length + args.kemCt.length + args.pubR.length
1033
+ );
1034
+ let offset = 0;
1035
+ message.set(CARDANO_POE_XWING_KEK_SALT_PREFIX, offset);
1036
+ offset += CARDANO_POE_XWING_KEK_SALT_PREFIX.length;
1037
+ message.set(args.kemCt, offset);
1038
+ offset += args.kemCt.length;
1039
+ message.set(args.pubR, offset);
1040
+ return sha256(message);
953
1041
  }
954
1042
  var CARDANO_POE_HKDF_INFO_KEK = new TextEncoder().encode("cardano-poe-kek-v1");
955
1043
  var CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 = new TextEncoder().encode(
@@ -995,6 +1083,13 @@ function concat2(a, b) {
995
1083
  out.set(b, a.length);
996
1084
  return out;
997
1085
  }
1086
+ function bytesKey(bytes) {
1087
+ let s = "";
1088
+ for (let i = 0; i < bytes.length; i++) {
1089
+ s += String.fromCharCode(bytes[i]);
1090
+ }
1091
+ return s;
1092
+ }
998
1093
  function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
999
1094
  if (envelope.scheme !== 1) {
1000
1095
  throw new EciesSealedPoeError(
@@ -1018,6 +1113,12 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1018
1113
  if (n < 1) {
1019
1114
  throw new EciesSealedPoeError("ENC_SLOTS_EMPTY", `envelope.slots.length=${n} must be >= 1`);
1020
1115
  }
1116
+ if (n > MAX_SLOTS) {
1117
+ throw new EciesSealedPoeError(
1118
+ "ENC_SLOTS_TOO_MANY",
1119
+ `envelope.slots.length=${n} exceeds MAX_SLOTS=${MAX_SLOTS}`
1120
+ );
1121
+ }
1021
1122
  if (envelope.nonce.length !== NONCE_LENGTH2) {
1022
1123
  throw new EciesSealedPoeError(
1023
1124
  "NONCE_LENGTH_MISMATCH",
@@ -1030,6 +1131,7 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1030
1131
  `envelope.slots_mac MUST be exactly ${SLOTS_MAC_LENGTH2} bytes, got ${envelope.slots_mac.length}`
1031
1132
  );
1032
1133
  }
1134
+ const seenKemMaterial = /* @__PURE__ */ new Set();
1033
1135
  if (envelope.kem === "x25519") {
1034
1136
  for (let i = 0; i < n; i++) {
1035
1137
  const slot = envelope.slots[i];
@@ -1045,6 +1147,14 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1045
1147
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
1046
1148
  );
1047
1149
  }
1150
+ const key = bytesKey(slot.epk);
1151
+ if (seenKemMaterial.has(key)) {
1152
+ throw new EciesSealedPoeError(
1153
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
1154
+ `envelope.slots[${i}].epk duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
1155
+ );
1156
+ }
1157
+ seenKemMaterial.add(key);
1048
1158
  }
1049
1159
  } else {
1050
1160
  for (let i = 0; i < n; i++) {
@@ -1062,8 +1172,24 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1062
1172
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
1063
1173
  );
1064
1174
  }
1175
+ const key = bytesKey(enc);
1176
+ if (seenKemMaterial.has(key)) {
1177
+ throw new EciesSealedPoeError(
1178
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
1179
+ `envelope.slots[${i}].kem_ct duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
1180
+ );
1181
+ }
1182
+ seenKemMaterial.add(key);
1065
1183
  }
1066
1184
  }
1185
+ const perSlotBytes = envelope.kem === "x25519" ? X25519_PUBLIC_KEY_LENGTH2 + WRAP_LENGTH2 : MLKEM768X25519_ENC_LENGTH + WRAP_LENGTH2;
1186
+ const decodedEnvelopeBytes = NONCE_LENGTH2 + SLOTS_MAC_LENGTH2 + n * perSlotBytes;
1187
+ if (decodedEnvelopeBytes > MAX_DECODED_ENVELOPE_BYTES) {
1188
+ throw new EciesSealedPoeError(
1189
+ "ENC_ENVELOPE_TOO_LARGE",
1190
+ `decoded envelope size ${decodedEnvelopeBytes} exceeds MAX_DECODED_ENVELOPE_BYTES=${MAX_DECODED_ENVELOPE_BYTES}`
1191
+ );
1192
+ }
1067
1193
  if (multiPrivKeys !== void 0) {
1068
1194
  for (let i = 0; i < multiPrivKeys.length; i++) {
1069
1195
  if (multiPrivKeys[i].length !== X25519_SECRET_KEY_LENGTH2) {
@@ -1082,60 +1208,42 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1082
1208
  }
1083
1209
  }
1084
1210
  }
1211
+ var ZERO_IKM_32 = new Uint8Array(32);
1085
1212
  function tryX25519Slot(args) {
1086
- if (args.liveSlot) {
1087
- try {
1088
- const shared = x25519Ecdh({
1089
- secretKey: args.recipientSecretKey,
1090
- theirPublicKey: args.slot.epk
1091
- });
1092
- const kek = hkdfSha2562({
1093
- ikm: shared,
1094
- salt: concat2(args.slot.epk, args.pubRLocal),
1095
- info: CARDANO_POE_HKDF_INFO_KEK,
1096
- length: 32
1097
- });
1098
- return chacha20Poly1305Decrypt({
1099
- key: kek,
1100
- nonce: ZERO_NONCE_122,
1101
- aad: CARDANO_POE_HKDF_INFO_KEK,
1102
- ciphertext: args.slot.wrap
1103
- });
1104
- } catch (e) {
1105
- if (!(e instanceof AeadVerificationError) && !(e instanceof X25519LowOrderPointError)) {
1106
- throw e;
1107
- }
1108
- return null;
1109
- }
1110
- }
1213
+ const salt = concat2(args.slot.epk, args.pubRLocal);
1214
+ let shared;
1111
1215
  try {
1112
- const shared = x25519Ecdh({
1216
+ shared = x25519Ecdh({
1113
1217
  secretKey: args.recipientSecretKey,
1114
1218
  theirPublicKey: args.slot.epk
1115
1219
  });
1116
- hkdfSha2562({
1117
- ikm: shared,
1118
- salt: concat2(args.slot.epk, args.pubRLocal),
1119
- info: CARDANO_POE_HKDF_INFO_KEK,
1120
- length: 32
1121
- });
1122
1220
  } catch (e) {
1123
1221
  if (!(e instanceof X25519LowOrderPointError)) throw e;
1222
+ hkdfSha2562({ ikm: ZERO_IKM_32, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
1223
+ return null;
1224
+ }
1225
+ const kek = hkdfSha2562({ ikm: shared, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
1226
+ try {
1227
+ return chacha20Poly1305Decrypt({
1228
+ key: kek,
1229
+ nonce: ZERO_NONCE_122,
1230
+ aad: CARDANO_POE_HKDF_INFO_KEK,
1231
+ ciphertext: args.slot.wrap
1232
+ });
1233
+ } catch (e) {
1234
+ if (!(e instanceof AeadVerificationError)) throw e;
1235
+ return null;
1124
1236
  }
1125
- return null;
1126
1237
  }
1127
1238
  function tryMlkem768X25519Slot(args) {
1128
1239
  const enc = joinKemCt(args.slot.kem_ct);
1129
1240
  const ss = mlkem768x25519Decapsulate({ secretSeed: args.recipientSecretKey, enc });
1130
1241
  const kek = hkdfSha2562({
1131
1242
  ikm: ss,
1132
- salt: EMPTY_SALT22,
1243
+ salt: xwingKekSalt({ kemCt: enc, pubR: args.pubR }),
1133
1244
  info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
1134
1245
  length: 32
1135
1246
  });
1136
- if (!args.liveSlot) {
1137
- return null;
1138
- }
1139
1247
  try {
1140
1248
  return chacha20Poly1305Decrypt({
1141
1249
  key: kek,
@@ -1152,51 +1260,43 @@ function tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN,
1152
1260
  const n = envelope.slots.length;
1153
1261
  let cek = null;
1154
1262
  let matchedSlotIdx = -1;
1263
+ let cekConflict = false;
1264
+ const recordMatch = (candidate, i) => {
1265
+ if (candidate === null) return;
1266
+ if (cek === null) {
1267
+ cek = candidate;
1268
+ matchedSlotIdx = i;
1269
+ } else if (!compareCt(candidate, cek)) {
1270
+ cekConflict = true;
1271
+ }
1272
+ };
1155
1273
  if (envelope.kem === "x25519") {
1156
1274
  const pubRLocal = x25519PublicKey2({ secretKey: recipientSecretKey });
1157
1275
  for (let i = 0; i < n; i++) {
1158
1276
  if (slotsAttemptedOut !== void 0) {
1159
1277
  slotsAttemptedOut.count = i + 1;
1160
1278
  }
1161
- const candidate = tryX25519Slot({
1162
- slot: envelope.slots[i],
1163
- recipientSecretKey,
1164
- pubRLocal,
1165
- liveSlot: cek === null
1166
- });
1167
- if (cek === null && candidate !== null) {
1168
- cek = candidate;
1169
- matchedSlotIdx = i;
1170
- }
1279
+ recordMatch(tryX25519Slot({ slot: envelope.slots[i], recipientSecretKey, pubRLocal }), i);
1171
1280
  if (cek !== null && !constantTimeN) break;
1172
1281
  }
1173
1282
  } else {
1283
+ const pubR = mlkem768x25519Keygen2(recipientSecretKey).publicKey;
1174
1284
  for (let i = 0; i < n; i++) {
1175
1285
  if (slotsAttemptedOut !== void 0) {
1176
1286
  slotsAttemptedOut.count = i + 1;
1177
1287
  }
1178
- const candidate = tryMlkem768X25519Slot({
1179
- slot: envelope.slots[i],
1180
- recipientSecretKey,
1181
- liveSlot: cek === null
1182
- });
1183
- if (cek === null && candidate !== null) {
1184
- cek = candidate;
1185
- matchedSlotIdx = i;
1186
- }
1288
+ recordMatch(tryMlkem768X25519Slot({ slot: envelope.slots[i], recipientSecretKey, pubR }), i);
1187
1289
  if (cek !== null && !constantTimeN) break;
1188
1290
  }
1189
1291
  }
1190
- return cek === null ? null : { cek, slotIdx: matchedSlotIdx };
1292
+ return cek === null ? null : { cek, slotIdx: matchedSlotIdx, cekConflict };
1191
1293
  }
1192
- function tryRecipientUnwrap(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut) {
1193
- return tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut)?.cek ?? null;
1194
- }
1195
- function slotsMacCborBytes(envelope) {
1196
- return slotsToMacCbor(
1197
- envelope.slots,
1198
- envelope.kem
1199
- );
1294
+ function slotsHashBytes(envelope) {
1295
+ return computeSlotsHash({
1296
+ kem: envelope.kem,
1297
+ nonce: envelope.nonce,
1298
+ slots: envelope.slots
1299
+ });
1200
1300
  }
1201
1301
  function eciesSealedPoeUnwrap(args) {
1202
1302
  const { envelope, ciphertext } = args;
@@ -1225,34 +1325,38 @@ function eciesSealedPoeUnwrap(args) {
1225
1325
  } else {
1226
1326
  assertEnvelopeStructure(envelope, void 0, args.recipientSecretKey);
1227
1327
  }
1328
+ assertCiphertextWithinBound(ciphertext.length);
1329
+ const slotsHash = slotsHashBytes(envelope);
1228
1330
  let matchedCek = null;
1229
1331
  let anyCandidateRecovered = false;
1230
1332
  if (hasSingle) {
1231
1333
  const recipientSecretKey = args.recipientSecretKey;
1232
- const cek = tryRecipientUnwrap(
1334
+ const candidate = tryRecipientUnwrapWithIdx(
1233
1335
  envelope,
1234
1336
  recipientSecretKey,
1235
1337
  constantTimeN,
1236
1338
  args._slotsAttemptedOut
1237
1339
  );
1238
- if (cek === null) {
1340
+ if (candidate === null) {
1239
1341
  return { matched: false, reason: "WRONG_RECIPIENT_KEY" };
1240
1342
  }
1241
- const slotsCbor = slotsMacCborBytes(envelope);
1343
+ if (candidate.cekConflict) {
1344
+ return { matched: false, reason: "TAMPERED_HEADER" };
1345
+ }
1242
1346
  const hmacKey = hkdfSha2562({
1243
- ikm: cek,
1347
+ ikm: candidate.cek,
1244
1348
  salt: EMPTY_SALT22,
1245
1349
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1246
1350
  length: 32
1247
1351
  });
1248
- const slotsMacCalc = hmac(sha256, hmacKey, slotsCbor);
1352
+ const slotsMacCalc = hmac(sha256, hmacKey, slotsHash);
1249
1353
  if (!compareCt(slotsMacCalc, envelope.slots_mac)) {
1250
1354
  return { matched: false, reason: "TAMPERED_HEADER" };
1251
1355
  }
1252
- matchedCek = cek;
1356
+ matchedCek = candidate.cek;
1253
1357
  } else {
1254
- const slotsCbor = slotsMacCborBytes(envelope);
1255
1358
  const recipientSecretKeys = multiPrivKeys;
1359
+ let cekConflict = false;
1256
1360
  for (let k = 0; k < recipientSecretKeys.length; k++) {
1257
1361
  if (args._privsAttemptedOut !== void 0) {
1258
1362
  args._privsAttemptedOut.count = k + 1;
@@ -1260,7 +1364,7 @@ function eciesSealedPoeUnwrap(args) {
1260
1364
  if (args._slotsAttemptedOut !== void 0) {
1261
1365
  args._slotsAttemptedOut.count = 0;
1262
1366
  }
1263
- const cek = tryRecipientUnwrap(
1367
+ const candidate = tryRecipientUnwrapWithIdx(
1264
1368
  envelope,
1265
1369
  recipientSecretKeys[k],
1266
1370
  constantTimeN,
@@ -1269,20 +1373,25 @@ function eciesSealedPoeUnwrap(args) {
1269
1373
  if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
1270
1374
  args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
1271
1375
  }
1272
- if (cek === null) continue;
1376
+ if (candidate === null) continue;
1377
+ if (candidate.cekConflict) cekConflict = true;
1378
+ const cek = candidate.cek;
1273
1379
  const hmacKey = hkdfSha2562({
1274
1380
  ikm: cek,
1275
1381
  salt: EMPTY_SALT22,
1276
1382
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1277
1383
  length: 32
1278
1384
  });
1279
- const slotsMacCalc = hmac(sha256, hmacKey, slotsCbor);
1385
+ const slotsMacCalc = hmac(sha256, hmacKey, slotsHash);
1280
1386
  if (compareCt(slotsMacCalc, envelope.slots_mac)) {
1281
1387
  matchedCek = cek;
1282
1388
  break;
1283
1389
  }
1284
1390
  anyCandidateRecovered = true;
1285
1391
  }
1392
+ if (matchedCek !== null && cekConflict) {
1393
+ return { matched: false, reason: "TAMPERED_HEADER" };
1394
+ }
1286
1395
  if (matchedCek === null) {
1287
1396
  return {
1288
1397
  matched: false,
@@ -1290,10 +1399,16 @@ function eciesSealedPoeUnwrap(args) {
1290
1399
  };
1291
1400
  }
1292
1401
  }
1293
- const adContent = concat2(envelope.nonce, envelope.slots_mac);
1402
+ const payloadKey = slotsPayloadKey({ cek: matchedCek, nonce: envelope.nonce });
1403
+ const adContent = adContentSlots({
1404
+ kem: envelope.kem,
1405
+ nonce: envelope.nonce,
1406
+ slotsHash,
1407
+ slotsMac: envelope.slots_mac
1408
+ });
1294
1409
  try {
1295
1410
  const plaintext = xchacha20Poly1305Decrypt({
1296
- key: matchedCek,
1411
+ key: payloadKey,
1297
1412
  nonce: envelope.nonce,
1298
1413
  aad: adContent,
1299
1414
  ciphertext