@cardanowall/sdk-ts 0.2.0 → 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.
Files changed (49) hide show
  1. package/dist/client/index.cjs +2566 -1706
  2. package/dist/client/index.cjs.map +1 -1
  3. package/dist/client/index.d.cts +42 -5
  4. package/dist/client/index.d.ts +42 -5
  5. package/dist/client/index.js +2564 -1708
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/conformance/cli.cjs +5978 -3438
  8. package/dist/conformance/cli.cjs.map +1 -1
  9. package/dist/conformance/cli.js +5978 -3438
  10. package/dist/conformance/cli.js.map +1 -1
  11. package/dist/fetch/index.cjs +33 -14
  12. package/dist/fetch/index.cjs.map +1 -1
  13. package/dist/fetch/index.d.cts +2 -2
  14. package/dist/fetch/index.d.ts +2 -2
  15. package/dist/fetch/index.js +32 -15
  16. package/dist/fetch/index.js.map +1 -1
  17. package/dist/{fetch-outbound-BT5-NiYN.d.cts → fetch-outbound-dOK3ZxYa.d.cts} +7 -3
  18. package/dist/{fetch-outbound-BT5-NiYN.d.ts → fetch-outbound-dOK3ZxYa.d.ts} +7 -3
  19. package/dist/hash/index.cjs +1 -1
  20. package/dist/hash/index.cjs.map +1 -1
  21. package/dist/hash/index.js +1 -1
  22. package/dist/hash/index.js.map +1 -1
  23. package/dist/identity/index.cjs +460 -219
  24. package/dist/identity/index.cjs.map +1 -1
  25. package/dist/identity/index.d.cts +3 -2
  26. package/dist/identity/index.d.ts +3 -2
  27. package/dist/identity/index.js +460 -219
  28. package/dist/identity/index.js.map +1 -1
  29. package/dist/index.cjs +6912 -3678
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +7 -7
  32. package/dist/index.d.ts +7 -7
  33. package/dist/index.js +6890 -3672
  34. package/dist/index.js.map +1 -1
  35. package/dist/merkle/index.cjs +1 -1
  36. package/dist/merkle/index.js +1 -1
  37. package/dist/types-Cexm4VH9.d.cts +119 -0
  38. package/dist/types-CgoBub9J.d.ts +119 -0
  39. package/dist/{types-DGsZTMuZ.d.cts → types-Dp4wUSFI.d.cts} +220 -1
  40. package/dist/{types-DGsZTMuZ.d.ts → types-Dp4wUSFI.d.ts} +220 -1
  41. package/dist/verifier/index.cjs +5738 -3205
  42. package/dist/verifier/index.cjs.map +1 -1
  43. package/dist/verifier/index.d.cts +159 -111
  44. package/dist/verifier/index.d.ts +159 -111
  45. package/dist/verifier/index.js +5726 -3201
  46. package/dist/verifier/index.js.map +1 -1
  47. package/package.json +3 -3
  48. package/dist/types-B8Q3gW54.d.ts +0 -123
  49. package/dist/types-CLXdbjqr.d.cts +0 -123
@@ -11,10 +11,11 @@ import { anumber, abytes, randomBytes as randomBytes$1, u32, swap32IfBE } from '
11
11
  import { FFTCore, reverseBits } from '@noble/curves/abstract/fft.js';
12
12
  import * as ed from '@noble/ed25519';
13
13
  import '@noble/ciphers/utils.js';
14
+ import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
14
15
  import { hmac } from '@noble/hashes/hmac.js';
15
- import { xchacha20poly1305, chacha20poly1305 } from '@noble/ciphers/chacha.js';
16
16
  import { encode } from 'cbor2';
17
17
  import { sortCoreDeterministic } from 'cbor2/sorts';
18
+ import 'hash-wasm';
18
19
 
19
20
  // ../crypto-core/dist/seed-derive.js
20
21
  var abytesDoc = abytes;
@@ -845,18 +846,17 @@ function chacha20Poly1305Decrypt(opts2) {
845
846
  throw new AeadVerificationError("chacha20-poly1305 decrypt failed", { cause });
846
847
  }
847
848
  }
848
- function xchacha20Poly1305Decrypt(opts2) {
849
- try {
850
- return xchacha20poly1305(opts2.key, opts2.nonce, opts2.aad).decrypt(opts2.ciphertext);
851
- } catch (cause) {
852
- throw new AeadVerificationError("xchacha20-poly1305 decrypt failed", { cause });
853
- }
854
- }
855
- function hkdfSha2562(opts2) {
856
- return hkdf(sha256, opts2.ikm, opts2.salt, opts2.info, opts2.length);
857
- }
858
849
  var MLKEM768X25519_ENC_LENGTH = 1120;
859
850
  var MLKEM768X25519_SEED_LENGTH2 = 32;
851
+ function mlkem768x25519Keygen2(seed) {
852
+ if (seed.length !== MLKEM768X25519_SEED_LENGTH2) {
853
+ throw new Error(
854
+ `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH2} bytes, got ${seed.length}`
855
+ );
856
+ }
857
+ const { secretKey, publicKey } = XWing.keygen(seed);
858
+ return { secretSeed: secretKey, publicKey };
859
+ }
860
860
  function mlkem768x25519Decapsulate(opts2) {
861
861
  if (opts2.secretSeed.length !== MLKEM768X25519_SEED_LENGTH2) {
862
862
  throw new Error(
@@ -891,6 +891,9 @@ function x25519Ecdh(opts2) {
891
891
  throw e;
892
892
  }
893
893
  }
894
+ function hkdfSha2562(opts2) {
895
+ return hkdf(sha256, opts2.ikm, opts2.salt, opts2.info, opts2.length);
896
+ }
894
897
  var EciesSealedPoeError = class extends Error {
895
898
  code;
896
899
  constructor(code, message, options) {
@@ -899,6 +902,139 @@ var EciesSealedPoeError = class extends Error {
899
902
  this.code = code;
900
903
  }
901
904
  };
905
+ var CHUNK_SIZE = 65536;
906
+ var TAG_SIZE = 16;
907
+ var NONCE_LENGTH = 12;
908
+ var COUNTER_LENGTH = 11;
909
+ var SEALED_CHUNK_SIZE = CHUNK_SIZE + TAG_SIZE;
910
+ var PAYLOAD_KEY_LENGTH = 32;
911
+ var EMPTY_AAD = new Uint8Array(0);
912
+ var StreamTamperedError = class extends Error {
913
+ code = "TAMPERED_CIPHERTEXT";
914
+ constructor(message, options) {
915
+ super(message, options);
916
+ this.name = "StreamTamperedError";
917
+ }
918
+ };
919
+ var ChunkNonce = class {
920
+ nonce = new Uint8Array(NONCE_LENGTH);
921
+ finished = false;
922
+ next(final) {
923
+ if (this.finished) {
924
+ throw new Error("STREAM: no chunks may follow the final chunk");
925
+ }
926
+ if (final) {
927
+ this.finished = true;
928
+ this.nonce[COUNTER_LENGTH] = 1;
929
+ }
930
+ const out = this.nonce.slice();
931
+ this.increment();
932
+ return out;
933
+ }
934
+ get done() {
935
+ return this.finished;
936
+ }
937
+ increment() {
938
+ for (let i = COUNTER_LENGTH - 1; i >= 0; i--) {
939
+ const v = this.nonce[i] + 1 & 255;
940
+ this.nonce[i] = v;
941
+ if (v !== 0) return;
942
+ }
943
+ throw new Error("STREAM: chunk counter overflow");
944
+ }
945
+ };
946
+ function assertPayloadKey(payloadKey) {
947
+ if (payloadKey.length !== PAYLOAD_KEY_LENGTH) {
948
+ throw new Error(
949
+ `STREAM: payloadKey MUST be exactly ${PAYLOAD_KEY_LENGTH} bytes, got ${payloadKey.length}`
950
+ );
951
+ }
952
+ }
953
+ var StreamOpener = class {
954
+ payloadKey;
955
+ nonce = new ChunkNonce();
956
+ chunkIndex = 0;
957
+ constructor(payloadKey) {
958
+ assertPayloadKey(payloadKey);
959
+ this.payloadKey = payloadKey;
960
+ }
961
+ openChunk(sealedChunk, final) {
962
+ if (sealedChunk.length < TAG_SIZE) {
963
+ throw new StreamTamperedError(
964
+ `STREAM: sealed chunk shorter than the ${TAG_SIZE}-byte tag floor`
965
+ );
966
+ }
967
+ if (!final && sealedChunk.length !== SEALED_CHUNK_SIZE) {
968
+ throw new StreamTamperedError(
969
+ `STREAM: non-final sealed chunk MUST be exactly ${SEALED_CHUNK_SIZE} bytes, got ${sealedChunk.length}`
970
+ );
971
+ }
972
+ if (final && sealedChunk.length > SEALED_CHUNK_SIZE) {
973
+ throw new StreamTamperedError(
974
+ `STREAM: final sealed chunk MUST be at most ${SEALED_CHUNK_SIZE} bytes, got ${sealedChunk.length}`
975
+ );
976
+ }
977
+ if (final && sealedChunk.length === TAG_SIZE && this.chunkIndex > 0) {
978
+ throw new StreamTamperedError("STREAM: zero-length final chunk on a non-empty stream");
979
+ }
980
+ let plaintext;
981
+ try {
982
+ plaintext = chacha20Poly1305Decrypt({
983
+ key: this.payloadKey,
984
+ nonce: this.nonce.next(final),
985
+ aad: EMPTY_AAD,
986
+ ciphertext: sealedChunk
987
+ });
988
+ } catch (e) {
989
+ if (!(e instanceof AeadVerificationError)) throw e;
990
+ throw new StreamTamperedError(`STREAM: chunk ${this.chunkIndex} tag verification failed`, {
991
+ cause: e
992
+ });
993
+ }
994
+ this.chunkIndex += 1;
995
+ return plaintext;
996
+ }
997
+ };
998
+ function streamOpen(args) {
999
+ const { ciphertext } = args;
1000
+ const total = ciphertext.length;
1001
+ if (total < TAG_SIZE) {
1002
+ throw new StreamTamperedError(
1003
+ `STREAM: ciphertext shorter than the ${TAG_SIZE}-byte single-tag floor`
1004
+ );
1005
+ }
1006
+ const rem = total % SEALED_CHUNK_SIZE;
1007
+ let nonFinalCount;
1008
+ let finalSealedLength;
1009
+ if (rem === 0) {
1010
+ nonFinalCount = total / SEALED_CHUNK_SIZE - 1;
1011
+ finalSealedLength = SEALED_CHUNK_SIZE;
1012
+ } else if (rem >= TAG_SIZE) {
1013
+ nonFinalCount = (total - rem) / SEALED_CHUNK_SIZE;
1014
+ finalSealedLength = rem;
1015
+ } else {
1016
+ throw new StreamTamperedError("STREAM: trailing bytes cannot form a well-formed final chunk");
1017
+ }
1018
+ if (nonFinalCount > 0 && finalSealedLength === TAG_SIZE) {
1019
+ throw new StreamTamperedError("STREAM: zero-length final chunk on a non-empty stream");
1020
+ }
1021
+ const opener = new StreamOpener(args.payloadKey);
1022
+ const out = new Uint8Array(nonFinalCount * CHUNK_SIZE + finalSealedLength - TAG_SIZE);
1023
+ let readOffset = 0;
1024
+ let writeOffset = 0;
1025
+ for (let i = 0; i < nonFinalCount; i++) {
1026
+ const plaintext = opener.openChunk(
1027
+ ciphertext.subarray(readOffset, readOffset + SEALED_CHUNK_SIZE),
1028
+ false
1029
+ );
1030
+ out.set(plaintext, writeOffset);
1031
+ readOffset += SEALED_CHUNK_SIZE;
1032
+ writeOffset += CHUNK_SIZE;
1033
+ }
1034
+ const finalPlaintext = opener.openChunk(ciphertext.subarray(readOffset), true);
1035
+ out.set(finalPlaintext, writeOffset);
1036
+ return out;
1037
+ }
902
1038
  function encodeCanonicalCbor(value) {
903
1039
  return encode(value, {
904
1040
  cde: true,
@@ -907,57 +1043,138 @@ function encodeCanonicalCbor(value) {
907
1043
  sortKeys: sortCoreDeterministic
908
1044
  });
909
1045
  }
910
- var CHUNK_MAX_BYTES = 64;
911
- function chunkKemCt(value) {
912
- if (value.length === 0) {
913
- throw new Error("chunkKemCt: refusing to chunk an empty byte string");
914
- }
915
- const chunks = [];
916
- for (let i = 0; i < value.length; i += CHUNK_MAX_BYTES) {
917
- chunks.push(value.subarray(i, Math.min(i + CHUNK_MAX_BYTES, value.length)));
918
- }
919
- return chunks;
920
- }
921
- function joinKemCt(chunks) {
922
- let total = 0;
923
- for (const c of chunks) total += c.length;
924
- const out = new Uint8Array(total);
925
- let offset = 0;
926
- for (const c of chunks) {
927
- out.set(c, offset);
928
- offset += c.length;
1046
+ var CARDANO_POE_ITEM_HASHES_PREFIX = new TextEncoder().encode(
1047
+ "cardano-poe-item-hashes-v1"
1048
+ );
1049
+ var CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX = new TextEncoder().encode(
1050
+ "cardano-poe-slots-transcript-v1"
1051
+ );
1052
+ var CARDANO_POE_PASSPHRASE_TRANSCRIPT_PREFIX = new TextEncoder().encode(
1053
+ "cardano-poe-passphrase-transcript-v1"
1054
+ );
1055
+ var CARDANO_POE_HKDF_INFO_SLOTS_MAC = new TextEncoder().encode(
1056
+ "cardano-poe-slots-mac-v1"
1057
+ );
1058
+ var CARDANO_POE_HKDF_INFO_PASSPHRASE_MAC = new TextEncoder().encode(
1059
+ "cardano-poe-passphrase-mac-v1"
1060
+ );
1061
+ var CARDANO_POE_HKDF_INFO_PAYLOAD = new TextEncoder().encode(
1062
+ "cardano-poe-payload-v1"
1063
+ );
1064
+ var CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE = new TextEncoder().encode(
1065
+ "cardano-poe-payload-passphrase-v1"
1066
+ );
1067
+ var CARDANO_POE_X25519_KEK_SALT_PREFIX = new TextEncoder().encode(
1068
+ "cardano-poe-x25519-kek-salt-v1"
1069
+ );
1070
+ var CARDANO_POE_XWING_KEK_SALT_PREFIX = new TextEncoder().encode(
1071
+ "cardano-poe-xwing-kek-salt-v1"
1072
+ );
1073
+ if (CARDANO_POE_ITEM_HASHES_PREFIX.length !== 26) {
1074
+ throw new Error("CARDANO_POE_ITEM_HASHES_PREFIX byte-length invariant violated (expected 26)");
1075
+ }
1076
+ if (CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length !== 31) {
1077
+ throw new Error(
1078
+ "CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX byte-length invariant violated (expected 31)"
1079
+ );
1080
+ }
1081
+ if (CARDANO_POE_PASSPHRASE_TRANSCRIPT_PREFIX.length !== 36) {
1082
+ throw new Error(
1083
+ "CARDANO_POE_PASSPHRASE_TRANSCRIPT_PREFIX byte-length invariant violated (expected 36)"
1084
+ );
1085
+ }
1086
+ if (CARDANO_POE_HKDF_INFO_SLOTS_MAC.length !== 24) {
1087
+ throw new Error("CARDANO_POE_HKDF_INFO_SLOTS_MAC byte-length invariant violated (expected 24)");
1088
+ }
1089
+ if (CARDANO_POE_HKDF_INFO_PASSPHRASE_MAC.length !== 29) {
1090
+ throw new Error(
1091
+ "CARDANO_POE_HKDF_INFO_PASSPHRASE_MAC byte-length invariant violated (expected 29)"
1092
+ );
1093
+ }
1094
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD.length !== 22) {
1095
+ throw new Error("CARDANO_POE_HKDF_INFO_PAYLOAD byte-length invariant violated (expected 22)");
1096
+ }
1097
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE.length !== 33) {
1098
+ throw new Error(
1099
+ "CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE byte-length invariant violated (expected 33)"
1100
+ );
1101
+ }
1102
+ if (CARDANO_POE_X25519_KEK_SALT_PREFIX.length !== 30) {
1103
+ throw new Error(
1104
+ "CARDANO_POE_X25519_KEK_SALT_PREFIX byte-length invariant violated (expected 30)"
1105
+ );
1106
+ }
1107
+ if (CARDANO_POE_XWING_KEK_SALT_PREFIX.length !== 29) {
1108
+ throw new Error("CARDANO_POE_XWING_KEK_SALT_PREFIX byte-length invariant violated (expected 29)");
1109
+ }
1110
+ var MAX_SLOTS = 1024;
1111
+ var MAX_DECODED_ENVELOPE_BYTES = 65536;
1112
+ var EMPTY_SALT2 = new Uint8Array(0);
1113
+ function labelledSha256(prefix, ...parts) {
1114
+ let total = prefix.length;
1115
+ for (const p of parts) total += p.length;
1116
+ const message = new Uint8Array(total);
1117
+ message.set(prefix, 0);
1118
+ let offset = prefix.length;
1119
+ for (const p of parts) {
1120
+ message.set(p, offset);
1121
+ offset += p.length;
929
1122
  }
930
- return out;
1123
+ return sha256(message);
931
1124
  }
932
- function slotsToMacCbor(slots, kem) {
933
- let value;
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
- }));
1125
+ function itemHashesHash(hashes3) {
1126
+ if (Object.keys(hashes3).length === 0) {
1127
+ throw new EciesSealedPoeError(
1128
+ "ENC_REQUIRES_CONTENT_HASH",
1129
+ "hashes MUST carry at least one content-hash entry"
1130
+ );
951
1131
  }
952
- return encodeCanonicalCbor(value);
1132
+ return labelledSha256(CARDANO_POE_ITEM_HASHES_PREFIX, encodeCanonicalCbor(hashes3));
1133
+ }
1134
+ function computeSlotsHash(args) {
1135
+ const slots = args.kem === "x25519" ? args.slots.map((s) => ({ epk: s.epk, wrap: s.wrap })) : args.slots.map((s) => ({
1136
+ kem_ct: s.kem_ct,
1137
+ wrap: s.wrap
1138
+ }));
1139
+ const transcript = {
1140
+ scheme: 1,
1141
+ path: "slots",
1142
+ aead: args.aead,
1143
+ kem: args.kem,
1144
+ nonce: args.nonce,
1145
+ slots,
1146
+ hashes_hash: args.hashesHash
1147
+ };
1148
+ return labelledSha256(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX, encodeCanonicalCbor(transcript));
953
1149
  }
1150
+ function computeSlotsMac(args) {
1151
+ const macKey = hkdfSha2562({
1152
+ ikm: args.cek,
1153
+ salt: EMPTY_SALT2,
1154
+ info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1155
+ length: 32
1156
+ });
1157
+ return hmac(sha256, macKey, args.slotsHash);
1158
+ }
1159
+ function slotsPayloadKey(args) {
1160
+ return hkdfSha2562({
1161
+ ikm: args.cek,
1162
+ salt: args.nonce,
1163
+ info: CARDANO_POE_HKDF_INFO_PAYLOAD,
1164
+ length: 32
1165
+ });
1166
+ }
1167
+ function x25519KekSalt(args) {
1168
+ return labelledSha256(CARDANO_POE_X25519_KEK_SALT_PREFIX, args.nonce, args.epk, args.pubR);
1169
+ }
1170
+ function xwingKekSalt(args) {
1171
+ return labelledSha256(CARDANO_POE_XWING_KEK_SALT_PREFIX, args.nonce, args.kemCt, args.pubR);
1172
+ }
1173
+ var SEALED_POE_AEAD = "chacha20-poly1305-stream64k";
954
1174
  var CARDANO_POE_HKDF_INFO_KEK = new TextEncoder().encode("cardano-poe-kek-v1");
955
1175
  var CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 = new TextEncoder().encode(
956
1176
  "cardano-poe-kek-mlkem768x25519-v1"
957
1177
  );
958
- var CARDANO_POE_HKDF_INFO_SLOTS_MAC = new TextEncoder().encode(
959
- "cardano-poe-slots-mac-v1"
960
- );
961
1178
  var ZERO_NONCE_12 = new Uint8Array(12);
962
1179
  if (CARDANO_POE_HKDF_INFO_KEK.length !== 18) {
963
1180
  throw new Error("CARDANO_POE_HKDF_INFO_KEK byte-length invariant violated (expected 18)");
@@ -967,9 +1184,6 @@ if (CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519.length !== 33) {
967
1184
  "CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 byte-length invariant violated (expected 33)"
968
1185
  );
969
1186
  }
970
- if (CARDANO_POE_HKDF_INFO_SLOTS_MAC.length !== 24) {
971
- throw new Error("CARDANO_POE_HKDF_INFO_SLOTS_MAC byte-length invariant violated (expected 24)");
972
- }
973
1187
  if (ZERO_NONCE_12.length !== 12) {
974
1188
  throw new Error("ZERO_NONCE_12 byte-length invariant violated (expected 12)");
975
1189
  }
@@ -979,33 +1193,74 @@ function compareCt(a, b) {
979
1193
  for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
980
1194
  return diff === 0;
981
1195
  }
1196
+ var CEK_LENGTH2 = 32;
1197
+ function newSlotAcceptanceState() {
1198
+ return {
1199
+ foundBit: 0,
1200
+ conflictBit: 0,
1201
+ selectedCek: new Uint8Array(CEK_LENGTH2),
1202
+ selectedSlotIdx: -1
1203
+ };
1204
+ }
1205
+ function foldSlotAcceptance(state, ok, candidateCek, slotIdx) {
1206
+ if (candidateCek.length !== CEK_LENGTH2) {
1207
+ throw new Error(
1208
+ `candidate CEK MUST be exactly ${CEK_LENGTH2} bytes, got ${candidateCek.length}`
1209
+ );
1210
+ }
1211
+ const okBit = ok & 1;
1212
+ const firstBit = okBit & (state.foundBit ^ 1);
1213
+ const firstByteMask = -firstBit & 255;
1214
+ const firstWordMask = -firstBit | 0;
1215
+ let diff = 0;
1216
+ for (let i = 0; i < CEK_LENGTH2; i++) {
1217
+ diff |= candidateCek[i] ^ state.selectedCek[i];
1218
+ }
1219
+ const neqBit = (diff | -diff) >>> 31 & 1;
1220
+ state.conflictBit |= okBit & state.foundBit & neqBit;
1221
+ for (let i = 0; i < CEK_LENGTH2; i++) {
1222
+ state.selectedCek[i] = candidateCek[i] & firstByteMask | state.selectedCek[i] & ~firstByteMask & 255;
1223
+ }
1224
+ state.selectedSlotIdx = slotIdx & firstWordMask | state.selectedSlotIdx & ~firstWordMask;
1225
+ state.foundBit |= okBit;
1226
+ }
1227
+ function finishSlotAcceptance(state) {
1228
+ const found = state.foundBit === 1;
1229
+ return {
1230
+ found,
1231
+ cekConflict: state.conflictBit === 1,
1232
+ selectedCek: found ? state.selectedCek : null,
1233
+ selectedSlotIdx: state.selectedSlotIdx
1234
+ };
1235
+ }
982
1236
  function selectBundleSecrets(envelope, bundle) {
983
1237
  return envelope.kem === "x25519" ? bundle.x25519PrivateKeys : bundle.mlkem768x25519SecretSeeds;
984
1238
  }
985
1239
  var ZERO_NONCE_122 = new Uint8Array(12);
986
- var EMPTY_SALT22 = new Uint8Array(0);
1240
+ var CEK_LENGTH3 = 32;
987
1241
  var X25519_SECRET_KEY_LENGTH2 = 32;
988
1242
  var X25519_PUBLIC_KEY_LENGTH2 = 32;
989
- var NONCE_LENGTH2 = 24;
1243
+ var NONCE_LENGTH3 = 24;
990
1244
  var WRAP_LENGTH2 = 48;
991
1245
  var SLOTS_MAC_LENGTH2 = 32;
992
- function concat2(a, b) {
993
- const out = new Uint8Array(a.length + b.length);
994
- out.set(a, 0);
995
- out.set(b, a.length);
996
- return out;
1246
+ function bytesKey(bytes) {
1247
+ let s = "";
1248
+ for (let i = 0; i < bytes.length; i++) {
1249
+ s += String.fromCharCode(bytes[i]);
1250
+ }
1251
+ return s;
997
1252
  }
998
1253
  function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
999
1254
  if (envelope.scheme !== 1) {
1000
1255
  throw new EciesSealedPoeError(
1001
- "UNSUPPORTED_ENC_VERSION",
1256
+ "UNSUPPORTED_ENVELOPE_SCHEME",
1002
1257
  `envelope.scheme=${String(envelope.scheme)} unsupported (expected 1)`
1003
1258
  );
1004
1259
  }
1005
- if (envelope.aead !== "xchacha20-poly1305") {
1260
+ if (envelope.aead !== SEALED_POE_AEAD) {
1006
1261
  throw new EciesSealedPoeError(
1007
1262
  "UNSUPPORTED_AEAD_ALG",
1008
- `envelope.aead=${String(envelope.aead)} unsupported (expected 'xchacha20-poly1305')`
1263
+ `envelope.aead=${String(envelope.aead)} unsupported (expected '${SEALED_POE_AEAD}')`
1009
1264
  );
1010
1265
  }
1011
1266
  if (envelope.kem !== "x25519" && envelope.kem !== "mlkem768x25519") {
@@ -1018,10 +1273,16 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1018
1273
  if (n < 1) {
1019
1274
  throw new EciesSealedPoeError("ENC_SLOTS_EMPTY", `envelope.slots.length=${n} must be >= 1`);
1020
1275
  }
1021
- if (envelope.nonce.length !== NONCE_LENGTH2) {
1276
+ if (n > MAX_SLOTS) {
1277
+ throw new EciesSealedPoeError(
1278
+ "ENC_SLOTS_TOO_MANY",
1279
+ `envelope.slots.length=${n} exceeds MAX_SLOTS=${MAX_SLOTS}`
1280
+ );
1281
+ }
1282
+ if (envelope.nonce.length !== NONCE_LENGTH3) {
1022
1283
  throw new EciesSealedPoeError(
1023
1284
  "NONCE_LENGTH_MISMATCH",
1024
- `envelope.nonce MUST be exactly ${NONCE_LENGTH2} bytes, got ${envelope.nonce.length}`
1285
+ `envelope.nonce MUST be exactly ${NONCE_LENGTH3} bytes, got ${envelope.nonce.length}`
1025
1286
  );
1026
1287
  }
1027
1288
  if (envelope.slots_mac.length !== SLOTS_MAC_LENGTH2) {
@@ -1030,6 +1291,7 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1030
1291
  `envelope.slots_mac MUST be exactly ${SLOTS_MAC_LENGTH2} bytes, got ${envelope.slots_mac.length}`
1031
1292
  );
1032
1293
  }
1294
+ const seenKemMaterial = /* @__PURE__ */ new Set();
1033
1295
  if (envelope.kem === "x25519") {
1034
1296
  for (let i = 0; i < n; i++) {
1035
1297
  const slot = envelope.slots[i];
@@ -1045,15 +1307,22 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1045
1307
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
1046
1308
  );
1047
1309
  }
1310
+ const key = bytesKey(slot.epk);
1311
+ if (seenKemMaterial.has(key)) {
1312
+ throw new EciesSealedPoeError(
1313
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
1314
+ `envelope.slots[${i}].epk duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
1315
+ );
1316
+ }
1317
+ seenKemMaterial.add(key);
1048
1318
  }
1049
1319
  } else {
1050
1320
  for (let i = 0; i < n; i++) {
1051
1321
  const slot = envelope.slots[i];
1052
- const enc = joinKemCt(slot.kem_ct);
1053
- if (enc.length !== MLKEM768X25519_ENC_LENGTH) {
1322
+ if (slot.kem_ct.length !== MLKEM768X25519_ENC_LENGTH) {
1054
1323
  throw new EciesSealedPoeError(
1055
1324
  "KEM_CT_LENGTH_MISMATCH",
1056
- `envelope.slots[${i}].kem_ct MUST reassemble to exactly ${MLKEM768X25519_ENC_LENGTH} bytes, got ${enc.length}`
1325
+ `envelope.slots[${i}].kem_ct MUST be exactly ${MLKEM768X25519_ENC_LENGTH} bytes, got ${slot.kem_ct.length}`
1057
1326
  );
1058
1327
  }
1059
1328
  if (slot.wrap.length !== WRAP_LENGTH2) {
@@ -1062,8 +1331,24 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1062
1331
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
1063
1332
  );
1064
1333
  }
1334
+ const key = bytesKey(slot.kem_ct);
1335
+ if (seenKemMaterial.has(key)) {
1336
+ throw new EciesSealedPoeError(
1337
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
1338
+ `envelope.slots[${i}].kem_ct duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
1339
+ );
1340
+ }
1341
+ seenKemMaterial.add(key);
1065
1342
  }
1066
1343
  }
1344
+ const perSlotBytes = envelope.kem === "x25519" ? X25519_PUBLIC_KEY_LENGTH2 + WRAP_LENGTH2 : MLKEM768X25519_ENC_LENGTH + WRAP_LENGTH2;
1345
+ const decodedEnvelopeBytes = NONCE_LENGTH3 + SLOTS_MAC_LENGTH2 + n * perSlotBytes;
1346
+ if (decodedEnvelopeBytes > MAX_DECODED_ENVELOPE_BYTES) {
1347
+ throw new EciesSealedPoeError(
1348
+ "ENC_ENVELOPE_TOO_LARGE",
1349
+ `decoded envelope size ${decodedEnvelopeBytes} exceeds MAX_DECODED_ENVELOPE_BYTES=${MAX_DECODED_ENVELOPE_BYTES}`
1350
+ );
1351
+ }
1067
1352
  if (multiPrivKeys !== void 0) {
1068
1353
  for (let i = 0; i < multiPrivKeys.length; i++) {
1069
1354
  if (multiPrivKeys[i].length !== X25519_SECRET_KEY_LENGTH2) {
@@ -1082,125 +1367,118 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1082
1367
  }
1083
1368
  }
1084
1369
  }
1085
- 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;
1370
+ var ZERO_IKM_32 = new Uint8Array(32);
1371
+ var DUMMY_CEK_32 = new Uint8Array(32);
1372
+ function wrapOpenOrDummy(kek, aad, wrap) {
1373
+ try {
1374
+ const plaintext = chacha20Poly1305Decrypt({
1375
+ key: kek,
1376
+ nonce: ZERO_NONCE_122,
1377
+ aad,
1378
+ ciphertext: wrap
1379
+ });
1380
+ if (plaintext.length === CEK_LENGTH3) {
1381
+ return { ok: 1, candidate: plaintext };
1109
1382
  }
1383
+ return { ok: 0, candidate: DUMMY_CEK_32 };
1384
+ } catch (e) {
1385
+ if (!(e instanceof AeadVerificationError)) throw e;
1386
+ return { ok: 0, candidate: DUMMY_CEK_32 };
1110
1387
  }
1388
+ }
1389
+ function tryX25519Slot(args) {
1390
+ const salt = x25519KekSalt({ nonce: args.nonce, epk: args.slot.epk, pubR: args.pubRLocal });
1391
+ let kemOk = 1;
1392
+ let kek;
1111
1393
  try {
1112
1394
  const shared = x25519Ecdh({
1113
1395
  secretKey: args.recipientSecretKey,
1114
1396
  theirPublicKey: args.slot.epk
1115
1397
  });
1116
- hkdfSha2562({
1117
- ikm: shared,
1118
- salt: concat2(args.slot.epk, args.pubRLocal),
1119
- info: CARDANO_POE_HKDF_INFO_KEK,
1120
- length: 32
1121
- });
1398
+ kek = hkdfSha2562({ ikm: shared, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
1122
1399
  } catch (e) {
1123
1400
  if (!(e instanceof X25519LowOrderPointError)) throw e;
1401
+ kemOk = 0;
1402
+ kek = hkdfSha2562({ ikm: ZERO_IKM_32, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
1124
1403
  }
1125
- return null;
1404
+ const opened = wrapOpenOrDummy(kek, CARDANO_POE_HKDF_INFO_KEK, args.slot.wrap);
1405
+ return { ok: kemOk & opened.ok, candidate: opened.candidate };
1126
1406
  }
1127
1407
  function tryMlkem768X25519Slot(args) {
1128
- const enc = joinKemCt(args.slot.kem_ct);
1129
- const ss = mlkem768x25519Decapsulate({ secretSeed: args.recipientSecretKey, enc });
1408
+ const ss = mlkem768x25519Decapsulate({
1409
+ secretSeed: args.recipientSecretKey,
1410
+ enc: args.slot.kem_ct
1411
+ });
1130
1412
  const kek = hkdfSha2562({
1131
1413
  ikm: ss,
1132
- salt: EMPTY_SALT22,
1414
+ salt: xwingKekSalt({ nonce: args.nonce, kemCt: args.slot.kem_ct, pubR: args.pubR }),
1133
1415
  info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
1134
1416
  length: 32
1135
1417
  });
1136
- if (!args.liveSlot) {
1137
- return null;
1138
- }
1139
- try {
1140
- return chacha20Poly1305Decrypt({
1141
- key: kek,
1142
- nonce: ZERO_NONCE_122,
1143
- aad: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
1144
- ciphertext: args.slot.wrap
1145
- });
1146
- } catch (e) {
1147
- if (!(e instanceof AeadVerificationError)) throw e;
1148
- return null;
1149
- }
1418
+ return wrapOpenOrDummy(kek, CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519, args.slot.wrap);
1150
1419
  }
1151
- function tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut) {
1420
+ function runPrivPass(envelope, recipientSecretKey, slotsHash, slotsAttemptedOut) {
1152
1421
  const n = envelope.slots.length;
1153
- let cek = null;
1154
- let matchedSlotIdx = -1;
1422
+ const state = newSlotAcceptanceState();
1423
+ let anyOpenedBit = 0;
1424
+ const acceptSlot = (slot, i) => {
1425
+ anyOpenedBit |= slot.ok;
1426
+ const macOk = Number(compareCt(computeSlotsMac({ cek: slot.candidate, slotsHash }), envelope.slots_mac)) & 1;
1427
+ foldSlotAcceptance(state, slot.ok & macOk, slot.candidate, i);
1428
+ };
1155
1429
  if (envelope.kem === "x25519") {
1156
1430
  const pubRLocal = x25519PublicKey2({ secretKey: recipientSecretKey });
1157
1431
  for (let i = 0; i < n; i++) {
1158
1432
  if (slotsAttemptedOut !== void 0) {
1159
1433
  slotsAttemptedOut.count = i + 1;
1160
1434
  }
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
- }
1171
- if (cek !== null && !constantTimeN) break;
1435
+ acceptSlot(
1436
+ tryX25519Slot({
1437
+ slot: envelope.slots[i],
1438
+ nonce: envelope.nonce,
1439
+ recipientSecretKey,
1440
+ pubRLocal
1441
+ }),
1442
+ i
1443
+ );
1172
1444
  }
1173
1445
  } else {
1446
+ const pubR = mlkem768x25519Keygen2(recipientSecretKey).publicKey;
1174
1447
  for (let i = 0; i < n; i++) {
1175
1448
  if (slotsAttemptedOut !== void 0) {
1176
1449
  slotsAttemptedOut.count = i + 1;
1177
1450
  }
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
- }
1187
- if (cek !== null && !constantTimeN) break;
1451
+ acceptSlot(
1452
+ tryMlkem768X25519Slot({
1453
+ slot: envelope.slots[i],
1454
+ nonce: envelope.nonce,
1455
+ recipientSecretKey,
1456
+ pubR
1457
+ }),
1458
+ i
1459
+ );
1188
1460
  }
1189
1461
  }
1190
- return cek === null ? null : { cek, slotIdx: matchedSlotIdx };
1191
- }
1192
- function tryRecipientUnwrap(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut) {
1193
- return tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut)?.cek ?? null;
1462
+ const outcome = finishSlotAcceptance(state);
1463
+ return {
1464
+ found: outcome.found,
1465
+ selectedCek: outcome.selectedCek,
1466
+ selectedSlotIdx: outcome.selectedSlotIdx,
1467
+ cekConflict: outcome.cekConflict,
1468
+ anyOpened: anyOpenedBit === 1
1469
+ };
1194
1470
  }
1195
- function slotsMacCborBytes(envelope) {
1196
- return slotsToMacCbor(
1197
- envelope.slots,
1198
- envelope.kem
1199
- );
1471
+ function slotsHashBytes(envelope, hashes3) {
1472
+ return computeSlotsHash({
1473
+ aead: envelope.aead,
1474
+ kem: envelope.kem,
1475
+ nonce: envelope.nonce,
1476
+ slots: envelope.slots,
1477
+ hashesHash: itemHashesHash(hashes3)
1478
+ });
1200
1479
  }
1201
1480
  function eciesSealedPoeUnwrap(args) {
1202
1481
  const { envelope, ciphertext } = args;
1203
- const constantTimeN = args.constantTimeN ?? true;
1204
1482
  const hasSingle = "recipientSecretKey" in args;
1205
1483
  const hasBundle = "recipientKeyBundle" in args;
1206
1484
  const multiPrivKeys = hasBundle ? selectBundleSecrets(envelope, args.recipientKeyBundle) : "recipientSecretKeys" in args ? args.recipientSecretKeys : void 0;
@@ -1225,85 +1503,47 @@ function eciesSealedPoeUnwrap(args) {
1225
1503
  } else {
1226
1504
  assertEnvelopeStructure(envelope, void 0, args.recipientSecretKey);
1227
1505
  }
1506
+ const slotsHash = slotsHashBytes(envelope, args.hashes);
1228
1507
  let matchedCek = null;
1229
- let anyCandidateRecovered = false;
1230
- if (hasSingle) {
1231
- const recipientSecretKey = args.recipientSecretKey;
1232
- const cek = tryRecipientUnwrap(
1233
- envelope,
1234
- recipientSecretKey,
1235
- constantTimeN,
1236
- args._slotsAttemptedOut
1237
- );
1238
- if (cek === null) {
1239
- return { matched: false, reason: "WRONG_RECIPIENT_KEY" };
1508
+ let anyOpenedAcrossPrivs = false;
1509
+ const privKeys = hasMulti ? multiPrivKeys : [args.recipientSecretKey];
1510
+ for (let k = 0; k < privKeys.length; k++) {
1511
+ if (args._privsAttemptedOut !== void 0) {
1512
+ args._privsAttemptedOut.count = k + 1;
1240
1513
  }
1241
- const slotsCbor = slotsMacCborBytes(envelope);
1242
- const hmacKey = hkdfSha2562({
1243
- ikm: cek,
1244
- salt: EMPTY_SALT22,
1245
- info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1246
- length: 32
1247
- });
1248
- const slotsMacCalc = hmac(sha256, hmacKey, slotsCbor);
1249
- if (!compareCt(slotsMacCalc, envelope.slots_mac)) {
1250
- return { matched: false, reason: "TAMPERED_HEADER" };
1514
+ if (args._slotsAttemptedOut !== void 0) {
1515
+ args._slotsAttemptedOut.count = 0;
1251
1516
  }
1252
- matchedCek = cek;
1253
- } else {
1254
- const slotsCbor = slotsMacCborBytes(envelope);
1255
- const recipientSecretKeys = multiPrivKeys;
1256
- for (let k = 0; k < recipientSecretKeys.length; k++) {
1257
- if (args._privsAttemptedOut !== void 0) {
1258
- args._privsAttemptedOut.count = k + 1;
1259
- }
1260
- if (args._slotsAttemptedOut !== void 0) {
1261
- args._slotsAttemptedOut.count = 0;
1262
- }
1263
- const cek = tryRecipientUnwrap(
1264
- envelope,
1265
- recipientSecretKeys[k],
1266
- constantTimeN,
1267
- args._slotsAttemptedOut
1268
- );
1269
- if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
1270
- args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
1271
- }
1272
- if (cek === null) continue;
1273
- const hmacKey = hkdfSha2562({
1274
- ikm: cek,
1275
- salt: EMPTY_SALT22,
1276
- info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1277
- length: 32
1278
- });
1279
- const slotsMacCalc = hmac(sha256, hmacKey, slotsCbor);
1280
- if (compareCt(slotsMacCalc, envelope.slots_mac)) {
1281
- matchedCek = cek;
1282
- break;
1283
- }
1284
- anyCandidateRecovered = true;
1517
+ const pass = runPrivPass(envelope, privKeys[k], slotsHash, args._slotsAttemptedOut);
1518
+ if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
1519
+ args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
1285
1520
  }
1286
- if (matchedCek === null) {
1287
- return {
1288
- matched: false,
1289
- reason: anyCandidateRecovered ? "TAMPERED_HEADER" : "WRONG_RECIPIENT_KEY"
1290
- };
1521
+ anyOpenedAcrossPrivs = anyOpenedAcrossPrivs || pass.anyOpened;
1522
+ if (!pass.found) continue;
1523
+ if (pass.cekConflict) {
1524
+ return { matched: false, reason: "TAMPERED_HEADER" };
1291
1525
  }
1526
+ matchedCek = pass.selectedCek;
1527
+ break;
1528
+ }
1529
+ if (matchedCek === null) {
1530
+ return {
1531
+ matched: false,
1532
+ reason: anyOpenedAcrossPrivs ? "TAMPERED_HEADER" : "WRONG_RECIPIENT_KEY"
1533
+ };
1292
1534
  }
1293
- const adContent = concat2(envelope.nonce, envelope.slots_mac);
1294
1535
  try {
1295
- const plaintext = xchacha20Poly1305Decrypt({
1296
- key: matchedCek,
1297
- nonce: envelope.nonce,
1298
- aad: adContent,
1536
+ const plaintext = streamOpen({
1537
+ payloadKey: slotsPayloadKey({ cek: matchedCek, nonce: envelope.nonce }),
1299
1538
  ciphertext
1300
1539
  });
1301
1540
  return { matched: true, plaintext };
1302
1541
  } catch (e) {
1303
- if (!(e instanceof AeadVerificationError)) throw e;
1542
+ if (!(e instanceof StreamTamperedError)) throw e;
1304
1543
  return { matched: false, reason: "TAMPERED_CIPHERTEXT" };
1305
1544
  }
1306
1545
  }
1546
+ new TextEncoder();
1307
1547
  ed.hashes.sha512 = sha512;
1308
1548
  ed.Point.CURVE().n;
1309
1549
  function signEd25519(opts2) {
@@ -1345,6 +1585,7 @@ function decryptSealedFromSeed(args) {
1345
1585
  return eciesSealedPoeUnwrap({
1346
1586
  envelope: args.envelope,
1347
1587
  ciphertext: args.ciphertext,
1588
+ hashes: args.hashes,
1348
1589
  recipientKeyBundle: recipientKeyBundleFromSeed(args.seed)
1349
1590
  });
1350
1591
  }