cpace-ts 0.1.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/dist/index.js ADDED
@@ -0,0 +1,759 @@
1
+ // src/cpace-errors.ts
2
+ var InvalidPeerElementError = class extends Error {
3
+ constructor(message = "CPaceSession: invalid peer element", options) {
4
+ super(message, options);
5
+ this.name = "InvalidPeerElementError";
6
+ }
7
+ };
8
+
9
+ // src/bytes.ts
10
+ function compareBytes(a, b) {
11
+ const len = Math.min(a.length, b.length);
12
+ for (let i = 0; i < len; i += 1) {
13
+ const ai = a[i] ?? 0;
14
+ const bi = b[i] ?? 0;
15
+ if (ai !== bi) return ai - bi;
16
+ }
17
+ return a.length - b.length;
18
+ }
19
+
20
+ // src/cpace-strings.ts
21
+ var textEncoder = new TextEncoder();
22
+ function utf8(value) {
23
+ return textEncoder.encode(value);
24
+ }
25
+ function leb128Encode(n) {
26
+ if (!Number.isSafeInteger(n) || n < 0) {
27
+ throw new RangeError("leb128Encode: n must be a non-negative safe integer");
28
+ }
29
+ const bytes = [];
30
+ let v = n;
31
+ while (true) {
32
+ let byte = v & 127;
33
+ v = Math.floor(v / 128);
34
+ if (v !== 0) byte |= 128;
35
+ bytes.push(byte);
36
+ if (v === 0) break;
37
+ }
38
+ return new Uint8Array(bytes);
39
+ }
40
+ function prependLen(data) {
41
+ const lenEnc = leb128Encode(data.length);
42
+ const out = new Uint8Array(lenEnc.length + data.length);
43
+ out.set(lenEnc, 0);
44
+ out.set(data, lenEnc.length);
45
+ return out;
46
+ }
47
+ function lvCat(...parts) {
48
+ let total = 0;
49
+ const prepped = [];
50
+ for (const p of parts) {
51
+ const withLen = prependLen(p);
52
+ prepped.push(withLen);
53
+ total += withLen.length;
54
+ }
55
+ const out = new Uint8Array(total);
56
+ let off = 0;
57
+ for (const w of prepped) {
58
+ out.set(w, off);
59
+ off += w.length;
60
+ }
61
+ return out;
62
+ }
63
+ function zeroBytes(n) {
64
+ return new Uint8Array(n);
65
+ }
66
+ function generatorString(dsi, prs, ci, sid, sInBytes) {
67
+ const prsPl = prependLen(prs);
68
+ const dsiPl = prependLen(dsi);
69
+ const lenZpad = Math.max(0, sInBytes - 1 - prsPl.length - dsiPl.length);
70
+ const zpad = zeroBytes(lenZpad);
71
+ return lvCat(
72
+ dsi,
73
+ prs,
74
+ zpad,
75
+ ci ?? new Uint8Array(0),
76
+ sid ?? new Uint8Array(0)
77
+ );
78
+ }
79
+ function lexicographicallyLarger(bytes1, bytes2) {
80
+ const minLen = Math.min(bytes1.length, bytes2.length);
81
+ for (let i = 0; i < minLen; i += 1) {
82
+ const b1 = bytes1[i];
83
+ const b2 = bytes2[i];
84
+ if (b1 > b2) {
85
+ return true;
86
+ } else if (b1 < b2) {
87
+ return false;
88
+ }
89
+ }
90
+ return bytes1.length > bytes2.length;
91
+ }
92
+ function oCat(bytes1, bytes2) {
93
+ if (lexicographicallyLarger(bytes1, bytes2)) {
94
+ return concat([utf8("oc"), bytes1, bytes2]);
95
+ }
96
+ return concat([utf8("oc"), bytes2, bytes1]);
97
+ }
98
+ function transcriptIr(ya, ada, yb, adb) {
99
+ const left = lvCat(ya, ada);
100
+ const right = lvCat(yb, adb);
101
+ return concat([left, right]);
102
+ }
103
+ function transcriptOc(ya, ada, yb, adb) {
104
+ const left = lvCat(ya, ada);
105
+ const right = lvCat(yb, adb);
106
+ return oCat(left, right);
107
+ }
108
+ function concat(chunks) {
109
+ let total = 0;
110
+ for (const c of chunks) total += c.length;
111
+ const out = new Uint8Array(total);
112
+ let off = 0;
113
+ for (const c of chunks) {
114
+ out.set(c, off);
115
+ off += c.length;
116
+ }
117
+ return out;
118
+ }
119
+
120
+ // src/elligator2-curve25519.ts
121
+ import init, {
122
+ elligator2_curve25519_u,
123
+ initSync
124
+ } from "../wasm/pkg/cpace_wasm.js";
125
+ var ready = null;
126
+ var inited = false;
127
+ async function initOnce() {
128
+ const wasmUrl = new URL("../wasm/pkg/cpace_wasm_bg.wasm", import.meta.url);
129
+ if (wasmUrl.protocol === "file:") {
130
+ const { readFile } = await import("fs/promises");
131
+ const bytes = await readFile(wasmUrl);
132
+ initSync({ module: bytes });
133
+ return;
134
+ }
135
+ const resp = await fetch(wasmUrl);
136
+ if (!resp.ok)
137
+ throw new Error(`Failed to load WASM: ${resp.status} ${resp.statusText}`);
138
+ await init(resp);
139
+ }
140
+ function initElligator2Wasm() {
141
+ if (inited) return Promise.resolve();
142
+ if (!ready) {
143
+ ready = initOnce().then(() => {
144
+ inited = true;
145
+ }).catch((e) => {
146
+ ready = null;
147
+ inited = false;
148
+ throw e;
149
+ });
150
+ }
151
+ return ready;
152
+ }
153
+ async function mapToCurveElligator2(u) {
154
+ if (u.length !== 32) throw new Error("Expected 32-byte input");
155
+ await initElligator2Wasm();
156
+ return elligator2_curve25519_u(u);
157
+ }
158
+
159
+ // src/x25519-noble.ts
160
+ import { x25519 } from "@noble/curves/ed25519.js";
161
+ function x25519Noble(privScalar, pubU) {
162
+ if (privScalar.length !== 32) {
163
+ throw new Error(
164
+ `x25519Noble: privScalar must be 32 bytes, got ${privScalar.length}`
165
+ );
166
+ }
167
+ if (pubU.length !== 32) {
168
+ throw new Error(`x25519Noble: pubU must be 32 bytes, got ${pubU.length}`);
169
+ }
170
+ const shared = x25519.getSharedSecret(privScalar, pubU);
171
+ if (shared.length !== 32) {
172
+ return shared.slice(0, 32);
173
+ }
174
+ let allZero = true;
175
+ for (let i = 0; i < 32; i++) allZero = allZero && shared[i] === 0;
176
+ if (allZero) {
177
+ throw new Error(
178
+ "x25519Noble: invalid public key (shared secret is all-zero)"
179
+ );
180
+ }
181
+ return shared;
182
+ }
183
+
184
+ // src/cpace-group-x25519.ts
185
+ var MAX = 65536;
186
+ function getRandomBytes(len) {
187
+ if (!Number.isInteger(len) || len < 0)
188
+ throw new RangeError("len must be a non-negative integer");
189
+ const c = globalThis.crypto;
190
+ if (!c?.getRandomValues) {
191
+ throw new Error(
192
+ "WebCrypto is unavailable. Requires secure context (HTTPS) or an environment with WebCrypto."
193
+ );
194
+ }
195
+ const out = new Uint8Array(len);
196
+ for (let i = 0; i < len; i += MAX) {
197
+ c.getRandomValues(out.subarray(i, Math.min(i + MAX, len)));
198
+ }
199
+ return out;
200
+ }
201
+ async function x255192(scalar, point) {
202
+ return x25519Noble(scalar, point);
203
+ }
204
+ var LowOrderPointError = class extends Error {
205
+ reason;
206
+ constructor(message = "X25519Group.scalarMultVfy: low-order or invalid point", options) {
207
+ super(message, options);
208
+ this.name = "LowOrderPointError";
209
+ this.reason = options?.reason ?? "low-order";
210
+ }
211
+ };
212
+ var X25519Group = class {
213
+ name = "X25519";
214
+ fieldSizeBytes = 32;
215
+ fieldSizeBits = 255;
216
+ // for SHA-512 (128 byte block)
217
+ sInBytes = 128;
218
+ DSI = utf8("CPace255");
219
+ // neutral element (0^32)
220
+ I = new Uint8Array(32);
221
+ async calculateGenerator(hash, prs, ci, sid) {
222
+ const genStr = generatorString(this.DSI, prs, ci, sid, this.sInBytes);
223
+ const h = await hash(genStr);
224
+ if (h.length < this.fieldSizeBytes) {
225
+ throw new Error("X25519Group.calculateGenerator: hash output too short");
226
+ }
227
+ const genStrHash = h.slice(0, this.fieldSizeBytes);
228
+ const lastIndex = this.fieldSizeBytes - 1;
229
+ const lastByte = genStrHash[lastIndex];
230
+ if (lastByte === void 0) {
231
+ throw new Error(
232
+ "X25519Group.calculateGenerator: invalid generator hash length"
233
+ );
234
+ }
235
+ genStrHash[lastIndex] = lastByte & 127;
236
+ const gU = await mapToCurveElligator2(genStrHash);
237
+ return this.serialize(gU);
238
+ }
239
+ sampleScalar() {
240
+ return getRandomBytes(this.fieldSizeBytes);
241
+ }
242
+ async scalarMult(scalar, point) {
243
+ return x255192(scalar, point);
244
+ }
245
+ async scalarMultVfy(scalar, point) {
246
+ const u = point.slice();
247
+ if (u.length !== this.fieldSizeBytes) {
248
+ throw new LowOrderPointError(
249
+ `X25519Group.scalarMultVfy: invalid point length (expected ${this.fieldSizeBytes} bytes, got ${u.length})`,
250
+ { reason: "length" }
251
+ );
252
+ }
253
+ const inputLastIndex = u.length - 1;
254
+ const inputLastByte = u[inputLastIndex];
255
+ if (inputLastByte === void 0) {
256
+ throw new LowOrderPointError(
257
+ "X25519Group.scalarMultVfy: invalid point length (missing last byte)",
258
+ { reason: "missing-last-byte" }
259
+ );
260
+ }
261
+ u[inputLastIndex] = inputLastByte & 127;
262
+ let r;
263
+ try {
264
+ r = await x255192(scalar, u);
265
+ } catch (err) {
266
+ throw new LowOrderPointError(
267
+ "X25519Group.scalarMultVfy: invalid point multiplication failed",
268
+ { cause: err, reason: "multiply-failed" }
269
+ );
270
+ }
271
+ if (compareBytes(r, this.I) === 0) {
272
+ throw new LowOrderPointError(
273
+ "X25519Group.scalarMultVfy: low-order result (all-zero shared secret)",
274
+ { reason: "low-order" }
275
+ );
276
+ }
277
+ const masked = r.slice();
278
+ if (masked.length !== this.fieldSizeBytes) {
279
+ throw new LowOrderPointError(
280
+ `X25519Group.scalarMultVfy: invalid shared secret length (expected ${this.fieldSizeBytes} bytes, got ${masked.length})`,
281
+ { reason: "shared-secret-length" }
282
+ );
283
+ }
284
+ const outputLastIndex = masked.length - 1;
285
+ const outputLastByte = masked[outputLastIndex];
286
+ if (outputLastByte === void 0) {
287
+ throw new LowOrderPointError(
288
+ "X25519Group.scalarMultVfy: invalid shared secret length (missing last byte)",
289
+ { reason: "shared-secret-length" }
290
+ );
291
+ }
292
+ masked[outputLastIndex] = outputLastByte & 127;
293
+ return masked;
294
+ }
295
+ serialize(point) {
296
+ if (point.length !== this.fieldSizeBytes) {
297
+ throw new Error(
298
+ `X25519Group.serialize: expected ${this.fieldSizeBytes} bytes, got ${point.length}`
299
+ );
300
+ }
301
+ return point.slice();
302
+ }
303
+ deserialize(buf) {
304
+ if (buf.length !== this.fieldSizeBytes) {
305
+ throw new Error(
306
+ `X25519Group.deserialize: expected ${this.fieldSizeBytes} bytes, got ${buf.length}`
307
+ );
308
+ }
309
+ return buf.slice();
310
+ }
311
+ };
312
+ var G_X25519 = new X25519Group();
313
+
314
+ // src/cpace-validation.ts
315
+ var MAX_INPUT_LENGTH = Number.MAX_SAFE_INTEGER;
316
+ function ensureBytes(name, value, {
317
+ optional = true,
318
+ minLength = 0,
319
+ maxLength = MAX_INPUT_LENGTH
320
+ } = {}) {
321
+ if (!Number.isSafeInteger(maxLength) || maxLength < 0) {
322
+ throw new RangeError(
323
+ `CPaceSession: ${name} maxLength must be a non-negative safe integer`
324
+ );
325
+ }
326
+ if (value === void 0) {
327
+ if (!optional) {
328
+ throw new Error(`CPaceSession: ${name} is required`);
329
+ }
330
+ return new Uint8Array(0);
331
+ }
332
+ if (!(value instanceof Uint8Array)) {
333
+ throw new TypeError(`CPaceSession: ${name} must be a Uint8Array`);
334
+ }
335
+ if (value.length < minLength) {
336
+ throw new Error(
337
+ `CPaceSession: ${name} must be at least ${minLength} bytes`
338
+ );
339
+ }
340
+ if (value.length > maxLength) {
341
+ throw new Error(`CPaceSession: ${name} must be at most ${maxLength} bytes`);
342
+ }
343
+ return value;
344
+ }
345
+ function ensureField(field, value, options, onError) {
346
+ try {
347
+ return ensureBytes(field, value, options);
348
+ } catch (err) {
349
+ const context = options === void 0 ? { field, value } : { field, value, options };
350
+ onError?.(err, context);
351
+ throw err;
352
+ }
353
+ }
354
+ function extractExpected(options) {
355
+ if (!options) return void 0;
356
+ const expected = {};
357
+ if (options.minLength !== void 0) expected.min = options.minLength;
358
+ if (options.maxLength !== void 0) expected.max = options.maxLength;
359
+ return Object.keys(expected).length > 0 ? expected : void 0;
360
+ }
361
+ function cleanObject(data) {
362
+ if (!data) return void 0;
363
+ const cleaned = {};
364
+ for (const [key, value] of Object.entries(data)) {
365
+ if (value === void 0) continue;
366
+ cleaned[key] = value;
367
+ }
368
+ return cleaned;
369
+ }
370
+ function generateSessionId() {
371
+ const length = 16;
372
+ const bytes = new Uint8Array(length);
373
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.getRandomValues === "function") {
374
+ globalThis.crypto.getRandomValues(bytes);
375
+ } else {
376
+ for (let i = 0; i < length; i += 1) {
377
+ bytes[i] = Math.floor(Math.random() * 256);
378
+ }
379
+ }
380
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
381
+ }
382
+
383
+ // src/cpace-audit.ts
384
+ var AUDIT_CODES = Object.freeze({
385
+ CPACE_SESSION_CREATED: "CPACE_SESSION_CREATED",
386
+ CPACE_START_BEGIN: "CPACE_START_BEGIN",
387
+ CPACE_START_SENT: "CPACE_START_SENT",
388
+ CPACE_RX_RECEIVED: "CPACE_RX_RECEIVED",
389
+ CPACE_FINISH_BEGIN: "CPACE_FINISH_BEGIN",
390
+ CPACE_FINISH_OK: "CPACE_FINISH_OK",
391
+ CPACE_INPUT_INVALID: "CPACE_INPUT_INVALID",
392
+ CPACE_PEER_INVALID: "CPACE_PEER_INVALID",
393
+ CPACE_LOW_ORDER_POINT: "CPACE_LOW_ORDER_POINT"
394
+ });
395
+ function emitAuditEvent(logger, sessionId, code, level, data) {
396
+ if (!logger) return;
397
+ const cleaned = cleanObject(data);
398
+ const event = {
399
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
400
+ sessionId,
401
+ level,
402
+ code,
403
+ ...cleaned ? { data: cleaned } : {}
404
+ };
405
+ void logger.audit(event);
406
+ }
407
+
408
+ // src/cpace-crypto.ts
409
+ var EMPTY = new Uint8Array(0);
410
+ async function computeLocalElement(suite, prs, ci, sid) {
411
+ const pwdPoint = await suite.group.calculateGenerator(
412
+ suite.hash,
413
+ prs,
414
+ ci ?? EMPTY,
415
+ sid ?? EMPTY
416
+ );
417
+ const scalar = suite.group.sampleScalar();
418
+ const point = await suite.group.scalarMult(scalar, pwdPoint);
419
+ const serialized = suite.group.serialize(point);
420
+ return { scalar, serialized };
421
+ }
422
+ async function deriveSharedSecretOrThrow(suite, ephemeralScalar, peerPayload, onPeerInvalid, onLowOrder) {
423
+ let peerPoint;
424
+ try {
425
+ peerPoint = suite.group.deserialize(peerPayload);
426
+ } catch (err) {
427
+ onPeerInvalid(
428
+ err instanceof Error ? err.name ?? "Error" : "UnknownError",
429
+ err instanceof Error ? err.message : void 0
430
+ );
431
+ throw new InvalidPeerElementError(void 0, {
432
+ cause: err instanceof Error ? err : void 0
433
+ });
434
+ }
435
+ let sharedSecret;
436
+ try {
437
+ sharedSecret = await suite.group.scalarMultVfy(ephemeralScalar, peerPoint);
438
+ } catch (err) {
439
+ if (err instanceof LowOrderPointError) {
440
+ onLowOrder();
441
+ } else {
442
+ onPeerInvalid(
443
+ err instanceof Error ? err.name ?? "Error" : "UnknownError",
444
+ err instanceof Error ? err.message : void 0
445
+ );
446
+ }
447
+ throw new InvalidPeerElementError(void 0, {
448
+ cause: err instanceof Error ? err : void 0
449
+ });
450
+ }
451
+ if (compareBytes(sharedSecret, suite.group.I) === 0) {
452
+ onLowOrder();
453
+ throw new InvalidPeerElementError();
454
+ }
455
+ return sharedSecret;
456
+ }
457
+ async function deriveIskAndSid(suite, transcript, sharedSecret, sid) {
458
+ const dsiIsk = concat([suite.group.DSI, utf8("_ISK")]);
459
+ const sidBytes = sid ?? EMPTY;
460
+ const lvPart = lvCat(dsiIsk, sidBytes, sharedSecret);
461
+ const isk = await suite.hash(concat([lvPart, transcript]));
462
+ if (sidBytes.length === 0) {
463
+ const sidOutput = await suite.hash(
464
+ concat([utf8("CPaceSidOutput"), transcript])
465
+ );
466
+ return { isk, sidOutput };
467
+ }
468
+ return { isk };
469
+ }
470
+
471
+ // src/cpace-message.ts
472
+ function validateAndSanitizePeerMessage(suite, msg, ensureBytes2, onInvalid) {
473
+ if (!(msg.payload instanceof Uint8Array)) {
474
+ throw new InvalidPeerElementError(
475
+ "CPaceSession.receive: peer payload must be a Uint8Array"
476
+ );
477
+ }
478
+ const expectedPayloadLength = suite.group.fieldSizeBytes;
479
+ if (msg.payload.length !== expectedPayloadLength) {
480
+ onInvalid("peer.payload", "invalid length", {
481
+ expected: expectedPayloadLength,
482
+ actual: msg.payload.length
483
+ });
484
+ throw new InvalidPeerElementError(
485
+ `CPaceSession.receive: peer payload must be ${expectedPayloadLength} bytes`
486
+ );
487
+ }
488
+ if (!(msg.ad instanceof Uint8Array)) {
489
+ onInvalid("peer.ad", "peer ad must be a Uint8Array");
490
+ throw new InvalidPeerElementError(
491
+ "CPaceSession.receive: peer ad must be a Uint8Array"
492
+ );
493
+ }
494
+ const peerAd = ensureBytes2("peer ad", msg.ad);
495
+ return { type: "msg", payload: msg.payload, ad: peerAd };
496
+ }
497
+
498
+ // src/cpace-transcript.ts
499
+ function makeTranscriptIR(role, localMsg, localAd, peerPayload, peerAd) {
500
+ return role === "initiator" ? transcriptIr(localMsg, localAd, peerPayload, peerAd) : transcriptIr(peerPayload, peerAd, localMsg, localAd);
501
+ }
502
+ function makeTranscriptOC(localMsg, localAd, peerPayload, peerAd) {
503
+ return transcriptOc(localMsg, localAd, peerPayload, peerAd);
504
+ }
505
+
506
+ // src/cpace-session.ts
507
+ var EMPTY2 = new Uint8Array(0);
508
+ var CPaceSession = class {
509
+ auditLogger;
510
+ sessionId;
511
+ inputs;
512
+ ephemeralScalar;
513
+ localMsg;
514
+ iskValue;
515
+ sidValue;
516
+ /**
517
+ * Instantiate a CPace session for a local participant.
518
+ *
519
+ * @param options Protocol inputs and optional audit logger/session id.
520
+ */
521
+ constructor(options) {
522
+ const { audit, sessionId, ...inputs } = options;
523
+ this.auditLogger = audit;
524
+ this.sessionId = sessionId ?? generateSessionId();
525
+ this.inputs = {
526
+ ...inputs,
527
+ ad: inputs.ad ?? EMPTY2,
528
+ ci: inputs.ci ?? EMPTY2,
529
+ sid: inputs.sid ?? EMPTY2
530
+ };
531
+ const { mode, role, suite, ci, sid, ad } = this.inputs;
532
+ if (mode === "symmetric" && role !== "symmetric" || mode === "initiator-responder" && role === "symmetric") {
533
+ this.reportInputInvalid("role", "role must match selected mode", {
534
+ mode,
535
+ role
536
+ });
537
+ throw new Error("CPaceSession: invalid mode/role combination");
538
+ }
539
+ this.emitAudit(AUDIT_CODES.CPACE_SESSION_CREATED, "info", {
540
+ mode,
541
+ role,
542
+ suite: suite.name,
543
+ ci_len: ci?.length ?? 0,
544
+ sid_len: sid?.length ?? 0,
545
+ ad_len: ad?.length
546
+ });
547
+ }
548
+ /**
549
+ * Produce the local CPace message when acting as initiator or symmetric peer.
550
+ *
551
+ * @returns The outbound CPace message, or `undefined` when a responder should wait.
552
+ * @throws Error if required inputs are missing or invalid.
553
+ */
554
+ async start() {
555
+ const { suite, prs, ci, sid, ad, role, mode } = this.inputs;
556
+ const normalizedPrs = this.ensureRequired("prs", prs, { minLength: 1 });
557
+ const normalizedAd = this.ensureRequired("ad", ad);
558
+ this.emitAudit(AUDIT_CODES.CPACE_START_BEGIN, "info", { mode, role });
559
+ const { scalar: ephemeralScalar, serialized: localMsg } = await computeLocalElement(suite, normalizedPrs, ci, sid);
560
+ this.ephemeralScalar = ephemeralScalar;
561
+ this.localMsg = localMsg;
562
+ if (mode === "initiator-responder" && role === "responder") {
563
+ return void 0;
564
+ }
565
+ const outbound = {
566
+ type: "msg",
567
+ payload: this.localMsg,
568
+ ad: normalizedAd
569
+ };
570
+ this.emitAudit(AUDIT_CODES.CPACE_START_SENT, "info", {
571
+ payload_len: outbound.payload.length,
572
+ ad_len: outbound.ad.length
573
+ });
574
+ return outbound;
575
+ }
576
+ /**
577
+ * Consume a peer CPace message and, when required, return a response.
578
+ *
579
+ * @param msg Peer message containing the serialized group element and optional AD.
580
+ * @returns A response message for responder roles, otherwise `undefined`.
581
+ * @throws InvalidPeerElementError when peer inputs are malformed or low-order.
582
+ */
583
+ async receive(msg) {
584
+ const { prs, sid, ad, role, mode, suite } = this.inputs;
585
+ this.ensureRequired("prs", prs, { minLength: 1 });
586
+ const localAd = this.ensureRequired("ad", ad);
587
+ const sanitizedPeerMsg = validateAndSanitizePeerMessage(
588
+ suite,
589
+ msg,
590
+ (field, value) => this.ensureRequired(field, value),
591
+ (field, reason, extra) => this.reportInputInvalid(field, reason, extra)
592
+ );
593
+ await this.ensureResponderHasLocalMsg(mode, role);
594
+ this.emitAudit(AUDIT_CODES.CPACE_RX_RECEIVED, "info", {
595
+ payload_len: sanitizedPeerMsg.payload.length,
596
+ ad_len: sanitizedPeerMsg.ad.length
597
+ });
598
+ this.iskValue = await this.finish(sid, sanitizedPeerMsg);
599
+ if (mode === "initiator-responder" && role === "responder") {
600
+ if (!this.localMsg) {
601
+ throw new Error("CPaceSession.receive: missing outbound message");
602
+ }
603
+ const response = {
604
+ type: "msg",
605
+ payload: this.localMsg,
606
+ ad: localAd
607
+ // responder's ADb (may be EMPTY)
608
+ };
609
+ this.emitAudit(AUDIT_CODES.CPACE_START_SENT, "info", {
610
+ payload_len: response.payload.length,
611
+ ad_len: response.ad.length
612
+ });
613
+ return response;
614
+ }
615
+ return void 0;
616
+ }
617
+ /**
618
+ * Export the derived session key after `receive` completes the handshake.
619
+ *
620
+ * @returns The session's intermediate shared key (ISK).
621
+ * @throws Error if the session has not successfully finished.
622
+ */
623
+ exportISK() {
624
+ if (!this.iskValue) throw new Error("CPaceSession: not finished");
625
+ return this.iskValue.slice();
626
+ }
627
+ /**
628
+ * Obtain the session identifier output negotiated during the handshake, if any.
629
+ */
630
+ get sidOutput() {
631
+ return this.sidValue ? this.sidValue.slice() : void 0;
632
+ }
633
+ async finish(sid, peerMsg) {
634
+ if (!this.ephemeralScalar || !this.localMsg) {
635
+ throw new Error("CPaceSession.finish: session not started");
636
+ }
637
+ const { suite, mode, role, ad } = this.inputs;
638
+ const localAd = this.ensureRequired("ad", ad);
639
+ const peerAd = this.ensureRequired("peer ad", peerMsg.ad);
640
+ this.emitAudit(AUDIT_CODES.CPACE_FINISH_BEGIN, "info", { mode, role });
641
+ const sharedSecret = await deriveSharedSecretOrThrow(
642
+ suite,
643
+ this.ephemeralScalar,
644
+ peerMsg.payload,
645
+ (errorName, message) => {
646
+ this.emitAudit(AUDIT_CODES.CPACE_PEER_INVALID, "error", {
647
+ error: errorName,
648
+ message
649
+ });
650
+ },
651
+ () => {
652
+ this.emitAudit(AUDIT_CODES.CPACE_LOW_ORDER_POINT, "security", {});
653
+ }
654
+ );
655
+ let transcript;
656
+ if (mode === "initiator-responder") {
657
+ if (role === "initiator") {
658
+ transcript = makeTranscriptIR(
659
+ "initiator",
660
+ this.localMsg,
661
+ localAd,
662
+ peerMsg.payload,
663
+ peerAd
664
+ );
665
+ } else if (role === "responder") {
666
+ transcript = makeTranscriptIR(
667
+ "responder",
668
+ this.localMsg,
669
+ localAd,
670
+ peerMsg.payload,
671
+ peerAd
672
+ );
673
+ } else {
674
+ throw new Error(
675
+ "CPaceSession.finish: symmetric role in initiator-responder mode"
676
+ );
677
+ }
678
+ } else {
679
+ transcript = makeTranscriptOC(
680
+ this.localMsg,
681
+ localAd,
682
+ peerMsg.payload,
683
+ peerAd
684
+ );
685
+ }
686
+ const { isk, sidOutput } = await deriveIskAndSid(
687
+ suite,
688
+ transcript,
689
+ sharedSecret,
690
+ sid
691
+ );
692
+ this.sidValue = sidOutput;
693
+ this.zeroizeSecrets(sharedSecret);
694
+ this.emitAudit(AUDIT_CODES.CPACE_FINISH_OK, "info", {
695
+ transcript_type: mode === "initiator-responder" ? "ir" : "oc",
696
+ sid_provided: Boolean(sid?.length)
697
+ });
698
+ return isk;
699
+ }
700
+ /** @internal */
701
+ async ensureResponderHasLocalMsg(mode, role) {
702
+ if (mode === "initiator-responder" && role === "responder" && !this.localMsg) {
703
+ await this.start();
704
+ }
705
+ }
706
+ /** @internal */
707
+ zeroizeSecrets(...buffers) {
708
+ if (this.ephemeralScalar) {
709
+ this.ephemeralScalar.fill(0);
710
+ this.ephemeralScalar = void 0;
711
+ }
712
+ for (const buffer of buffers) {
713
+ buffer.fill(0);
714
+ }
715
+ }
716
+ ensureRequired(field, value, options) {
717
+ const enforcedOptions = { ...options, optional: false };
718
+ return ensureField(field, value, enforcedOptions, (err, ctx) => {
719
+ this.reportInputInvalid(
720
+ field,
721
+ err instanceof Error ? err.message : "validation failed",
722
+ this.buildValidationAuditExtra(ctx)
723
+ );
724
+ });
725
+ }
726
+ buildValidationAuditExtra(ctx) {
727
+ return {
728
+ expected: extractExpected(ctx.options),
729
+ actual: ctx.value instanceof Uint8Array ? ctx.value.length : ctx.value === void 0 ? "undefined" : null
730
+ };
731
+ }
732
+ reportInputInvalid(field, reason, extra) {
733
+ const extras = cleanObject(extra);
734
+ this.emitAudit(AUDIT_CODES.CPACE_INPUT_INVALID, "warn", {
735
+ field,
736
+ reason,
737
+ ...extras ?? {}
738
+ });
739
+ }
740
+ emitAudit(code, level, data) {
741
+ emitAuditEvent(this.auditLogger, this.sessionId, code, level, data);
742
+ }
743
+ };
744
+
745
+ // src/hash.ts
746
+ async function sha512(input) {
747
+ if (typeof crypto === "undefined" || !crypto.subtle) {
748
+ throw new Error("sha512: WebCrypto SubtleCrypto is not available");
749
+ }
750
+ const digest = await crypto.subtle.digest("SHA-512", input);
751
+ return new Uint8Array(digest);
752
+ }
753
+ export {
754
+ CPaceSession,
755
+ G_X25519,
756
+ InvalidPeerElementError,
757
+ sha512
758
+ };
759
+ //# sourceMappingURL=index.js.map