@cardanowall/crypto-core 0.0.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.
- package/LICENSE +202 -0
- package/README.md +192 -0
- package/dist/aead.cjs +44 -0
- package/dist/aead.cjs.map +1 -0
- package/dist/aead.d.cts +38 -0
- package/dist/aead.d.ts +38 -0
- package/dist/aead.js +38 -0
- package/dist/aead.js.map +1 -0
- package/dist/cbor.cjs +69 -0
- package/dist/cbor.cjs.map +1 -0
- package/dist/cbor.d.cts +17 -0
- package/dist/cbor.d.ts +17 -0
- package/dist/cbor.js +64 -0
- package/dist/cbor.js.map +1 -0
- package/dist/cose.cjs +430 -0
- package/dist/cose.cjs.map +1 -0
- package/dist/cose.d.cts +72 -0
- package/dist/cose.d.ts +72 -0
- package/dist/cose.js +398 -0
- package/dist/cose.js.map +1 -0
- package/dist/hash.cjs +165 -0
- package/dist/hash.cjs.map +1 -0
- package/dist/hash.d.cts +30 -0
- package/dist/hash.d.ts +30 -0
- package/dist/hash.js +155 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.cjs +1856 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1759 -0
- package/dist/index.js.map +1 -0
- package/dist/kdf.cjs +26 -0
- package/dist/kdf.cjs.map +1 -0
- package/dist/kdf.d.cts +25 -0
- package/dist/kdf.d.ts +25 -0
- package/dist/kdf.js +23 -0
- package/dist/kdf.js.map +1 -0
- package/dist/kem.cjs +86 -0
- package/dist/kem.cjs.map +1 -0
- package/dist/kem.d.cts +47 -0
- package/dist/kem.d.ts +47 -0
- package/dist/kem.js +73 -0
- package/dist/kem.js.map +1 -0
- package/dist/merkle.cjs +284 -0
- package/dist/merkle.cjs.map +1 -0
- package/dist/merkle.d.cts +24 -0
- package/dist/merkle.d.ts +24 -0
- package/dist/merkle.js +279 -0
- package/dist/merkle.js.map +1 -0
- package/dist/recipient.cjs +141 -0
- package/dist/recipient.cjs.map +1 -0
- package/dist/recipient.d.cts +16 -0
- package/dist/recipient.d.ts +16 -0
- package/dist/recipient.js +135 -0
- package/dist/recipient.js.map +1 -0
- package/dist/sealed-poe.cjs +851 -0
- package/dist/sealed-poe.cjs.map +1 -0
- package/dist/sealed-poe.d.cts +134 -0
- package/dist/sealed-poe.d.ts +134 -0
- package/dist/sealed-poe.js +838 -0
- package/dist/sealed-poe.js.map +1 -0
- package/dist/seed-derive.cjs +129 -0
- package/dist/seed-derive.cjs.map +1 -0
- package/dist/seed-derive.d.cts +28 -0
- package/dist/seed-derive.d.ts +28 -0
- package/dist/seed-derive.js +101 -0
- package/dist/seed-derive.js.map +1 -0
- package/dist/sig.cjs +77 -0
- package/dist/sig.cjs.map +1 -0
- package/dist/sig.d.cts +17 -0
- package/dist/sig.d.ts +17 -0
- package/dist/sig.js +53 -0
- package/dist/sig.js.map +1 -0
- package/dist/util.cjs +36 -0
- package/dist/util.cjs.map +1 -0
- package/dist/util.d.cts +5 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.js +33 -0
- package/dist/util.js.map +1 -0
- package/package.json +122 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var utils_js = require('@noble/ciphers/utils.js');
|
|
4
|
+
var hmac_js = require('@noble/hashes/hmac.js');
|
|
5
|
+
var sha2_js = require('@noble/hashes/sha2.js');
|
|
6
|
+
var chacha_js = require('@noble/ciphers/chacha.js');
|
|
7
|
+
var hkdf_js = require('@noble/hashes/hkdf.js');
|
|
8
|
+
var hybrid_js = require('@noble/post-quantum/hybrid.js');
|
|
9
|
+
var ed25519_js = require('@noble/curves/ed25519.js');
|
|
10
|
+
var cbor2 = require('cbor2');
|
|
11
|
+
var sorts = require('cbor2/sorts');
|
|
12
|
+
|
|
13
|
+
// src/sealed-poe/wrap.ts
|
|
14
|
+
|
|
15
|
+
// src/aead/errors.ts
|
|
16
|
+
var AeadVerificationError = class extends Error {
|
|
17
|
+
code = "aead_verification_failed";
|
|
18
|
+
constructor(message, options) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.name = "AeadVerificationError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/aead/chacha20-poly1305.ts
|
|
25
|
+
function chacha20Poly1305Encrypt(opts) {
|
|
26
|
+
return chacha_js.chacha20poly1305(opts.key, opts.nonce, opts.aad).encrypt(opts.plaintext);
|
|
27
|
+
}
|
|
28
|
+
function chacha20Poly1305Decrypt(opts) {
|
|
29
|
+
try {
|
|
30
|
+
return chacha_js.chacha20poly1305(opts.key, opts.nonce, opts.aad).decrypt(opts.ciphertext);
|
|
31
|
+
} catch (cause) {
|
|
32
|
+
throw new AeadVerificationError("chacha20-poly1305 decrypt failed", { cause });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function xchacha20Poly1305Encrypt(opts) {
|
|
36
|
+
return chacha_js.xchacha20poly1305(opts.key, opts.nonce, opts.aad).encrypt(opts.plaintext);
|
|
37
|
+
}
|
|
38
|
+
function xchacha20Poly1305Decrypt(opts) {
|
|
39
|
+
try {
|
|
40
|
+
return chacha_js.xchacha20poly1305(opts.key, opts.nonce, opts.aad).decrypt(opts.ciphertext);
|
|
41
|
+
} catch (cause) {
|
|
42
|
+
throw new AeadVerificationError("xchacha20-poly1305 decrypt failed", { cause });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function hkdfSha256(opts) {
|
|
46
|
+
return hkdf_js.hkdf(sha2_js.sha256, opts.ikm, opts.salt, opts.info, opts.length);
|
|
47
|
+
}
|
|
48
|
+
var MLKEM768X25519_PUBLIC_KEY_LENGTH = 1216;
|
|
49
|
+
var MLKEM768X25519_ENC_LENGTH = 1120;
|
|
50
|
+
var MLKEM768X25519_SEED_LENGTH = 32;
|
|
51
|
+
var MLKEM768X25519_ESEED_LENGTH = 64;
|
|
52
|
+
function mlkem768x25519Encapsulate(opts) {
|
|
53
|
+
if (opts.publicKey.length !== MLKEM768X25519_PUBLIC_KEY_LENGTH) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`mlkem768x25519 public key must be ${MLKEM768X25519_PUBLIC_KEY_LENGTH} bytes, got ${opts.publicKey.length}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (opts.eseed !== void 0 && opts.eseed.length !== MLKEM768X25519_ESEED_LENGTH) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`mlkem768x25519 eseed must be ${MLKEM768X25519_ESEED_LENGTH} bytes, got ${opts.eseed.length}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const { cipherText, sharedSecret } = hybrid_js.XWing.encapsulate(opts.publicKey, opts.eseed);
|
|
64
|
+
return { enc: cipherText, ss: sharedSecret };
|
|
65
|
+
}
|
|
66
|
+
function mlkem768x25519Decapsulate(opts) {
|
|
67
|
+
if (opts.secretSeed.length !== MLKEM768X25519_SEED_LENGTH) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`mlkem768x25519 secret seed must be ${MLKEM768X25519_SEED_LENGTH} bytes, got ${opts.secretSeed.length}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (opts.enc.length !== MLKEM768X25519_ENC_LENGTH) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`mlkem768x25519 enc must be ${MLKEM768X25519_ENC_LENGTH} bytes, got ${opts.enc.length}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return hybrid_js.XWing.decapsulate(opts.enc, opts.secretSeed);
|
|
78
|
+
}
|
|
79
|
+
var X25519LowOrderPointError = class extends Error {
|
|
80
|
+
code = "X25519_LOW_ORDER_POINT";
|
|
81
|
+
constructor(options) {
|
|
82
|
+
super("x25519 ECDH rejected: peer public key is a small-order point", options);
|
|
83
|
+
this.name = "X25519LowOrderPointError";
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var NOBLE_LOW_ORDER_MESSAGE = "invalid private or public key received";
|
|
87
|
+
function x25519PublicKey(opts) {
|
|
88
|
+
return ed25519_js.x25519.getPublicKey(opts.secretKey);
|
|
89
|
+
}
|
|
90
|
+
function x25519Ecdh(opts) {
|
|
91
|
+
try {
|
|
92
|
+
return ed25519_js.x25519.getSharedSecret(opts.secretKey, opts.theirPublicKey);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e instanceof Error && e.message === NOBLE_LOW_ORDER_MESSAGE) {
|
|
95
|
+
throw new X25519LowOrderPointError({ cause: e });
|
|
96
|
+
}
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/sealed-poe/errors.ts
|
|
102
|
+
var EciesSealedPoeError = class extends Error {
|
|
103
|
+
code;
|
|
104
|
+
constructor(code, message, options) {
|
|
105
|
+
super(message, options);
|
|
106
|
+
this.name = "EciesSealedPoeError";
|
|
107
|
+
this.code = code;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
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
|
+
// src/sealed-poe/slots-codec.ts
|
|
120
|
+
var CHUNK_MAX_BYTES = 64;
|
|
121
|
+
function chunkKemCt(value) {
|
|
122
|
+
if (value.length === 0) {
|
|
123
|
+
throw new Error("chunkKemCt: refusing to chunk an empty byte string");
|
|
124
|
+
}
|
|
125
|
+
const chunks = [];
|
|
126
|
+
for (let i = 0; i < value.length; i += CHUNK_MAX_BYTES) {
|
|
127
|
+
chunks.push(value.subarray(i, Math.min(i + CHUNK_MAX_BYTES, value.length)));
|
|
128
|
+
}
|
|
129
|
+
return chunks;
|
|
130
|
+
}
|
|
131
|
+
function joinKemCt(chunks) {
|
|
132
|
+
let total = 0;
|
|
133
|
+
for (const c of chunks) total += c.length;
|
|
134
|
+
const out = new Uint8Array(total);
|
|
135
|
+
let offset = 0;
|
|
136
|
+
for (const c of chunks) {
|
|
137
|
+
out.set(c, offset);
|
|
138
|
+
offset += c.length;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
function slotsToMacCbor(slots, kem) {
|
|
143
|
+
let value;
|
|
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);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/sealed-poe/wrap.ts
|
|
166
|
+
var CARDANO_POE_HKDF_INFO_KEK = new TextEncoder().encode("cardano-poe-kek-v1");
|
|
167
|
+
var CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 = new TextEncoder().encode(
|
|
168
|
+
"cardano-poe-kek-mlkem768x25519-v1"
|
|
169
|
+
);
|
|
170
|
+
var CARDANO_POE_HKDF_INFO_SLOTS_MAC = new TextEncoder().encode(
|
|
171
|
+
"cardano-poe-slots-mac-v1"
|
|
172
|
+
);
|
|
173
|
+
var ZERO_NONCE_12 = new Uint8Array(12);
|
|
174
|
+
var EMPTY_SALT = new Uint8Array(0);
|
|
175
|
+
var X25519_PUBLIC_KEY_LENGTH = 32;
|
|
176
|
+
var X25519_SECRET_KEY_LENGTH = 32;
|
|
177
|
+
var CEK_LENGTH = 32;
|
|
178
|
+
var NONCE_LENGTH = 24;
|
|
179
|
+
var WRAP_LENGTH = 48;
|
|
180
|
+
var SLOTS_MAC_LENGTH = 32;
|
|
181
|
+
if (CARDANO_POE_HKDF_INFO_KEK.length !== 18) {
|
|
182
|
+
throw new Error("CARDANO_POE_HKDF_INFO_KEK byte-length invariant violated (expected 18)");
|
|
183
|
+
}
|
|
184
|
+
if (CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519.length !== 33) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 byte-length invariant violated (expected 33)"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (CARDANO_POE_HKDF_INFO_SLOTS_MAC.length !== 24) {
|
|
190
|
+
throw new Error("CARDANO_POE_HKDF_INFO_SLOTS_MAC byte-length invariant violated (expected 24)");
|
|
191
|
+
}
|
|
192
|
+
if (ZERO_NONCE_12.length !== 12) {
|
|
193
|
+
throw new Error("ZERO_NONCE_12 byte-length invariant violated (expected 12)");
|
|
194
|
+
}
|
|
195
|
+
function concat(a, b) {
|
|
196
|
+
const out = new Uint8Array(a.length + b.length);
|
|
197
|
+
out.set(a, 0);
|
|
198
|
+
out.set(b, a.length);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function uniformIndexBelow(m) {
|
|
202
|
+
const limit = 4294967296 - 4294967296 % m;
|
|
203
|
+
const buf = new Uint32Array(1);
|
|
204
|
+
let x;
|
|
205
|
+
do {
|
|
206
|
+
crypto.getRandomValues(buf);
|
|
207
|
+
x = buf[0];
|
|
208
|
+
} while (x >= limit);
|
|
209
|
+
return x % m;
|
|
210
|
+
}
|
|
211
|
+
function csprngShuffle(arr) {
|
|
212
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
213
|
+
const j = uniformIndexBelow(i + 1);
|
|
214
|
+
const tmp = arr[i];
|
|
215
|
+
arr[i] = arr[j];
|
|
216
|
+
arr[j] = tmp;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function wrapSlotX25519(args) {
|
|
220
|
+
const privEph = args.privEph ?? utils_js.randomBytes(X25519_SECRET_KEY_LENGTH);
|
|
221
|
+
if (privEph.length !== X25519_SECRET_KEY_LENGTH) {
|
|
222
|
+
throw new EciesSealedPoeError(
|
|
223
|
+
"INVALID_EPHEMERAL_SECRET_LENGTH",
|
|
224
|
+
`ephemeralSecrets[${args.slotIdx}] MUST be exactly ${X25519_SECRET_KEY_LENGTH} bytes, got ${privEph.length}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const epk = x25519PublicKey({ secretKey: privEph });
|
|
228
|
+
const shared = x25519Ecdh({ secretKey: privEph, theirPublicKey: args.pubR });
|
|
229
|
+
const kek = hkdfSha256({
|
|
230
|
+
ikm: shared,
|
|
231
|
+
salt: concat(epk, args.pubR),
|
|
232
|
+
info: CARDANO_POE_HKDF_INFO_KEK,
|
|
233
|
+
length: 32
|
|
234
|
+
});
|
|
235
|
+
const wrap = chacha20Poly1305Encrypt({
|
|
236
|
+
key: kek,
|
|
237
|
+
nonce: ZERO_NONCE_12,
|
|
238
|
+
aad: CARDANO_POE_HKDF_INFO_KEK,
|
|
239
|
+
plaintext: args.cek
|
|
240
|
+
});
|
|
241
|
+
if (wrap.length !== WRAP_LENGTH) {
|
|
242
|
+
throw new Error(`internal: wrap.length=${wrap.length}, expected ${WRAP_LENGTH}`);
|
|
243
|
+
}
|
|
244
|
+
return { epk, wrap };
|
|
245
|
+
}
|
|
246
|
+
function wrapSlotMlkem768X25519(args) {
|
|
247
|
+
const { enc, ss } = mlkem768x25519Encapsulate({
|
|
248
|
+
publicKey: args.pubR,
|
|
249
|
+
...args.eseed !== void 0 ? { eseed: args.eseed } : {}
|
|
250
|
+
});
|
|
251
|
+
if (enc.length !== MLKEM768X25519_ENC_LENGTH) {
|
|
252
|
+
throw new Error(`internal: enc.length=${enc.length}, expected ${MLKEM768X25519_ENC_LENGTH}`);
|
|
253
|
+
}
|
|
254
|
+
const kek = hkdfSha256({
|
|
255
|
+
ikm: ss,
|
|
256
|
+
salt: EMPTY_SALT,
|
|
257
|
+
info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
|
|
258
|
+
length: 32
|
|
259
|
+
});
|
|
260
|
+
const wrap = chacha20Poly1305Encrypt({
|
|
261
|
+
key: kek,
|
|
262
|
+
nonce: ZERO_NONCE_12,
|
|
263
|
+
aad: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
|
|
264
|
+
plaintext: args.cek
|
|
265
|
+
});
|
|
266
|
+
if (wrap.length !== WRAP_LENGTH) {
|
|
267
|
+
throw new Error(`internal: wrap.length=${wrap.length}, expected ${WRAP_LENGTH}`);
|
|
268
|
+
}
|
|
269
|
+
return { kem_ct: chunkKemCt(enc), wrap };
|
|
270
|
+
}
|
|
271
|
+
function eciesSealedPoeWrap(args) {
|
|
272
|
+
const { plaintext, recipientPublicKeys } = args;
|
|
273
|
+
const kem = args.kem ?? "x25519";
|
|
274
|
+
const n = recipientPublicKeys.length;
|
|
275
|
+
if (n < 1) {
|
|
276
|
+
throw new EciesSealedPoeError(
|
|
277
|
+
"ENC_SLOTS_EMPTY",
|
|
278
|
+
`recipientPublicKeys.length=${n} must be >= 1`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
const expectedPubLen = kem === "x25519" ? X25519_PUBLIC_KEY_LENGTH : MLKEM768X25519_PUBLIC_KEY_LENGTH;
|
|
282
|
+
for (let i = 0; i < n; i++) {
|
|
283
|
+
const pub = recipientPublicKeys[i];
|
|
284
|
+
if (pub === void 0 || pub.length !== expectedPubLen) {
|
|
285
|
+
throw new EciesSealedPoeError(
|
|
286
|
+
"KEM_EPK_LENGTH_MISMATCH",
|
|
287
|
+
`recipientPublicKeys[${i}] MUST be exactly ${expectedPubLen} bytes for kem='${kem}'`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (kem === "x25519") {
|
|
292
|
+
if (args.eseeds !== void 0) {
|
|
293
|
+
throw new EciesSealedPoeError(
|
|
294
|
+
"EPHEMERAL_SECRETS_COUNT_MISMATCH",
|
|
295
|
+
"eseeds is an X-Wing (mlkem768x25519) override and MUST NOT be supplied for kem='x25519'"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
if (args.ephemeralSecrets !== void 0 && args.ephemeralSecrets.length !== n) {
|
|
299
|
+
throw new EciesSealedPoeError(
|
|
300
|
+
"EPHEMERAL_SECRETS_COUNT_MISMATCH",
|
|
301
|
+
`ephemeralSecrets.length=${args.ephemeralSecrets.length} must match recipientPublicKeys.length=${n}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
if (args.ephemeralSecrets !== void 0) {
|
|
306
|
+
throw new EciesSealedPoeError(
|
|
307
|
+
"EPHEMERAL_SECRETS_COUNT_MISMATCH",
|
|
308
|
+
"ephemeralSecrets is an X25519 override and MUST NOT be supplied for kem='mlkem768x25519'"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (args.eseeds !== void 0) {
|
|
312
|
+
if (args.eseeds.length !== n) {
|
|
313
|
+
throw new EciesSealedPoeError(
|
|
314
|
+
"EPHEMERAL_SECRETS_COUNT_MISMATCH",
|
|
315
|
+
`eseeds.length=${args.eseeds.length} must match recipientPublicKeys.length=${n}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
for (let i = 0; i < n; i++) {
|
|
319
|
+
const eseed = args.eseeds[i];
|
|
320
|
+
if (eseed.length !== MLKEM768X25519_ESEED_LENGTH) {
|
|
321
|
+
throw new EciesSealedPoeError(
|
|
322
|
+
"INVALID_EPHEMERAL_SECRET_LENGTH",
|
|
323
|
+
`eseeds[${i}] MUST be exactly ${MLKEM768X25519_ESEED_LENGTH} bytes, got ${eseed.length}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const cek = args.cek ?? utils_js.randomBytes(CEK_LENGTH);
|
|
330
|
+
const nonce = args.nonce ?? utils_js.randomBytes(NONCE_LENGTH);
|
|
331
|
+
if (cek.length !== CEK_LENGTH) {
|
|
332
|
+
throw new EciesSealedPoeError(
|
|
333
|
+
"INVALID_CEK_LENGTH",
|
|
334
|
+
`cek MUST be exactly ${CEK_LENGTH} bytes, got ${cek.length}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (nonce.length !== NONCE_LENGTH) {
|
|
338
|
+
throw new EciesSealedPoeError(
|
|
339
|
+
"NONCE_LENGTH_MISMATCH",
|
|
340
|
+
`nonce MUST be exactly ${NONCE_LENGTH} bytes, got ${nonce.length}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
let envelope;
|
|
344
|
+
if (kem === "x25519") {
|
|
345
|
+
const slots = [];
|
|
346
|
+
for (let i = 0; i < n; i++) {
|
|
347
|
+
slots.push(
|
|
348
|
+
wrapSlotX25519({
|
|
349
|
+
pubR: recipientPublicKeys[i],
|
|
350
|
+
privEph: args.ephemeralSecrets ? args.ephemeralSecrets[i] : void 0,
|
|
351
|
+
cek,
|
|
352
|
+
slotIdx: i
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (args.skipShuffle !== true) {
|
|
357
|
+
csprngShuffle(slots);
|
|
358
|
+
}
|
|
359
|
+
const slotsMac = computeSlotsMac(cek, slots, "x25519");
|
|
360
|
+
envelope = {
|
|
361
|
+
scheme: 1,
|
|
362
|
+
aead: "xchacha20-poly1305",
|
|
363
|
+
kem: "x25519",
|
|
364
|
+
nonce,
|
|
365
|
+
slots,
|
|
366
|
+
slots_mac: slotsMac
|
|
367
|
+
};
|
|
368
|
+
} else {
|
|
369
|
+
const slots = [];
|
|
370
|
+
for (let i = 0; i < n; i++) {
|
|
371
|
+
slots.push(
|
|
372
|
+
wrapSlotMlkem768X25519({
|
|
373
|
+
pubR: recipientPublicKeys[i],
|
|
374
|
+
eseed: args.eseeds ? args.eseeds[i] : void 0,
|
|
375
|
+
cek
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
if (args.skipShuffle !== true) {
|
|
380
|
+
csprngShuffle(slots);
|
|
381
|
+
}
|
|
382
|
+
const slotsMac = computeSlotsMac(cek, slots, "mlkem768x25519");
|
|
383
|
+
envelope = {
|
|
384
|
+
scheme: 1,
|
|
385
|
+
aead: "xchacha20-poly1305",
|
|
386
|
+
kem: "mlkem768x25519",
|
|
387
|
+
nonce,
|
|
388
|
+
slots,
|
|
389
|
+
slots_mac: slotsMac
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const adContent = concat(nonce, envelope.slots_mac);
|
|
393
|
+
const ciphertext = xchacha20Poly1305Encrypt({
|
|
394
|
+
key: cek,
|
|
395
|
+
nonce,
|
|
396
|
+
aad: adContent,
|
|
397
|
+
plaintext
|
|
398
|
+
});
|
|
399
|
+
return { envelope, ciphertext };
|
|
400
|
+
}
|
|
401
|
+
function computeSlotsMac(cek, slots, kem) {
|
|
402
|
+
const hmacKey = hkdfSha256({
|
|
403
|
+
ikm: cek,
|
|
404
|
+
salt: EMPTY_SALT,
|
|
405
|
+
info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
|
|
406
|
+
length: 32
|
|
407
|
+
});
|
|
408
|
+
const slotsCbor = slotsToMacCbor(slots, kem);
|
|
409
|
+
const slotsMac = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
|
|
410
|
+
if (slotsMac.length !== SLOTS_MAC_LENGTH) {
|
|
411
|
+
throw new Error(`internal: slots_mac.length=${slotsMac.length}, expected ${SLOTS_MAC_LENGTH}`);
|
|
412
|
+
}
|
|
413
|
+
return slotsMac;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/util/compare-ct.ts
|
|
417
|
+
function compareCt(a, b) {
|
|
418
|
+
if (a.length !== b.length) return false;
|
|
419
|
+
let diff = 0;
|
|
420
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
421
|
+
return diff === 0;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/sealed-poe/unwrap.ts
|
|
425
|
+
function selectBundleSecrets(envelope, bundle) {
|
|
426
|
+
return envelope.kem === "x25519" ? bundle.x25519PrivateKeys : bundle.mlkem768x25519SecretSeeds;
|
|
427
|
+
}
|
|
428
|
+
var ZERO_NONCE_122 = new Uint8Array(12);
|
|
429
|
+
var EMPTY_SALT2 = new Uint8Array(0);
|
|
430
|
+
var X25519_SECRET_KEY_LENGTH2 = 32;
|
|
431
|
+
var X25519_PUBLIC_KEY_LENGTH2 = 32;
|
|
432
|
+
var NONCE_LENGTH2 = 24;
|
|
433
|
+
var WRAP_LENGTH2 = 48;
|
|
434
|
+
var SLOTS_MAC_LENGTH2 = 32;
|
|
435
|
+
function concat2(a, b) {
|
|
436
|
+
const out = new Uint8Array(a.length + b.length);
|
|
437
|
+
out.set(a, 0);
|
|
438
|
+
out.set(b, a.length);
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
|
|
442
|
+
if (envelope.scheme !== 1) {
|
|
443
|
+
throw new EciesSealedPoeError(
|
|
444
|
+
"UNSUPPORTED_ENC_VERSION",
|
|
445
|
+
`envelope.scheme=${String(envelope.scheme)} unsupported (expected 1)`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (envelope.aead !== "xchacha20-poly1305") {
|
|
449
|
+
throw new EciesSealedPoeError(
|
|
450
|
+
"UNSUPPORTED_AEAD_ALG",
|
|
451
|
+
`envelope.aead=${String(envelope.aead)} unsupported (expected 'xchacha20-poly1305')`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (envelope.kem !== "x25519" && envelope.kem !== "mlkem768x25519") {
|
|
455
|
+
throw new EciesSealedPoeError(
|
|
456
|
+
"UNSUPPORTED_KEM_ALG",
|
|
457
|
+
`envelope.kem=${String(envelope.kem)} unsupported (expected 'x25519' or 'mlkem768x25519')`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const n = envelope.slots.length;
|
|
461
|
+
if (n < 1) {
|
|
462
|
+
throw new EciesSealedPoeError("ENC_SLOTS_EMPTY", `envelope.slots.length=${n} must be >= 1`);
|
|
463
|
+
}
|
|
464
|
+
if (envelope.nonce.length !== NONCE_LENGTH2) {
|
|
465
|
+
throw new EciesSealedPoeError(
|
|
466
|
+
"NONCE_LENGTH_MISMATCH",
|
|
467
|
+
`envelope.nonce MUST be exactly ${NONCE_LENGTH2} bytes, got ${envelope.nonce.length}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (envelope.slots_mac.length !== SLOTS_MAC_LENGTH2) {
|
|
471
|
+
throw new EciesSealedPoeError(
|
|
472
|
+
"ENC_SLOTS_MAC_INVALID_LENGTH",
|
|
473
|
+
`envelope.slots_mac MUST be exactly ${SLOTS_MAC_LENGTH2} bytes, got ${envelope.slots_mac.length}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
if (envelope.kem === "x25519") {
|
|
477
|
+
for (let i = 0; i < n; i++) {
|
|
478
|
+
const slot = envelope.slots[i];
|
|
479
|
+
if (slot.epk.length !== X25519_PUBLIC_KEY_LENGTH2) {
|
|
480
|
+
throw new EciesSealedPoeError(
|
|
481
|
+
"KEM_EPK_LENGTH_MISMATCH",
|
|
482
|
+
`envelope.slots[${i}].epk MUST be exactly ${X25519_PUBLIC_KEY_LENGTH2} bytes, got ${slot.epk.length}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
if (slot.wrap.length !== WRAP_LENGTH2) {
|
|
486
|
+
throw new EciesSealedPoeError(
|
|
487
|
+
"WRAP_LENGTH_MISMATCH",
|
|
488
|
+
`envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
for (let i = 0; i < n; i++) {
|
|
494
|
+
const slot = envelope.slots[i];
|
|
495
|
+
const enc = joinKemCt(slot.kem_ct);
|
|
496
|
+
if (enc.length !== MLKEM768X25519_ENC_LENGTH) {
|
|
497
|
+
throw new EciesSealedPoeError(
|
|
498
|
+
"KEM_CT_LENGTH_MISMATCH",
|
|
499
|
+
`envelope.slots[${i}].kem_ct MUST reassemble to exactly ${MLKEM768X25519_ENC_LENGTH} bytes, got ${enc.length}`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (slot.wrap.length !== WRAP_LENGTH2) {
|
|
503
|
+
throw new EciesSealedPoeError(
|
|
504
|
+
"WRAP_LENGTH_MISMATCH",
|
|
505
|
+
`envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (multiPrivKeys !== void 0) {
|
|
511
|
+
for (let i = 0; i < multiPrivKeys.length; i++) {
|
|
512
|
+
if (multiPrivKeys[i].length !== X25519_SECRET_KEY_LENGTH2) {
|
|
513
|
+
throw new EciesSealedPoeError(
|
|
514
|
+
"INVALID_RECIPIENT_KEY",
|
|
515
|
+
`recipientSecretKeys[${i}] MUST be exactly ${X25519_SECRET_KEY_LENGTH2} bytes, got ${multiPrivKeys[i].length}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} else if (singlePrivKey !== void 0) {
|
|
520
|
+
if (singlePrivKey.length !== X25519_SECRET_KEY_LENGTH2) {
|
|
521
|
+
throw new EciesSealedPoeError(
|
|
522
|
+
"INVALID_RECIPIENT_KEY",
|
|
523
|
+
`recipientSecretKey MUST be exactly ${X25519_SECRET_KEY_LENGTH2} bytes, got ${singlePrivKey.length}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
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
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
const shared = x25519Ecdh({
|
|
556
|
+
secretKey: args.recipientSecretKey,
|
|
557
|
+
theirPublicKey: args.slot.epk
|
|
558
|
+
});
|
|
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
|
+
} catch (e) {
|
|
566
|
+
if (!(e instanceof X25519LowOrderPointError)) throw e;
|
|
567
|
+
}
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
function tryMlkem768X25519Slot(args) {
|
|
571
|
+
const enc = joinKemCt(args.slot.kem_ct);
|
|
572
|
+
const ss = mlkem768x25519Decapsulate({ secretSeed: args.recipientSecretKey, enc });
|
|
573
|
+
const kek = hkdfSha256({
|
|
574
|
+
ikm: ss,
|
|
575
|
+
salt: EMPTY_SALT2,
|
|
576
|
+
info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
|
|
577
|
+
length: 32
|
|
578
|
+
});
|
|
579
|
+
if (!args.liveSlot) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
return chacha20Poly1305Decrypt({
|
|
584
|
+
key: kek,
|
|
585
|
+
nonce: ZERO_NONCE_122,
|
|
586
|
+
aad: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
|
|
587
|
+
ciphertext: args.slot.wrap
|
|
588
|
+
});
|
|
589
|
+
} catch (e) {
|
|
590
|
+
if (!(e instanceof AeadVerificationError)) throw e;
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut) {
|
|
595
|
+
const n = envelope.slots.length;
|
|
596
|
+
let cek = null;
|
|
597
|
+
let matchedSlotIdx = -1;
|
|
598
|
+
if (envelope.kem === "x25519") {
|
|
599
|
+
const pubRLocal = x25519PublicKey({ secretKey: recipientSecretKey });
|
|
600
|
+
for (let i = 0; i < n; i++) {
|
|
601
|
+
if (slotsAttemptedOut !== void 0) {
|
|
602
|
+
slotsAttemptedOut.count = i + 1;
|
|
603
|
+
}
|
|
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
|
+
}
|
|
614
|
+
if (cek !== null && !constantTimeN) break;
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
for (let i = 0; i < n; i++) {
|
|
618
|
+
if (slotsAttemptedOut !== void 0) {
|
|
619
|
+
slotsAttemptedOut.count = i + 1;
|
|
620
|
+
}
|
|
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
|
+
}
|
|
630
|
+
if (cek !== null && !constantTimeN) break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
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;
|
|
637
|
+
}
|
|
638
|
+
function slotsMacCborBytes(envelope) {
|
|
639
|
+
return slotsToMacCbor(
|
|
640
|
+
envelope.slots,
|
|
641
|
+
envelope.kem
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
function eciesSealedPoeUnwrap(args) {
|
|
645
|
+
const { envelope, ciphertext } = args;
|
|
646
|
+
const constantTimeN = args.constantTimeN ?? true;
|
|
647
|
+
const hasSingle = "recipientSecretKey" in args;
|
|
648
|
+
const hasBundle = "recipientKeyBundle" in args;
|
|
649
|
+
const multiPrivKeys = hasBundle ? selectBundleSecrets(envelope, args.recipientKeyBundle) : "recipientSecretKeys" in args ? args.recipientSecretKeys : void 0;
|
|
650
|
+
const hasMulti = multiPrivKeys !== void 0;
|
|
651
|
+
if (hasSingle === hasMulti) {
|
|
652
|
+
throw new EciesSealedPoeError(
|
|
653
|
+
"INVALID_RECIPIENT_KEY",
|
|
654
|
+
"exactly one of recipientSecretKey / recipientSecretKeys / recipientKeyBundle MUST be supplied"
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (hasMulti && multiPrivKeys.length === 0) {
|
|
658
|
+
if (hasBundle) {
|
|
659
|
+
return { matched: false, reason: "WRONG_RECIPIENT_KEY" };
|
|
660
|
+
}
|
|
661
|
+
throw new EciesSealedPoeError(
|
|
662
|
+
"INVALID_RECIPIENT_KEY",
|
|
663
|
+
"recipientSecretKeys MUST be a non-empty array, got length=0"
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (hasMulti) {
|
|
667
|
+
assertEnvelopeStructure(envelope, multiPrivKeys, void 0);
|
|
668
|
+
} else {
|
|
669
|
+
assertEnvelopeStructure(envelope, void 0, args.recipientSecretKey);
|
|
670
|
+
}
|
|
671
|
+
let matchedCek = null;
|
|
672
|
+
let anyCandidateRecovered = false;
|
|
673
|
+
if (hasSingle) {
|
|
674
|
+
const recipientSecretKey = args.recipientSecretKey;
|
|
675
|
+
const cek = tryRecipientUnwrap(
|
|
676
|
+
envelope,
|
|
677
|
+
recipientSecretKey,
|
|
678
|
+
constantTimeN,
|
|
679
|
+
args._slotsAttemptedOut
|
|
680
|
+
);
|
|
681
|
+
if (cek === null) {
|
|
682
|
+
return { matched: false, reason: "WRONG_RECIPIENT_KEY" };
|
|
683
|
+
}
|
|
684
|
+
const slotsCbor = slotsMacCborBytes(envelope);
|
|
685
|
+
const hmacKey = hkdfSha256({
|
|
686
|
+
ikm: cek,
|
|
687
|
+
salt: EMPTY_SALT2,
|
|
688
|
+
info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
|
|
689
|
+
length: 32
|
|
690
|
+
});
|
|
691
|
+
const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
|
|
692
|
+
if (!compareCt(slotsMacCalc, envelope.slots_mac)) {
|
|
693
|
+
return { matched: false, reason: "TAMPERED_HEADER" };
|
|
694
|
+
}
|
|
695
|
+
matchedCek = cek;
|
|
696
|
+
} else {
|
|
697
|
+
const slotsCbor = slotsMacCborBytes(envelope);
|
|
698
|
+
const recipientSecretKeys = multiPrivKeys;
|
|
699
|
+
for (let k = 0; k < recipientSecretKeys.length; k++) {
|
|
700
|
+
if (args._privsAttemptedOut !== void 0) {
|
|
701
|
+
args._privsAttemptedOut.count = k + 1;
|
|
702
|
+
}
|
|
703
|
+
if (args._slotsAttemptedOut !== void 0) {
|
|
704
|
+
args._slotsAttemptedOut.count = 0;
|
|
705
|
+
}
|
|
706
|
+
const cek = tryRecipientUnwrap(
|
|
707
|
+
envelope,
|
|
708
|
+
recipientSecretKeys[k],
|
|
709
|
+
constantTimeN,
|
|
710
|
+
args._slotsAttemptedOut
|
|
711
|
+
);
|
|
712
|
+
if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
|
|
713
|
+
args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
|
|
714
|
+
}
|
|
715
|
+
if (cek === null) continue;
|
|
716
|
+
const hmacKey = hkdfSha256({
|
|
717
|
+
ikm: cek,
|
|
718
|
+
salt: EMPTY_SALT2,
|
|
719
|
+
info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
|
|
720
|
+
length: 32
|
|
721
|
+
});
|
|
722
|
+
const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
|
|
723
|
+
if (compareCt(slotsMacCalc, envelope.slots_mac)) {
|
|
724
|
+
matchedCek = cek;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
anyCandidateRecovered = true;
|
|
728
|
+
}
|
|
729
|
+
if (matchedCek === null) {
|
|
730
|
+
return {
|
|
731
|
+
matched: false,
|
|
732
|
+
reason: anyCandidateRecovered ? "TAMPERED_HEADER" : "WRONG_RECIPIENT_KEY"
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const adContent = concat2(envelope.nonce, envelope.slots_mac);
|
|
737
|
+
try {
|
|
738
|
+
const plaintext = xchacha20Poly1305Decrypt({
|
|
739
|
+
key: matchedCek,
|
|
740
|
+
nonce: envelope.nonce,
|
|
741
|
+
aad: adContent,
|
|
742
|
+
ciphertext
|
|
743
|
+
});
|
|
744
|
+
return { matched: true, plaintext };
|
|
745
|
+
} catch (e) {
|
|
746
|
+
if (!(e instanceof AeadVerificationError)) throw e;
|
|
747
|
+
return { matched: false, reason: "TAMPERED_CIPHERTEXT" };
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function eciesSealedPoeTrialDecrypt(args) {
|
|
751
|
+
const { envelope } = args;
|
|
752
|
+
const constantTimeN = args.constantTimeN ?? true;
|
|
753
|
+
const hasBundle = "recipientKeyBundle" in args;
|
|
754
|
+
const recipientSecretKeys = hasBundle ? selectBundleSecrets(envelope, args.recipientKeyBundle) : args.recipientSecretKeys;
|
|
755
|
+
if (recipientSecretKeys.length === 0) {
|
|
756
|
+
if (hasBundle) {
|
|
757
|
+
return { kind: "no_aead_pass" };
|
|
758
|
+
}
|
|
759
|
+
throw new EciesSealedPoeError(
|
|
760
|
+
"INVALID_RECIPIENT_KEY",
|
|
761
|
+
"recipientSecretKeys MUST be a non-empty array, got length=0"
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
assertEnvelopeStructure(envelope, recipientSecretKeys, void 0);
|
|
765
|
+
const slotsCbor = slotsMacCborBytes(envelope);
|
|
766
|
+
let anyCandidateRecovered = false;
|
|
767
|
+
for (let k = 0; k < recipientSecretKeys.length; k++) {
|
|
768
|
+
if (args._privsAttemptedOut !== void 0) {
|
|
769
|
+
args._privsAttemptedOut.count = k + 1;
|
|
770
|
+
}
|
|
771
|
+
if (args._slotsAttemptedOut !== void 0) {
|
|
772
|
+
args._slotsAttemptedOut.count = 0;
|
|
773
|
+
}
|
|
774
|
+
const candidate = tryRecipientUnwrapWithIdx(
|
|
775
|
+
envelope,
|
|
776
|
+
recipientSecretKeys[k],
|
|
777
|
+
constantTimeN,
|
|
778
|
+
args._slotsAttemptedOut
|
|
779
|
+
);
|
|
780
|
+
if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
|
|
781
|
+
args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
|
|
782
|
+
}
|
|
783
|
+
if (candidate === null) continue;
|
|
784
|
+
const hmacKey = hkdfSha256({
|
|
785
|
+
ikm: candidate.cek,
|
|
786
|
+
salt: EMPTY_SALT2,
|
|
787
|
+
info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
|
|
788
|
+
length: 32
|
|
789
|
+
});
|
|
790
|
+
const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
|
|
791
|
+
if (compareCt(slotsMacCalc, envelope.slots_mac)) {
|
|
792
|
+
return { kind: "match", slotIdx: candidate.slotIdx, cek: candidate.cek };
|
|
793
|
+
}
|
|
794
|
+
anyCandidateRecovered = true;
|
|
795
|
+
}
|
|
796
|
+
return anyCandidateRecovered ? { kind: "aead_pass_no_mac_match" } : { kind: "no_aead_pass" };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/sealed-poe/envelope-from-parsed.ts
|
|
800
|
+
function sealedEnvelopeFromParsed(enc) {
|
|
801
|
+
if (enc.scheme !== 1 || enc.aead !== "xchacha20-poly1305") return null;
|
|
802
|
+
if (enc.nonce === void 0 || enc.slots_mac === void 0) return null;
|
|
803
|
+
const slots = enc.slots;
|
|
804
|
+
if (slots === void 0 || slots.length < 1) return null;
|
|
805
|
+
if (enc.kem === "x25519") {
|
|
806
|
+
const x25519Slots = [];
|
|
807
|
+
for (const s of slots) {
|
|
808
|
+
if (s.epk === void 0 || s.wrap === void 0) return null;
|
|
809
|
+
x25519Slots.push({ epk: s.epk, wrap: s.wrap });
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
scheme: 1,
|
|
813
|
+
aead: "xchacha20-poly1305",
|
|
814
|
+
kem: "x25519",
|
|
815
|
+
nonce: enc.nonce,
|
|
816
|
+
slots: x25519Slots,
|
|
817
|
+
slots_mac: enc.slots_mac
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (enc.kem === "mlkem768x25519") {
|
|
821
|
+
const hybridSlots = [];
|
|
822
|
+
for (const s of slots) {
|
|
823
|
+
if (s.kem_ct === void 0 || s.wrap === void 0) return null;
|
|
824
|
+
hybridSlots.push({ kem_ct: s.kem_ct, wrap: s.wrap });
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
scheme: 1,
|
|
828
|
+
aead: "xchacha20-poly1305",
|
|
829
|
+
kem: "mlkem768x25519",
|
|
830
|
+
nonce: enc.nonce,
|
|
831
|
+
slots: hybridSlots,
|
|
832
|
+
slots_mac: enc.slots_mac
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
exports.CARDANO_POE_HKDF_INFO_KEK = CARDANO_POE_HKDF_INFO_KEK;
|
|
839
|
+
exports.CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 = CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519;
|
|
840
|
+
exports.CARDANO_POE_HKDF_INFO_SLOTS_MAC = CARDANO_POE_HKDF_INFO_SLOTS_MAC;
|
|
841
|
+
exports.EciesSealedPoeError = EciesSealedPoeError;
|
|
842
|
+
exports.chunkKemCt = chunkKemCt;
|
|
843
|
+
exports.eciesSealedPoeTrialDecrypt = eciesSealedPoeTrialDecrypt;
|
|
844
|
+
exports.eciesSealedPoeUnwrap = eciesSealedPoeUnwrap;
|
|
845
|
+
exports.eciesSealedPoeWrap = eciesSealedPoeWrap;
|
|
846
|
+
exports.joinKemCt = joinKemCt;
|
|
847
|
+
exports.sealedEnvelopeFromParsed = sealedEnvelopeFromParsed;
|
|
848
|
+
exports.slotsToMacCbor = slotsToMacCbor;
|
|
849
|
+
exports.uniformIndexBelow = uniformIndexBelow;
|
|
850
|
+
//# sourceMappingURL=sealed-poe.cjs.map
|
|
851
|
+
//# sourceMappingURL=sealed-poe.cjs.map
|