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