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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/index.cjs +797 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +135 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +759 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/wasm/pkg/.gitignore +1 -0
- package/wasm/pkg/cpace_wasm.d.ts +38 -0
- package/wasm/pkg/cpace_wasm.js +180 -0
- package/wasm/pkg/cpace_wasm_bg.js +81 -0
- package/wasm/pkg/cpace_wasm_bg.wasm +0 -0
- package/wasm/pkg/cpace_wasm_bg.wasm.d.ts +9 -0
- package/wasm/pkg/package.json +15 -0
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
|