@aegis-fluxion/core 0.7.2 → 0.8.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/README.md +51 -1
- package/dist/index.cjs +580 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +581 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -13,20 +13,27 @@ var DEFAULT_CLOSE_REASON = "";
|
|
|
13
13
|
var POLICY_VIOLATION_CLOSE_CODE = 1008;
|
|
14
14
|
var POLICY_VIOLATION_CLOSE_REASON = "Connection rejected by middleware.";
|
|
15
15
|
var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
16
|
+
var INTERNAL_SESSION_TICKET_EVENT = "__session:ticket";
|
|
16
17
|
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
17
18
|
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
18
19
|
var READY_EVENT = "ready";
|
|
19
20
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
21
|
+
var HANDSHAKE_PROTOCOL_VERSION = 1;
|
|
20
22
|
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
21
23
|
var GCM_IV_LENGTH = 12;
|
|
22
24
|
var GCM_AUTH_TAG_LENGTH = 16;
|
|
23
25
|
var ENCRYPTION_KEY_LENGTH = 32;
|
|
24
26
|
var ENCRYPTED_PACKET_VERSION = 1;
|
|
25
27
|
var ENCRYPTED_PACKET_PREFIX_LENGTH = 1 + GCM_IV_LENGTH + GCM_AUTH_TAG_LENGTH;
|
|
28
|
+
var SESSION_TICKET_VERSION = 1;
|
|
26
29
|
var BINARY_PAYLOAD_MARKER = "__afxBinaryPayload";
|
|
27
30
|
var BINARY_PAYLOAD_VERSION = 1;
|
|
28
31
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
29
32
|
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
33
|
+
var DEFAULT_SESSION_RESUMPTION_ENABLED = true;
|
|
34
|
+
var DEFAULT_SESSION_TICKET_TTL_MS = 10 * 6e4;
|
|
35
|
+
var DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE = 1e4;
|
|
36
|
+
var RESUMPTION_NONCE_LENGTH = 16;
|
|
30
37
|
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
31
38
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
32
39
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
@@ -248,7 +255,7 @@ function decodeCloseReason(reason) {
|
|
|
248
255
|
return reason.toString("utf8");
|
|
249
256
|
}
|
|
250
257
|
function isReservedEmitEvent(event) {
|
|
251
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
258
|
+
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
252
259
|
}
|
|
253
260
|
function isPromiseLike(value) {
|
|
254
261
|
return typeof value === "object" && value !== null && "then" in value;
|
|
@@ -339,16 +346,149 @@ function createEphemeralHandshakeState() {
|
|
|
339
346
|
localPublicKey: ecdh.getPublicKey("base64")
|
|
340
347
|
};
|
|
341
348
|
}
|
|
349
|
+
function decodeBase64ToBuffer(value, fieldName) {
|
|
350
|
+
if (typeof value !== "string") {
|
|
351
|
+
throw new Error(`${fieldName} must be a base64 string.`);
|
|
352
|
+
}
|
|
353
|
+
const normalizedValue = value.trim();
|
|
354
|
+
if (normalizedValue.length === 0) {
|
|
355
|
+
throw new Error(`${fieldName} must be a non-empty base64 string.`);
|
|
356
|
+
}
|
|
357
|
+
const decodedBuffer = Buffer.from(normalizedValue, "base64");
|
|
358
|
+
if (decodedBuffer.length === 0) {
|
|
359
|
+
throw new Error(`${fieldName} could not be decoded from base64.`);
|
|
360
|
+
}
|
|
361
|
+
const canonicalInput = normalizedValue.replace(/=+$/u, "");
|
|
362
|
+
const canonicalDecoded = decodedBuffer.toString("base64").replace(/=+$/u, "");
|
|
363
|
+
if (canonicalInput !== canonicalDecoded) {
|
|
364
|
+
throw new Error(`${fieldName} is not valid base64 content.`);
|
|
365
|
+
}
|
|
366
|
+
return decodedBuffer;
|
|
367
|
+
}
|
|
368
|
+
function equalsConstantTime(left, right) {
|
|
369
|
+
if (left.length !== right.length) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
return crypto.timingSafeEqual(left, right);
|
|
373
|
+
}
|
|
374
|
+
function createResumeClientProof(sessionSecret, sessionId, clientNonce) {
|
|
375
|
+
return crypto.createHmac("sha256", sessionSecret).update("afx-resume-client-proof:v1").update(sessionId).update(clientNonce).digest();
|
|
376
|
+
}
|
|
377
|
+
function createResumeServerProof(resumedKey, sessionId, clientNonce) {
|
|
378
|
+
return crypto.createHmac("sha256", resumedKey).update("afx-resume-server-proof:v1").update(sessionId).update(clientNonce).digest();
|
|
379
|
+
}
|
|
380
|
+
function deriveSessionTicketSecret(baseKey) {
|
|
381
|
+
return crypto.createHmac("sha256", baseKey).update("afx-session-ticket:v1").digest();
|
|
382
|
+
}
|
|
383
|
+
function deriveResumedEncryptionKey(sessionSecret, clientNonce) {
|
|
384
|
+
const derivedKey = crypto.createHash("sha256").update("afx-resume-encryption-key:v1").update(sessionSecret).update(clientNonce).digest();
|
|
385
|
+
if (derivedKey.length !== ENCRYPTION_KEY_LENGTH) {
|
|
386
|
+
throw new Error("Failed to derive a valid resumed AES-256 key.");
|
|
387
|
+
}
|
|
388
|
+
return derivedKey;
|
|
389
|
+
}
|
|
342
390
|
function parseHandshakePayload(data) {
|
|
343
391
|
if (typeof data !== "object" || data === null) {
|
|
344
392
|
throw new Error("Invalid handshake payload format.");
|
|
345
393
|
}
|
|
346
394
|
const payload = data;
|
|
347
|
-
if (typeof payload.
|
|
348
|
-
|
|
395
|
+
if (typeof payload.type !== "string") {
|
|
396
|
+
if (typeof payload.publicKey === "string" && payload.publicKey.length > 0) {
|
|
397
|
+
return {
|
|
398
|
+
type: "hello",
|
|
399
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
400
|
+
publicKey: payload.publicKey
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
throw new Error("Handshake payload must include a valid type.");
|
|
404
|
+
}
|
|
405
|
+
const protocolVersion = payload.protocolVersion === void 0 ? HANDSHAKE_PROTOCOL_VERSION : payload.protocolVersion;
|
|
406
|
+
if (protocolVersion !== HANDSHAKE_PROTOCOL_VERSION) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Unsupported handshake protocol version: ${String(protocolVersion)}.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (payload.type === "hello") {
|
|
412
|
+
if (typeof payload.publicKey !== "string" || payload.publicKey.length === 0) {
|
|
413
|
+
throw new Error("Handshake hello payload must include a non-empty public key.");
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
type: "hello",
|
|
417
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
418
|
+
publicKey: payload.publicKey
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (payload.type === "resume") {
|
|
422
|
+
if (typeof payload.sessionId !== "string" || payload.sessionId.trim().length === 0) {
|
|
423
|
+
throw new Error("Handshake resume payload must include a non-empty sessionId.");
|
|
424
|
+
}
|
|
425
|
+
if (typeof payload.clientNonce !== "string" || payload.clientNonce.length === 0) {
|
|
426
|
+
throw new Error("Handshake resume payload must include a non-empty clientNonce.");
|
|
427
|
+
}
|
|
428
|
+
if (typeof payload.clientProof !== "string" || payload.clientProof.length === 0) {
|
|
429
|
+
throw new Error("Handshake resume payload must include a non-empty clientProof.");
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
type: "resume",
|
|
433
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
434
|
+
sessionId: payload.sessionId.trim(),
|
|
435
|
+
clientNonce: payload.clientNonce,
|
|
436
|
+
clientProof: payload.clientProof
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (payload.type === "resume-ack") {
|
|
440
|
+
if (typeof payload.ok !== "boolean") {
|
|
441
|
+
throw new Error("Handshake resume-ack payload must include boolean ok.");
|
|
442
|
+
}
|
|
443
|
+
const normalizedPayload = {
|
|
444
|
+
type: "resume-ack",
|
|
445
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
446
|
+
ok: payload.ok
|
|
447
|
+
};
|
|
448
|
+
if (typeof payload.sessionId === "string" && payload.sessionId.trim().length > 0) {
|
|
449
|
+
normalizedPayload.sessionId = payload.sessionId.trim();
|
|
450
|
+
}
|
|
451
|
+
if (typeof payload.serverProof === "string" && payload.serverProof.length > 0) {
|
|
452
|
+
normalizedPayload.serverProof = payload.serverProof;
|
|
453
|
+
}
|
|
454
|
+
if (typeof payload.reason === "string" && payload.reason.trim().length > 0) {
|
|
455
|
+
normalizedPayload.reason = payload.reason.trim();
|
|
456
|
+
}
|
|
457
|
+
return normalizedPayload;
|
|
458
|
+
}
|
|
459
|
+
throw new Error(`Unsupported handshake payload type: ${payload.type}.`);
|
|
460
|
+
}
|
|
461
|
+
function parseSessionTicketPayload(data) {
|
|
462
|
+
if (typeof data !== "object" || data === null) {
|
|
463
|
+
throw new Error("Invalid session ticket payload format.");
|
|
464
|
+
}
|
|
465
|
+
const payload = data;
|
|
466
|
+
if (payload.version !== SESSION_TICKET_VERSION) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Unsupported session ticket payload version: ${String(payload.version)}.`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (typeof payload.sessionId !== "string" || payload.sessionId.trim().length === 0) {
|
|
472
|
+
throw new Error("Session ticket payload must include a non-empty sessionId.");
|
|
473
|
+
}
|
|
474
|
+
if (typeof payload.secret !== "string" || payload.secret.length === 0) {
|
|
475
|
+
throw new Error("Session ticket payload must include a non-empty secret.");
|
|
476
|
+
}
|
|
477
|
+
if (typeof payload.issuedAt !== "number" || !Number.isFinite(payload.issuedAt)) {
|
|
478
|
+
throw new Error("Session ticket payload issuedAt must be a finite number.");
|
|
479
|
+
}
|
|
480
|
+
if (typeof payload.expiresAt !== "number" || !Number.isFinite(payload.expiresAt)) {
|
|
481
|
+
throw new Error("Session ticket payload expiresAt must be a finite number.");
|
|
482
|
+
}
|
|
483
|
+
if (payload.expiresAt <= payload.issuedAt) {
|
|
484
|
+
throw new Error("Session ticket payload expiresAt must be greater than issuedAt.");
|
|
349
485
|
}
|
|
350
486
|
return {
|
|
351
|
-
|
|
487
|
+
version: SESSION_TICKET_VERSION,
|
|
488
|
+
sessionId: payload.sessionId.trim(),
|
|
489
|
+
secret: payload.secret,
|
|
490
|
+
issuedAt: payload.issuedAt,
|
|
491
|
+
expiresAt: payload.expiresAt
|
|
352
492
|
};
|
|
353
493
|
}
|
|
354
494
|
function deriveEncryptionKey(sharedSecret) {
|
|
@@ -418,6 +558,7 @@ var SecureServer = class {
|
|
|
418
558
|
adapter = null;
|
|
419
559
|
heartbeatConfig;
|
|
420
560
|
rateLimitConfig;
|
|
561
|
+
sessionResumptionConfig;
|
|
421
562
|
heartbeatIntervalHandle = null;
|
|
422
563
|
clientsById = /* @__PURE__ */ new Map();
|
|
423
564
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
@@ -439,10 +580,12 @@ var SecureServer = class {
|
|
|
439
580
|
clientIpByClientId = /* @__PURE__ */ new Map();
|
|
440
581
|
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
441
582
|
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
583
|
+
sessionTicketStore = /* @__PURE__ */ new Map();
|
|
442
584
|
constructor(options) {
|
|
443
|
-
const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
|
|
585
|
+
const { heartbeat, rateLimit, sessionResumption, adapter, ...socketServerOptions } = options;
|
|
444
586
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
445
587
|
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
588
|
+
this.sessionResumptionConfig = this.resolveSessionResumptionConfig(sessionResumption);
|
|
446
589
|
this.socketServer = new WebSocket.WebSocketServer(socketServerOptions);
|
|
447
590
|
this.bindSocketServerEvents();
|
|
448
591
|
this.startHeartbeatLoop();
|
|
@@ -528,8 +671,8 @@ var SecureServer = class {
|
|
|
528
671
|
this.errorHandlers.add(handler);
|
|
529
672
|
return this;
|
|
530
673
|
}
|
|
531
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
532
|
-
throw new Error(`The event "${
|
|
674
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
675
|
+
throw new Error(`The event "${event}" is reserved for internal use.`);
|
|
533
676
|
}
|
|
534
677
|
const typedHandler = handler;
|
|
535
678
|
const listeners = this.customEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
@@ -560,7 +703,7 @@ var SecureServer = class {
|
|
|
560
703
|
this.errorHandlers.delete(handler);
|
|
561
704
|
return this;
|
|
562
705
|
}
|
|
563
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
706
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
564
707
|
return this;
|
|
565
708
|
}
|
|
566
709
|
const listeners = this.customEventHandlers.get(event);
|
|
@@ -693,6 +836,7 @@ var SecureServer = class {
|
|
|
693
836
|
this.rateLimitBucketsByClientId.clear();
|
|
694
837
|
this.rateLimitBucketsByIp.clear();
|
|
695
838
|
this.clientIpByClientId.clear();
|
|
839
|
+
this.sessionTicketStore.clear();
|
|
696
840
|
this.socketServer.close();
|
|
697
841
|
} catch (error) {
|
|
698
842
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
@@ -769,6 +913,94 @@ var SecureServer = class {
|
|
|
769
913
|
disconnectReason
|
|
770
914
|
};
|
|
771
915
|
}
|
|
916
|
+
resolveSessionResumptionConfig(sessionResumptionOptions) {
|
|
917
|
+
const ticketTtlMs = sessionResumptionOptions?.ticketTtlMs ?? DEFAULT_SESSION_TICKET_TTL_MS;
|
|
918
|
+
const maxCachedTickets = sessionResumptionOptions?.maxCachedTickets ?? DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE;
|
|
919
|
+
if (!Number.isFinite(ticketTtlMs) || ticketTtlMs <= 0) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
"Server sessionResumption ticketTtlMs must be a positive number."
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (!Number.isInteger(maxCachedTickets) || maxCachedTickets <= 0) {
|
|
925
|
+
throw new Error(
|
|
926
|
+
"Server sessionResumption maxCachedTickets must be a positive integer."
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
enabled: sessionResumptionOptions?.enabled ?? DEFAULT_SESSION_RESUMPTION_ENABLED,
|
|
931
|
+
ticketTtlMs,
|
|
932
|
+
maxCachedTickets
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
pruneExpiredSessionTickets(now) {
|
|
936
|
+
for (const [sessionId, ticketRecord] of this.sessionTicketStore.entries()) {
|
|
937
|
+
if (ticketRecord.expiresAt <= now) {
|
|
938
|
+
this.sessionTicketStore.delete(sessionId);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
evictSessionTicketsIfNeeded() {
|
|
943
|
+
while (this.sessionTicketStore.size > this.sessionResumptionConfig.maxCachedTickets) {
|
|
944
|
+
let oldestSessionId = null;
|
|
945
|
+
let oldestIssuedAt = Number.POSITIVE_INFINITY;
|
|
946
|
+
for (const [sessionId, ticketRecord] of this.sessionTicketStore.entries()) {
|
|
947
|
+
if (ticketRecord.issuedAt < oldestIssuedAt) {
|
|
948
|
+
oldestIssuedAt = ticketRecord.issuedAt;
|
|
949
|
+
oldestSessionId = sessionId;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!oldestSessionId) {
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
this.sessionTicketStore.delete(oldestSessionId);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
getSessionTicket(sessionId) {
|
|
959
|
+
const now = Date.now();
|
|
960
|
+
this.pruneExpiredSessionTickets(now);
|
|
961
|
+
const ticketRecord = this.sessionTicketStore.get(sessionId);
|
|
962
|
+
if (!ticketRecord) {
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
if (ticketRecord.expiresAt <= now) {
|
|
966
|
+
this.sessionTicketStore.delete(sessionId);
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
return ticketRecord;
|
|
970
|
+
}
|
|
971
|
+
issueSessionTicket(socket, baseKey) {
|
|
972
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const now = Date.now();
|
|
976
|
+
this.pruneExpiredSessionTickets(now);
|
|
977
|
+
const sessionId = crypto.randomUUID();
|
|
978
|
+
const sessionSecret = deriveSessionTicketSecret(baseKey);
|
|
979
|
+
const expiresAt = now + this.sessionResumptionConfig.ticketTtlMs;
|
|
980
|
+
const ticketRecord = {
|
|
981
|
+
sessionId,
|
|
982
|
+
secret: sessionSecret,
|
|
983
|
+
issuedAt: now,
|
|
984
|
+
expiresAt
|
|
985
|
+
};
|
|
986
|
+
this.sessionTicketStore.set(sessionId, ticketRecord);
|
|
987
|
+
this.evictSessionTicketsIfNeeded();
|
|
988
|
+
const ticketPayload = {
|
|
989
|
+
version: SESSION_TICKET_VERSION,
|
|
990
|
+
sessionId,
|
|
991
|
+
secret: sessionSecret.toString("base64"),
|
|
992
|
+
issuedAt: now,
|
|
993
|
+
expiresAt
|
|
994
|
+
};
|
|
995
|
+
void this.sendOrQueuePayload(socket, {
|
|
996
|
+
event: INTERNAL_SESSION_TICKET_EVENT,
|
|
997
|
+
data: ticketPayload
|
|
998
|
+
}).catch((error) => {
|
|
999
|
+
this.notifyError(
|
|
1000
|
+
normalizeToError(error, "Failed to deliver secure session ticket.")
|
|
1001
|
+
);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
772
1004
|
createRateLimitBucket(now) {
|
|
773
1005
|
return {
|
|
774
1006
|
windowStartedAt: now,
|
|
@@ -1139,6 +1371,14 @@ var SecureServer = class {
|
|
|
1139
1371
|
await this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
1140
1372
|
return;
|
|
1141
1373
|
}
|
|
1374
|
+
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1375
|
+
this.notifyError(
|
|
1376
|
+
new Error(
|
|
1377
|
+
`Client ${client.id} attempted to send reserved internal session ticket event.`
|
|
1378
|
+
)
|
|
1379
|
+
);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1142
1382
|
const interceptedData = await this.applyMessageMiddleware(
|
|
1143
1383
|
"incoming",
|
|
1144
1384
|
client,
|
|
@@ -1452,10 +1692,104 @@ var SecureServer = class {
|
|
|
1452
1692
|
this.sendRaw(
|
|
1453
1693
|
socket,
|
|
1454
1694
|
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
1695
|
+
type: "hello",
|
|
1696
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
1455
1697
|
publicKey: localPublicKey
|
|
1456
1698
|
})
|
|
1457
1699
|
);
|
|
1458
1700
|
}
|
|
1701
|
+
sendResumeAck(socket, payload) {
|
|
1702
|
+
const responsePayload = {
|
|
1703
|
+
type: "resume-ack",
|
|
1704
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
1705
|
+
ok: payload.ok
|
|
1706
|
+
};
|
|
1707
|
+
if (payload.sessionId !== void 0 && payload.sessionId.length > 0) {
|
|
1708
|
+
responsePayload.sessionId = payload.sessionId;
|
|
1709
|
+
}
|
|
1710
|
+
if (payload.serverProof !== void 0 && payload.serverProof.length > 0) {
|
|
1711
|
+
responsePayload.serverProof = payload.serverProof;
|
|
1712
|
+
}
|
|
1713
|
+
if (payload.reason !== void 0 && payload.reason.length > 0) {
|
|
1714
|
+
responsePayload.reason = payload.reason;
|
|
1715
|
+
}
|
|
1716
|
+
this.sendRaw(
|
|
1717
|
+
socket,
|
|
1718
|
+
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, responsePayload)
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
handleResumeHandshake(client, payload) {
|
|
1722
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
1723
|
+
this.sendResumeAck(client.socket, {
|
|
1724
|
+
ok: false,
|
|
1725
|
+
reason: "Session resumption is disabled."
|
|
1726
|
+
});
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const ticketRecord = this.getSessionTicket(payload.sessionId);
|
|
1730
|
+
if (!ticketRecord) {
|
|
1731
|
+
this.sendResumeAck(client.socket, {
|
|
1732
|
+
ok: false,
|
|
1733
|
+
reason: "Session ticket is unknown or expired."
|
|
1734
|
+
});
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
try {
|
|
1738
|
+
const clientNonce = decodeBase64ToBuffer(
|
|
1739
|
+
payload.clientNonce,
|
|
1740
|
+
"Handshake resume clientNonce"
|
|
1741
|
+
);
|
|
1742
|
+
if (clientNonce.length !== RESUMPTION_NONCE_LENGTH) {
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
`Handshake resume clientNonce must be ${RESUMPTION_NONCE_LENGTH} bytes.`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
const receivedProof = decodeBase64ToBuffer(
|
|
1748
|
+
payload.clientProof,
|
|
1749
|
+
"Handshake resume clientProof"
|
|
1750
|
+
);
|
|
1751
|
+
const expectedProof = createResumeClientProof(
|
|
1752
|
+
ticketRecord.secret,
|
|
1753
|
+
ticketRecord.sessionId,
|
|
1754
|
+
clientNonce
|
|
1755
|
+
);
|
|
1756
|
+
if (!equalsConstantTime(receivedProof, expectedProof)) {
|
|
1757
|
+
this.sendResumeAck(client.socket, {
|
|
1758
|
+
ok: false,
|
|
1759
|
+
reason: "Session resumption proof validation failed."
|
|
1760
|
+
});
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
this.sessionTicketStore.delete(ticketRecord.sessionId);
|
|
1764
|
+
const resumedKey = deriveResumedEncryptionKey(ticketRecord.secret, clientNonce);
|
|
1765
|
+
const serverProof = createResumeServerProof(
|
|
1766
|
+
resumedKey,
|
|
1767
|
+
ticketRecord.sessionId,
|
|
1768
|
+
clientNonce
|
|
1769
|
+
).toString("base64");
|
|
1770
|
+
const handshakeState = this.handshakeStateBySocket.get(client.socket);
|
|
1771
|
+
if (!handshakeState) {
|
|
1772
|
+
throw new Error(`Missing handshake state for client ${client.id}.`);
|
|
1773
|
+
}
|
|
1774
|
+
this.sharedSecretBySocket.set(client.socket, resumedKey);
|
|
1775
|
+
this.encryptionKeyBySocket.set(client.socket, resumedKey);
|
|
1776
|
+
handshakeState.isReady = true;
|
|
1777
|
+
this.sendResumeAck(client.socket, {
|
|
1778
|
+
ok: true,
|
|
1779
|
+
sessionId: ticketRecord.sessionId,
|
|
1780
|
+
serverProof
|
|
1781
|
+
});
|
|
1782
|
+
void this.flushQueuedPayloads(client.socket);
|
|
1783
|
+
this.notifyReady(client);
|
|
1784
|
+
this.issueSessionTicket(client.socket, resumedKey);
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
this.sendResumeAck(client.socket, {
|
|
1787
|
+
ok: false,
|
|
1788
|
+
reason: "Session resumption payload was invalid."
|
|
1789
|
+
});
|
|
1790
|
+
this.notifyError(normalizeToError(error, "Failed to resume secure server session."));
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1459
1793
|
handleInternalHandshake(client, data) {
|
|
1460
1794
|
try {
|
|
1461
1795
|
const payload = parseHandshakePayload(data);
|
|
@@ -1466,14 +1800,22 @@ var SecureServer = class {
|
|
|
1466
1800
|
if (handshakeState.isReady) {
|
|
1467
1801
|
return;
|
|
1468
1802
|
}
|
|
1803
|
+
if (payload.type === "resume") {
|
|
1804
|
+
this.handleResumeHandshake(client, payload);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (payload.type === "resume-ack") {
|
|
1808
|
+
throw new Error("SecureServer received unexpected resume-ack handshake payload.");
|
|
1809
|
+
}
|
|
1469
1810
|
const remotePublicKey = Buffer.from(payload.publicKey, "base64");
|
|
1470
1811
|
const sharedSecret = handshakeState.ecdh.computeSecret(remotePublicKey);
|
|
1471
1812
|
const encryptionKey = deriveEncryptionKey(sharedSecret);
|
|
1472
1813
|
this.sharedSecretBySocket.set(client.socket, sharedSecret);
|
|
1473
1814
|
this.encryptionKeyBySocket.set(client.socket, encryptionKey);
|
|
1474
1815
|
handshakeState.isReady = true;
|
|
1475
|
-
this.flushQueuedPayloads(client.socket);
|
|
1816
|
+
void this.flushQueuedPayloads(client.socket);
|
|
1476
1817
|
this.notifyReady(client);
|
|
1818
|
+
this.issueSessionTicket(client.socket, encryptionKey);
|
|
1477
1819
|
} catch (error) {
|
|
1478
1820
|
this.notifyError(normalizeToError(error, "Failed to complete server handshake."));
|
|
1479
1821
|
}
|
|
@@ -1687,6 +2029,9 @@ var SecureClient = class {
|
|
|
1687
2029
|
this.url = url;
|
|
1688
2030
|
this.options = options;
|
|
1689
2031
|
this.reconnectConfig = this.resolveReconnectConfig(this.options.reconnect);
|
|
2032
|
+
this.sessionResumptionConfig = this.resolveSessionResumptionConfig(
|
|
2033
|
+
this.options.sessionResumption
|
|
2034
|
+
);
|
|
1690
2035
|
if (this.options.autoConnect ?? true) {
|
|
1691
2036
|
this.connect();
|
|
1692
2037
|
}
|
|
@@ -1695,6 +2040,7 @@ var SecureClient = class {
|
|
|
1695
2040
|
options;
|
|
1696
2041
|
socket = null;
|
|
1697
2042
|
reconnectConfig;
|
|
2043
|
+
sessionResumptionConfig;
|
|
1698
2044
|
reconnectAttemptCount = 0;
|
|
1699
2045
|
reconnectTimer = null;
|
|
1700
2046
|
isManualDisconnectRequested = false;
|
|
@@ -1706,6 +2052,7 @@ var SecureClient = class {
|
|
|
1706
2052
|
handshakeState = null;
|
|
1707
2053
|
pendingPayloadQueue = [];
|
|
1708
2054
|
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
2055
|
+
sessionTicket = null;
|
|
1709
2056
|
get readyState() {
|
|
1710
2057
|
return this.socket?.readyState ?? null;
|
|
1711
2058
|
}
|
|
@@ -1764,8 +2111,8 @@ var SecureClient = class {
|
|
|
1764
2111
|
this.errorHandlers.add(handler);
|
|
1765
2112
|
return this;
|
|
1766
2113
|
}
|
|
1767
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
1768
|
-
throw new Error(`The event "${
|
|
2114
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2115
|
+
throw new Error(`The event "${event}" is reserved for internal use.`);
|
|
1769
2116
|
}
|
|
1770
2117
|
const typedHandler = handler;
|
|
1771
2118
|
const listeners = this.customEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
@@ -1796,7 +2143,7 @@ var SecureClient = class {
|
|
|
1796
2143
|
this.errorHandlers.delete(handler);
|
|
1797
2144
|
return this;
|
|
1798
2145
|
}
|
|
1799
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
2146
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1800
2147
|
return this;
|
|
1801
2148
|
}
|
|
1802
2149
|
const listeners = this.customEventHandlers.get(event);
|
|
@@ -1902,6 +2249,24 @@ var SecureClient = class {
|
|
|
1902
2249
|
maxAttempts
|
|
1903
2250
|
};
|
|
1904
2251
|
}
|
|
2252
|
+
resolveSessionResumptionConfig(sessionResumptionOptions) {
|
|
2253
|
+
if (typeof sessionResumptionOptions === "boolean") {
|
|
2254
|
+
return {
|
|
2255
|
+
enabled: sessionResumptionOptions,
|
|
2256
|
+
maxAcceptedTicketTtlMs: DEFAULT_SESSION_TICKET_TTL_MS
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
const maxAcceptedTicketTtlMs = sessionResumptionOptions?.maxAcceptedTicketTtlMs ?? DEFAULT_SESSION_TICKET_TTL_MS;
|
|
2260
|
+
if (!Number.isFinite(maxAcceptedTicketTtlMs) || maxAcceptedTicketTtlMs <= 0) {
|
|
2261
|
+
throw new Error(
|
|
2262
|
+
"Client sessionResumption maxAcceptedTicketTtlMs must be a positive number."
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
enabled: sessionResumptionOptions?.enabled ?? DEFAULT_SESSION_RESUMPTION_ENABLED,
|
|
2267
|
+
maxAcceptedTicketTtlMs
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
1905
2270
|
scheduleReconnect() {
|
|
1906
2271
|
if (!this.reconnectConfig.enabled || this.reconnectTimer) {
|
|
1907
2272
|
return;
|
|
@@ -1949,7 +2314,6 @@ var SecureClient = class {
|
|
|
1949
2314
|
socket.on("open", () => {
|
|
1950
2315
|
this.clearReconnectTimer();
|
|
1951
2316
|
this.reconnectAttemptCount = 0;
|
|
1952
|
-
this.sendInternalHandshake();
|
|
1953
2317
|
this.notifyConnect();
|
|
1954
2318
|
});
|
|
1955
2319
|
socket.on("message", (rawData) => {
|
|
@@ -2005,6 +2369,10 @@ var SecureClient = class {
|
|
|
2005
2369
|
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
2006
2370
|
return;
|
|
2007
2371
|
}
|
|
2372
|
+
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2373
|
+
this.handleSessionTicket(decryptedEnvelope.data);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2008
2376
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data);
|
|
2009
2377
|
} catch (error) {
|
|
2010
2378
|
this.notifyError(normalizeToError(error, "Failed to process incoming client message."));
|
|
@@ -2218,11 +2586,45 @@ var SecureClient = class {
|
|
|
2218
2586
|
}
|
|
2219
2587
|
this.pendingRpcRequests.clear();
|
|
2220
2588
|
}
|
|
2589
|
+
handleSessionTicket(data) {
|
|
2590
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
try {
|
|
2594
|
+
const ticketPayload = parseSessionTicketPayload(data);
|
|
2595
|
+
const now = Date.now();
|
|
2596
|
+
if (ticketPayload.expiresAt <= now) {
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const ticketTtlMs = ticketPayload.expiresAt - ticketPayload.issuedAt;
|
|
2600
|
+
if (ticketTtlMs > this.sessionResumptionConfig.maxAcceptedTicketTtlMs) {
|
|
2601
|
+
throw new Error("Session ticket TTL exceeds client trust policy.");
|
|
2602
|
+
}
|
|
2603
|
+
const sessionSecret = decodeBase64ToBuffer(
|
|
2604
|
+
ticketPayload.secret,
|
|
2605
|
+
"Session ticket secret"
|
|
2606
|
+
);
|
|
2607
|
+
if (sessionSecret.length !== ENCRYPTION_KEY_LENGTH) {
|
|
2608
|
+
throw new Error("Session ticket secret has invalid length.");
|
|
2609
|
+
}
|
|
2610
|
+
this.sessionTicket = {
|
|
2611
|
+
sessionId: ticketPayload.sessionId,
|
|
2612
|
+
secret: sessionSecret,
|
|
2613
|
+
issuedAt: ticketPayload.issuedAt,
|
|
2614
|
+
expiresAt: ticketPayload.expiresAt
|
|
2615
|
+
};
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
this.notifyError(normalizeToError(error, "Failed to process session ticket payload."));
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2221
2620
|
createClientHandshakeState() {
|
|
2222
2621
|
const { ecdh, localPublicKey } = createEphemeralHandshakeState();
|
|
2223
2622
|
return {
|
|
2224
2623
|
ecdh,
|
|
2225
2624
|
localPublicKey,
|
|
2625
|
+
clientHelloSent: false,
|
|
2626
|
+
pendingServerPublicKey: null,
|
|
2627
|
+
resumeAttempt: null,
|
|
2226
2628
|
isReady: false,
|
|
2227
2629
|
sharedSecret: null,
|
|
2228
2630
|
encryptionKey: null
|
|
@@ -2236,15 +2638,171 @@ var SecureClient = class {
|
|
|
2236
2638
|
if (!this.handshakeState) {
|
|
2237
2639
|
throw new Error("Missing client handshake state.");
|
|
2238
2640
|
}
|
|
2641
|
+
if (this.handshakeState.clientHelloSent) {
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2239
2644
|
this.socket.send(
|
|
2240
2645
|
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
2646
|
+
type: "hello",
|
|
2647
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
2241
2648
|
publicKey: this.handshakeState.localPublicKey
|
|
2242
2649
|
})
|
|
2243
2650
|
);
|
|
2651
|
+
this.handshakeState.clientHelloSent = true;
|
|
2244
2652
|
} catch (error) {
|
|
2245
2653
|
this.notifyError(normalizeToError(error, "Failed to send client handshake payload."));
|
|
2246
2654
|
}
|
|
2247
2655
|
}
|
|
2656
|
+
shouldAttemptSessionResumption() {
|
|
2657
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
const sessionTicket = this.sessionTicket;
|
|
2661
|
+
if (!sessionTicket) {
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
const now = Date.now();
|
|
2665
|
+
if (sessionTicket.expiresAt <= now) {
|
|
2666
|
+
this.sessionTicket = null;
|
|
2667
|
+
return false;
|
|
2668
|
+
}
|
|
2669
|
+
const ticketTtlMs = sessionTicket.expiresAt - sessionTicket.issuedAt;
|
|
2670
|
+
if (ticketTtlMs > this.sessionResumptionConfig.maxAcceptedTicketTtlMs) {
|
|
2671
|
+
this.sessionTicket = null;
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
return true;
|
|
2675
|
+
}
|
|
2676
|
+
sendResumeHandshake() {
|
|
2677
|
+
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
2678
|
+
return false;
|
|
2679
|
+
}
|
|
2680
|
+
if (!this.handshakeState || !this.sessionTicket) {
|
|
2681
|
+
return false;
|
|
2682
|
+
}
|
|
2683
|
+
if (this.handshakeState.clientHelloSent) {
|
|
2684
|
+
return false;
|
|
2685
|
+
}
|
|
2686
|
+
if (this.handshakeState.resumeAttempt?.status === "pending") {
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
2689
|
+
try {
|
|
2690
|
+
const clientNonce = crypto.randomBytes(RESUMPTION_NONCE_LENGTH);
|
|
2691
|
+
const resumedKey = deriveResumedEncryptionKey(this.sessionTicket.secret, clientNonce);
|
|
2692
|
+
const clientProof = createResumeClientProof(
|
|
2693
|
+
this.sessionTicket.secret,
|
|
2694
|
+
this.sessionTicket.sessionId,
|
|
2695
|
+
clientNonce
|
|
2696
|
+
);
|
|
2697
|
+
this.socket.send(
|
|
2698
|
+
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
2699
|
+
type: "resume",
|
|
2700
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
2701
|
+
sessionId: this.sessionTicket.sessionId,
|
|
2702
|
+
clientNonce: clientNonce.toString("base64"),
|
|
2703
|
+
clientProof: clientProof.toString("base64")
|
|
2704
|
+
})
|
|
2705
|
+
);
|
|
2706
|
+
this.handshakeState.resumeAttempt = {
|
|
2707
|
+
status: "pending",
|
|
2708
|
+
sessionId: this.sessionTicket.sessionId,
|
|
2709
|
+
clientNonce,
|
|
2710
|
+
resumedKey
|
|
2711
|
+
};
|
|
2712
|
+
return true;
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
this.notifyError(normalizeToError(error, "Failed to dispatch resume handshake payload."));
|
|
2715
|
+
this.sessionTicket = null;
|
|
2716
|
+
this.handshakeState.resumeAttempt = null;
|
|
2717
|
+
return false;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
completeFullHandshake(serverPublicKey) {
|
|
2721
|
+
if (!this.handshakeState) {
|
|
2722
|
+
throw new Error("Missing client handshake state.");
|
|
2723
|
+
}
|
|
2724
|
+
if (this.handshakeState.isReady) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
this.sendInternalHandshake();
|
|
2728
|
+
const remotePublicKey = Buffer.from(serverPublicKey, "base64");
|
|
2729
|
+
const sharedSecret = this.handshakeState.ecdh.computeSecret(remotePublicKey);
|
|
2730
|
+
this.handshakeState.sharedSecret = sharedSecret;
|
|
2731
|
+
this.handshakeState.encryptionKey = deriveEncryptionKey(sharedSecret);
|
|
2732
|
+
this.handshakeState.resumeAttempt = null;
|
|
2733
|
+
this.handshakeState.pendingServerPublicKey = null;
|
|
2734
|
+
this.handshakeState.isReady = true;
|
|
2735
|
+
void this.flushPendingPayloadQueue();
|
|
2736
|
+
this.notifyReady();
|
|
2737
|
+
}
|
|
2738
|
+
fallbackToFullHandshake() {
|
|
2739
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
if (this.handshakeState.resumeAttempt) {
|
|
2743
|
+
this.handshakeState.resumeAttempt.status = "failed";
|
|
2744
|
+
}
|
|
2745
|
+
const pendingServerPublicKey = this.handshakeState.pendingServerPublicKey;
|
|
2746
|
+
if (pendingServerPublicKey) {
|
|
2747
|
+
this.completeFullHandshake(pendingServerPublicKey);
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
this.sendInternalHandshake();
|
|
2751
|
+
}
|
|
2752
|
+
handleServerHelloHandshake(payload) {
|
|
2753
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
this.handshakeState.pendingServerPublicKey = payload.publicKey;
|
|
2757
|
+
if (this.shouldAttemptSessionResumption() && this.sendResumeHandshake()) {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
this.completeFullHandshake(payload.publicKey);
|
|
2761
|
+
}
|
|
2762
|
+
handleResumeAckHandshake(payload) {
|
|
2763
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
const resumeAttempt = this.handshakeState.resumeAttempt;
|
|
2767
|
+
if (!resumeAttempt || resumeAttempt.status !== "pending") {
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (!payload.ok) {
|
|
2771
|
+
this.sessionTicket = null;
|
|
2772
|
+
this.fallbackToFullHandshake();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (payload.sessionId !== resumeAttempt.sessionId || !payload.serverProof) {
|
|
2776
|
+
this.sessionTicket = null;
|
|
2777
|
+
this.fallbackToFullHandshake();
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
try {
|
|
2781
|
+
const receivedServerProof = decodeBase64ToBuffer(
|
|
2782
|
+
payload.serverProof,
|
|
2783
|
+
"Handshake resume-ack serverProof"
|
|
2784
|
+
);
|
|
2785
|
+
const expectedServerProof = createResumeServerProof(
|
|
2786
|
+
resumeAttempt.resumedKey,
|
|
2787
|
+
resumeAttempt.sessionId,
|
|
2788
|
+
resumeAttempt.clientNonce
|
|
2789
|
+
);
|
|
2790
|
+
if (!equalsConstantTime(receivedServerProof, expectedServerProof)) {
|
|
2791
|
+
throw new Error("Resume server proof validation failed.");
|
|
2792
|
+
}
|
|
2793
|
+
this.handshakeState.sharedSecret = resumeAttempt.resumedKey;
|
|
2794
|
+
this.handshakeState.encryptionKey = resumeAttempt.resumedKey;
|
|
2795
|
+
this.handshakeState.pendingServerPublicKey = null;
|
|
2796
|
+
resumeAttempt.status = "accepted";
|
|
2797
|
+
this.handshakeState.isReady = true;
|
|
2798
|
+
void this.flushPendingPayloadQueue();
|
|
2799
|
+
this.notifyReady();
|
|
2800
|
+
} catch (error) {
|
|
2801
|
+
this.notifyError(normalizeToError(error, "Failed to verify resume server proof."));
|
|
2802
|
+
this.sessionTicket = null;
|
|
2803
|
+
this.fallbackToFullHandshake();
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2248
2806
|
handleInternalHandshake(data) {
|
|
2249
2807
|
try {
|
|
2250
2808
|
const payload = parseHandshakePayload(data);
|
|
@@ -2254,13 +2812,15 @@ var SecureClient = class {
|
|
|
2254
2812
|
if (this.handshakeState.isReady) {
|
|
2255
2813
|
return;
|
|
2256
2814
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2815
|
+
if (payload.type === "hello") {
|
|
2816
|
+
this.handleServerHelloHandshake(payload);
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
if (payload.type === "resume-ack") {
|
|
2820
|
+
this.handleResumeAckHandshake(payload);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
throw new Error("SecureClient received unexpected resume request handshake payload.");
|
|
2264
2824
|
} catch (error) {
|
|
2265
2825
|
this.notifyError(normalizeToError(error, "Failed to complete client handshake."));
|
|
2266
2826
|
}
|