@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID,
|
|
1
|
+
import { randomUUID, randomBytes, createHmac, createDecipheriv, createCipheriv, createECDH, timingSafeEqual, createHash } from 'crypto';
|
|
2
2
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
@@ -7,20 +7,27 @@ var DEFAULT_CLOSE_REASON = "";
|
|
|
7
7
|
var POLICY_VIOLATION_CLOSE_CODE = 1008;
|
|
8
8
|
var POLICY_VIOLATION_CLOSE_REASON = "Connection rejected by middleware.";
|
|
9
9
|
var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
10
|
+
var INTERNAL_SESSION_TICKET_EVENT = "__session:ticket";
|
|
10
11
|
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
11
12
|
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
12
13
|
var READY_EVENT = "ready";
|
|
13
14
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
15
|
+
var HANDSHAKE_PROTOCOL_VERSION = 1;
|
|
14
16
|
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
15
17
|
var GCM_IV_LENGTH = 12;
|
|
16
18
|
var GCM_AUTH_TAG_LENGTH = 16;
|
|
17
19
|
var ENCRYPTION_KEY_LENGTH = 32;
|
|
18
20
|
var ENCRYPTED_PACKET_VERSION = 1;
|
|
19
21
|
var ENCRYPTED_PACKET_PREFIX_LENGTH = 1 + GCM_IV_LENGTH + GCM_AUTH_TAG_LENGTH;
|
|
22
|
+
var SESSION_TICKET_VERSION = 1;
|
|
20
23
|
var BINARY_PAYLOAD_MARKER = "__afxBinaryPayload";
|
|
21
24
|
var BINARY_PAYLOAD_VERSION = 1;
|
|
22
25
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
23
26
|
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
27
|
+
var DEFAULT_SESSION_RESUMPTION_ENABLED = true;
|
|
28
|
+
var DEFAULT_SESSION_TICKET_TTL_MS = 10 * 6e4;
|
|
29
|
+
var DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE = 1e4;
|
|
30
|
+
var RESUMPTION_NONCE_LENGTH = 16;
|
|
24
31
|
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
25
32
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
26
33
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
@@ -242,7 +249,7 @@ function decodeCloseReason(reason) {
|
|
|
242
249
|
return reason.toString("utf8");
|
|
243
250
|
}
|
|
244
251
|
function isReservedEmitEvent(event) {
|
|
245
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
252
|
+
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
246
253
|
}
|
|
247
254
|
function isPromiseLike(value) {
|
|
248
255
|
return typeof value === "object" && value !== null && "then" in value;
|
|
@@ -333,16 +340,149 @@ function createEphemeralHandshakeState() {
|
|
|
333
340
|
localPublicKey: ecdh.getPublicKey("base64")
|
|
334
341
|
};
|
|
335
342
|
}
|
|
343
|
+
function decodeBase64ToBuffer(value, fieldName) {
|
|
344
|
+
if (typeof value !== "string") {
|
|
345
|
+
throw new Error(`${fieldName} must be a base64 string.`);
|
|
346
|
+
}
|
|
347
|
+
const normalizedValue = value.trim();
|
|
348
|
+
if (normalizedValue.length === 0) {
|
|
349
|
+
throw new Error(`${fieldName} must be a non-empty base64 string.`);
|
|
350
|
+
}
|
|
351
|
+
const decodedBuffer = Buffer.from(normalizedValue, "base64");
|
|
352
|
+
if (decodedBuffer.length === 0) {
|
|
353
|
+
throw new Error(`${fieldName} could not be decoded from base64.`);
|
|
354
|
+
}
|
|
355
|
+
const canonicalInput = normalizedValue.replace(/=+$/u, "");
|
|
356
|
+
const canonicalDecoded = decodedBuffer.toString("base64").replace(/=+$/u, "");
|
|
357
|
+
if (canonicalInput !== canonicalDecoded) {
|
|
358
|
+
throw new Error(`${fieldName} is not valid base64 content.`);
|
|
359
|
+
}
|
|
360
|
+
return decodedBuffer;
|
|
361
|
+
}
|
|
362
|
+
function equalsConstantTime(left, right) {
|
|
363
|
+
if (left.length !== right.length) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return timingSafeEqual(left, right);
|
|
367
|
+
}
|
|
368
|
+
function createResumeClientProof(sessionSecret, sessionId, clientNonce) {
|
|
369
|
+
return createHmac("sha256", sessionSecret).update("afx-resume-client-proof:v1").update(sessionId).update(clientNonce).digest();
|
|
370
|
+
}
|
|
371
|
+
function createResumeServerProof(resumedKey, sessionId, clientNonce) {
|
|
372
|
+
return createHmac("sha256", resumedKey).update("afx-resume-server-proof:v1").update(sessionId).update(clientNonce).digest();
|
|
373
|
+
}
|
|
374
|
+
function deriveSessionTicketSecret(baseKey) {
|
|
375
|
+
return createHmac("sha256", baseKey).update("afx-session-ticket:v1").digest();
|
|
376
|
+
}
|
|
377
|
+
function deriveResumedEncryptionKey(sessionSecret, clientNonce) {
|
|
378
|
+
const derivedKey = createHash("sha256").update("afx-resume-encryption-key:v1").update(sessionSecret).update(clientNonce).digest();
|
|
379
|
+
if (derivedKey.length !== ENCRYPTION_KEY_LENGTH) {
|
|
380
|
+
throw new Error("Failed to derive a valid resumed AES-256 key.");
|
|
381
|
+
}
|
|
382
|
+
return derivedKey;
|
|
383
|
+
}
|
|
336
384
|
function parseHandshakePayload(data) {
|
|
337
385
|
if (typeof data !== "object" || data === null) {
|
|
338
386
|
throw new Error("Invalid handshake payload format.");
|
|
339
387
|
}
|
|
340
388
|
const payload = data;
|
|
341
|
-
if (typeof payload.
|
|
342
|
-
|
|
389
|
+
if (typeof payload.type !== "string") {
|
|
390
|
+
if (typeof payload.publicKey === "string" && payload.publicKey.length > 0) {
|
|
391
|
+
return {
|
|
392
|
+
type: "hello",
|
|
393
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
394
|
+
publicKey: payload.publicKey
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
throw new Error("Handshake payload must include a valid type.");
|
|
398
|
+
}
|
|
399
|
+
const protocolVersion = payload.protocolVersion === void 0 ? HANDSHAKE_PROTOCOL_VERSION : payload.protocolVersion;
|
|
400
|
+
if (protocolVersion !== HANDSHAKE_PROTOCOL_VERSION) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Unsupported handshake protocol version: ${String(protocolVersion)}.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (payload.type === "hello") {
|
|
406
|
+
if (typeof payload.publicKey !== "string" || payload.publicKey.length === 0) {
|
|
407
|
+
throw new Error("Handshake hello payload must include a non-empty public key.");
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
type: "hello",
|
|
411
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
412
|
+
publicKey: payload.publicKey
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (payload.type === "resume") {
|
|
416
|
+
if (typeof payload.sessionId !== "string" || payload.sessionId.trim().length === 0) {
|
|
417
|
+
throw new Error("Handshake resume payload must include a non-empty sessionId.");
|
|
418
|
+
}
|
|
419
|
+
if (typeof payload.clientNonce !== "string" || payload.clientNonce.length === 0) {
|
|
420
|
+
throw new Error("Handshake resume payload must include a non-empty clientNonce.");
|
|
421
|
+
}
|
|
422
|
+
if (typeof payload.clientProof !== "string" || payload.clientProof.length === 0) {
|
|
423
|
+
throw new Error("Handshake resume payload must include a non-empty clientProof.");
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
type: "resume",
|
|
427
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
428
|
+
sessionId: payload.sessionId.trim(),
|
|
429
|
+
clientNonce: payload.clientNonce,
|
|
430
|
+
clientProof: payload.clientProof
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (payload.type === "resume-ack") {
|
|
434
|
+
if (typeof payload.ok !== "boolean") {
|
|
435
|
+
throw new Error("Handshake resume-ack payload must include boolean ok.");
|
|
436
|
+
}
|
|
437
|
+
const normalizedPayload = {
|
|
438
|
+
type: "resume-ack",
|
|
439
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
440
|
+
ok: payload.ok
|
|
441
|
+
};
|
|
442
|
+
if (typeof payload.sessionId === "string" && payload.sessionId.trim().length > 0) {
|
|
443
|
+
normalizedPayload.sessionId = payload.sessionId.trim();
|
|
444
|
+
}
|
|
445
|
+
if (typeof payload.serverProof === "string" && payload.serverProof.length > 0) {
|
|
446
|
+
normalizedPayload.serverProof = payload.serverProof;
|
|
447
|
+
}
|
|
448
|
+
if (typeof payload.reason === "string" && payload.reason.trim().length > 0) {
|
|
449
|
+
normalizedPayload.reason = payload.reason.trim();
|
|
450
|
+
}
|
|
451
|
+
return normalizedPayload;
|
|
452
|
+
}
|
|
453
|
+
throw new Error(`Unsupported handshake payload type: ${payload.type}.`);
|
|
454
|
+
}
|
|
455
|
+
function parseSessionTicketPayload(data) {
|
|
456
|
+
if (typeof data !== "object" || data === null) {
|
|
457
|
+
throw new Error("Invalid session ticket payload format.");
|
|
458
|
+
}
|
|
459
|
+
const payload = data;
|
|
460
|
+
if (payload.version !== SESSION_TICKET_VERSION) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Unsupported session ticket payload version: ${String(payload.version)}.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (typeof payload.sessionId !== "string" || payload.sessionId.trim().length === 0) {
|
|
466
|
+
throw new Error("Session ticket payload must include a non-empty sessionId.");
|
|
467
|
+
}
|
|
468
|
+
if (typeof payload.secret !== "string" || payload.secret.length === 0) {
|
|
469
|
+
throw new Error("Session ticket payload must include a non-empty secret.");
|
|
470
|
+
}
|
|
471
|
+
if (typeof payload.issuedAt !== "number" || !Number.isFinite(payload.issuedAt)) {
|
|
472
|
+
throw new Error("Session ticket payload issuedAt must be a finite number.");
|
|
473
|
+
}
|
|
474
|
+
if (typeof payload.expiresAt !== "number" || !Number.isFinite(payload.expiresAt)) {
|
|
475
|
+
throw new Error("Session ticket payload expiresAt must be a finite number.");
|
|
476
|
+
}
|
|
477
|
+
if (payload.expiresAt <= payload.issuedAt) {
|
|
478
|
+
throw new Error("Session ticket payload expiresAt must be greater than issuedAt.");
|
|
343
479
|
}
|
|
344
480
|
return {
|
|
345
|
-
|
|
481
|
+
version: SESSION_TICKET_VERSION,
|
|
482
|
+
sessionId: payload.sessionId.trim(),
|
|
483
|
+
secret: payload.secret,
|
|
484
|
+
issuedAt: payload.issuedAt,
|
|
485
|
+
expiresAt: payload.expiresAt
|
|
346
486
|
};
|
|
347
487
|
}
|
|
348
488
|
function deriveEncryptionKey(sharedSecret) {
|
|
@@ -412,6 +552,7 @@ var SecureServer = class {
|
|
|
412
552
|
adapter = null;
|
|
413
553
|
heartbeatConfig;
|
|
414
554
|
rateLimitConfig;
|
|
555
|
+
sessionResumptionConfig;
|
|
415
556
|
heartbeatIntervalHandle = null;
|
|
416
557
|
clientsById = /* @__PURE__ */ new Map();
|
|
417
558
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
@@ -433,10 +574,12 @@ var SecureServer = class {
|
|
|
433
574
|
clientIpByClientId = /* @__PURE__ */ new Map();
|
|
434
575
|
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
435
576
|
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
577
|
+
sessionTicketStore = /* @__PURE__ */ new Map();
|
|
436
578
|
constructor(options) {
|
|
437
|
-
const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
|
|
579
|
+
const { heartbeat, rateLimit, sessionResumption, adapter, ...socketServerOptions } = options;
|
|
438
580
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
439
581
|
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
582
|
+
this.sessionResumptionConfig = this.resolveSessionResumptionConfig(sessionResumption);
|
|
440
583
|
this.socketServer = new WebSocketServer(socketServerOptions);
|
|
441
584
|
this.bindSocketServerEvents();
|
|
442
585
|
this.startHeartbeatLoop();
|
|
@@ -522,8 +665,8 @@ var SecureServer = class {
|
|
|
522
665
|
this.errorHandlers.add(handler);
|
|
523
666
|
return this;
|
|
524
667
|
}
|
|
525
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
526
|
-
throw new Error(`The event "${
|
|
668
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
669
|
+
throw new Error(`The event "${event}" is reserved for internal use.`);
|
|
527
670
|
}
|
|
528
671
|
const typedHandler = handler;
|
|
529
672
|
const listeners = this.customEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
@@ -554,7 +697,7 @@ var SecureServer = class {
|
|
|
554
697
|
this.errorHandlers.delete(handler);
|
|
555
698
|
return this;
|
|
556
699
|
}
|
|
557
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
700
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
558
701
|
return this;
|
|
559
702
|
}
|
|
560
703
|
const listeners = this.customEventHandlers.get(event);
|
|
@@ -687,6 +830,7 @@ var SecureServer = class {
|
|
|
687
830
|
this.rateLimitBucketsByClientId.clear();
|
|
688
831
|
this.rateLimitBucketsByIp.clear();
|
|
689
832
|
this.clientIpByClientId.clear();
|
|
833
|
+
this.sessionTicketStore.clear();
|
|
690
834
|
this.socketServer.close();
|
|
691
835
|
} catch (error) {
|
|
692
836
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
@@ -763,6 +907,94 @@ var SecureServer = class {
|
|
|
763
907
|
disconnectReason
|
|
764
908
|
};
|
|
765
909
|
}
|
|
910
|
+
resolveSessionResumptionConfig(sessionResumptionOptions) {
|
|
911
|
+
const ticketTtlMs = sessionResumptionOptions?.ticketTtlMs ?? DEFAULT_SESSION_TICKET_TTL_MS;
|
|
912
|
+
const maxCachedTickets = sessionResumptionOptions?.maxCachedTickets ?? DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE;
|
|
913
|
+
if (!Number.isFinite(ticketTtlMs) || ticketTtlMs <= 0) {
|
|
914
|
+
throw new Error(
|
|
915
|
+
"Server sessionResumption ticketTtlMs must be a positive number."
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (!Number.isInteger(maxCachedTickets) || maxCachedTickets <= 0) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
"Server sessionResumption maxCachedTickets must be a positive integer."
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
enabled: sessionResumptionOptions?.enabled ?? DEFAULT_SESSION_RESUMPTION_ENABLED,
|
|
925
|
+
ticketTtlMs,
|
|
926
|
+
maxCachedTickets
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
pruneExpiredSessionTickets(now) {
|
|
930
|
+
for (const [sessionId, ticketRecord] of this.sessionTicketStore.entries()) {
|
|
931
|
+
if (ticketRecord.expiresAt <= now) {
|
|
932
|
+
this.sessionTicketStore.delete(sessionId);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
evictSessionTicketsIfNeeded() {
|
|
937
|
+
while (this.sessionTicketStore.size > this.sessionResumptionConfig.maxCachedTickets) {
|
|
938
|
+
let oldestSessionId = null;
|
|
939
|
+
let oldestIssuedAt = Number.POSITIVE_INFINITY;
|
|
940
|
+
for (const [sessionId, ticketRecord] of this.sessionTicketStore.entries()) {
|
|
941
|
+
if (ticketRecord.issuedAt < oldestIssuedAt) {
|
|
942
|
+
oldestIssuedAt = ticketRecord.issuedAt;
|
|
943
|
+
oldestSessionId = sessionId;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (!oldestSessionId) {
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
this.sessionTicketStore.delete(oldestSessionId);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
getSessionTicket(sessionId) {
|
|
953
|
+
const now = Date.now();
|
|
954
|
+
this.pruneExpiredSessionTickets(now);
|
|
955
|
+
const ticketRecord = this.sessionTicketStore.get(sessionId);
|
|
956
|
+
if (!ticketRecord) {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
if (ticketRecord.expiresAt <= now) {
|
|
960
|
+
this.sessionTicketStore.delete(sessionId);
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
return ticketRecord;
|
|
964
|
+
}
|
|
965
|
+
issueSessionTicket(socket, baseKey) {
|
|
966
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const now = Date.now();
|
|
970
|
+
this.pruneExpiredSessionTickets(now);
|
|
971
|
+
const sessionId = randomUUID();
|
|
972
|
+
const sessionSecret = deriveSessionTicketSecret(baseKey);
|
|
973
|
+
const expiresAt = now + this.sessionResumptionConfig.ticketTtlMs;
|
|
974
|
+
const ticketRecord = {
|
|
975
|
+
sessionId,
|
|
976
|
+
secret: sessionSecret,
|
|
977
|
+
issuedAt: now,
|
|
978
|
+
expiresAt
|
|
979
|
+
};
|
|
980
|
+
this.sessionTicketStore.set(sessionId, ticketRecord);
|
|
981
|
+
this.evictSessionTicketsIfNeeded();
|
|
982
|
+
const ticketPayload = {
|
|
983
|
+
version: SESSION_TICKET_VERSION,
|
|
984
|
+
sessionId,
|
|
985
|
+
secret: sessionSecret.toString("base64"),
|
|
986
|
+
issuedAt: now,
|
|
987
|
+
expiresAt
|
|
988
|
+
};
|
|
989
|
+
void this.sendOrQueuePayload(socket, {
|
|
990
|
+
event: INTERNAL_SESSION_TICKET_EVENT,
|
|
991
|
+
data: ticketPayload
|
|
992
|
+
}).catch((error) => {
|
|
993
|
+
this.notifyError(
|
|
994
|
+
normalizeToError(error, "Failed to deliver secure session ticket.")
|
|
995
|
+
);
|
|
996
|
+
});
|
|
997
|
+
}
|
|
766
998
|
createRateLimitBucket(now) {
|
|
767
999
|
return {
|
|
768
1000
|
windowStartedAt: now,
|
|
@@ -1133,6 +1365,14 @@ var SecureServer = class {
|
|
|
1133
1365
|
await this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
1134
1366
|
return;
|
|
1135
1367
|
}
|
|
1368
|
+
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1369
|
+
this.notifyError(
|
|
1370
|
+
new Error(
|
|
1371
|
+
`Client ${client.id} attempted to send reserved internal session ticket event.`
|
|
1372
|
+
)
|
|
1373
|
+
);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1136
1376
|
const interceptedData = await this.applyMessageMiddleware(
|
|
1137
1377
|
"incoming",
|
|
1138
1378
|
client,
|
|
@@ -1446,10 +1686,104 @@ var SecureServer = class {
|
|
|
1446
1686
|
this.sendRaw(
|
|
1447
1687
|
socket,
|
|
1448
1688
|
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
1689
|
+
type: "hello",
|
|
1690
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
1449
1691
|
publicKey: localPublicKey
|
|
1450
1692
|
})
|
|
1451
1693
|
);
|
|
1452
1694
|
}
|
|
1695
|
+
sendResumeAck(socket, payload) {
|
|
1696
|
+
const responsePayload = {
|
|
1697
|
+
type: "resume-ack",
|
|
1698
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
1699
|
+
ok: payload.ok
|
|
1700
|
+
};
|
|
1701
|
+
if (payload.sessionId !== void 0 && payload.sessionId.length > 0) {
|
|
1702
|
+
responsePayload.sessionId = payload.sessionId;
|
|
1703
|
+
}
|
|
1704
|
+
if (payload.serverProof !== void 0 && payload.serverProof.length > 0) {
|
|
1705
|
+
responsePayload.serverProof = payload.serverProof;
|
|
1706
|
+
}
|
|
1707
|
+
if (payload.reason !== void 0 && payload.reason.length > 0) {
|
|
1708
|
+
responsePayload.reason = payload.reason;
|
|
1709
|
+
}
|
|
1710
|
+
this.sendRaw(
|
|
1711
|
+
socket,
|
|
1712
|
+
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, responsePayload)
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
handleResumeHandshake(client, payload) {
|
|
1716
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
1717
|
+
this.sendResumeAck(client.socket, {
|
|
1718
|
+
ok: false,
|
|
1719
|
+
reason: "Session resumption is disabled."
|
|
1720
|
+
});
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
const ticketRecord = this.getSessionTicket(payload.sessionId);
|
|
1724
|
+
if (!ticketRecord) {
|
|
1725
|
+
this.sendResumeAck(client.socket, {
|
|
1726
|
+
ok: false,
|
|
1727
|
+
reason: "Session ticket is unknown or expired."
|
|
1728
|
+
});
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
try {
|
|
1732
|
+
const clientNonce = decodeBase64ToBuffer(
|
|
1733
|
+
payload.clientNonce,
|
|
1734
|
+
"Handshake resume clientNonce"
|
|
1735
|
+
);
|
|
1736
|
+
if (clientNonce.length !== RESUMPTION_NONCE_LENGTH) {
|
|
1737
|
+
throw new Error(
|
|
1738
|
+
`Handshake resume clientNonce must be ${RESUMPTION_NONCE_LENGTH} bytes.`
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const receivedProof = decodeBase64ToBuffer(
|
|
1742
|
+
payload.clientProof,
|
|
1743
|
+
"Handshake resume clientProof"
|
|
1744
|
+
);
|
|
1745
|
+
const expectedProof = createResumeClientProof(
|
|
1746
|
+
ticketRecord.secret,
|
|
1747
|
+
ticketRecord.sessionId,
|
|
1748
|
+
clientNonce
|
|
1749
|
+
);
|
|
1750
|
+
if (!equalsConstantTime(receivedProof, expectedProof)) {
|
|
1751
|
+
this.sendResumeAck(client.socket, {
|
|
1752
|
+
ok: false,
|
|
1753
|
+
reason: "Session resumption proof validation failed."
|
|
1754
|
+
});
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
this.sessionTicketStore.delete(ticketRecord.sessionId);
|
|
1758
|
+
const resumedKey = deriveResumedEncryptionKey(ticketRecord.secret, clientNonce);
|
|
1759
|
+
const serverProof = createResumeServerProof(
|
|
1760
|
+
resumedKey,
|
|
1761
|
+
ticketRecord.sessionId,
|
|
1762
|
+
clientNonce
|
|
1763
|
+
).toString("base64");
|
|
1764
|
+
const handshakeState = this.handshakeStateBySocket.get(client.socket);
|
|
1765
|
+
if (!handshakeState) {
|
|
1766
|
+
throw new Error(`Missing handshake state for client ${client.id}.`);
|
|
1767
|
+
}
|
|
1768
|
+
this.sharedSecretBySocket.set(client.socket, resumedKey);
|
|
1769
|
+
this.encryptionKeyBySocket.set(client.socket, resumedKey);
|
|
1770
|
+
handshakeState.isReady = true;
|
|
1771
|
+
this.sendResumeAck(client.socket, {
|
|
1772
|
+
ok: true,
|
|
1773
|
+
sessionId: ticketRecord.sessionId,
|
|
1774
|
+
serverProof
|
|
1775
|
+
});
|
|
1776
|
+
void this.flushQueuedPayloads(client.socket);
|
|
1777
|
+
this.notifyReady(client);
|
|
1778
|
+
this.issueSessionTicket(client.socket, resumedKey);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
this.sendResumeAck(client.socket, {
|
|
1781
|
+
ok: false,
|
|
1782
|
+
reason: "Session resumption payload was invalid."
|
|
1783
|
+
});
|
|
1784
|
+
this.notifyError(normalizeToError(error, "Failed to resume secure server session."));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1453
1787
|
handleInternalHandshake(client, data) {
|
|
1454
1788
|
try {
|
|
1455
1789
|
const payload = parseHandshakePayload(data);
|
|
@@ -1460,14 +1794,22 @@ var SecureServer = class {
|
|
|
1460
1794
|
if (handshakeState.isReady) {
|
|
1461
1795
|
return;
|
|
1462
1796
|
}
|
|
1797
|
+
if (payload.type === "resume") {
|
|
1798
|
+
this.handleResumeHandshake(client, payload);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (payload.type === "resume-ack") {
|
|
1802
|
+
throw new Error("SecureServer received unexpected resume-ack handshake payload.");
|
|
1803
|
+
}
|
|
1463
1804
|
const remotePublicKey = Buffer.from(payload.publicKey, "base64");
|
|
1464
1805
|
const sharedSecret = handshakeState.ecdh.computeSecret(remotePublicKey);
|
|
1465
1806
|
const encryptionKey = deriveEncryptionKey(sharedSecret);
|
|
1466
1807
|
this.sharedSecretBySocket.set(client.socket, sharedSecret);
|
|
1467
1808
|
this.encryptionKeyBySocket.set(client.socket, encryptionKey);
|
|
1468
1809
|
handshakeState.isReady = true;
|
|
1469
|
-
this.flushQueuedPayloads(client.socket);
|
|
1810
|
+
void this.flushQueuedPayloads(client.socket);
|
|
1470
1811
|
this.notifyReady(client);
|
|
1812
|
+
this.issueSessionTicket(client.socket, encryptionKey);
|
|
1471
1813
|
} catch (error) {
|
|
1472
1814
|
this.notifyError(normalizeToError(error, "Failed to complete server handshake."));
|
|
1473
1815
|
}
|
|
@@ -1681,6 +2023,9 @@ var SecureClient = class {
|
|
|
1681
2023
|
this.url = url;
|
|
1682
2024
|
this.options = options;
|
|
1683
2025
|
this.reconnectConfig = this.resolveReconnectConfig(this.options.reconnect);
|
|
2026
|
+
this.sessionResumptionConfig = this.resolveSessionResumptionConfig(
|
|
2027
|
+
this.options.sessionResumption
|
|
2028
|
+
);
|
|
1684
2029
|
if (this.options.autoConnect ?? true) {
|
|
1685
2030
|
this.connect();
|
|
1686
2031
|
}
|
|
@@ -1689,6 +2034,7 @@ var SecureClient = class {
|
|
|
1689
2034
|
options;
|
|
1690
2035
|
socket = null;
|
|
1691
2036
|
reconnectConfig;
|
|
2037
|
+
sessionResumptionConfig;
|
|
1692
2038
|
reconnectAttemptCount = 0;
|
|
1693
2039
|
reconnectTimer = null;
|
|
1694
2040
|
isManualDisconnectRequested = false;
|
|
@@ -1700,6 +2046,7 @@ var SecureClient = class {
|
|
|
1700
2046
|
handshakeState = null;
|
|
1701
2047
|
pendingPayloadQueue = [];
|
|
1702
2048
|
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
2049
|
+
sessionTicket = null;
|
|
1703
2050
|
get readyState() {
|
|
1704
2051
|
return this.socket?.readyState ?? null;
|
|
1705
2052
|
}
|
|
@@ -1758,8 +2105,8 @@ var SecureClient = class {
|
|
|
1758
2105
|
this.errorHandlers.add(handler);
|
|
1759
2106
|
return this;
|
|
1760
2107
|
}
|
|
1761
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
1762
|
-
throw new Error(`The event "${
|
|
2108
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2109
|
+
throw new Error(`The event "${event}" is reserved for internal use.`);
|
|
1763
2110
|
}
|
|
1764
2111
|
const typedHandler = handler;
|
|
1765
2112
|
const listeners = this.customEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
@@ -1790,7 +2137,7 @@ var SecureClient = class {
|
|
|
1790
2137
|
this.errorHandlers.delete(handler);
|
|
1791
2138
|
return this;
|
|
1792
2139
|
}
|
|
1793
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
2140
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1794
2141
|
return this;
|
|
1795
2142
|
}
|
|
1796
2143
|
const listeners = this.customEventHandlers.get(event);
|
|
@@ -1896,6 +2243,24 @@ var SecureClient = class {
|
|
|
1896
2243
|
maxAttempts
|
|
1897
2244
|
};
|
|
1898
2245
|
}
|
|
2246
|
+
resolveSessionResumptionConfig(sessionResumptionOptions) {
|
|
2247
|
+
if (typeof sessionResumptionOptions === "boolean") {
|
|
2248
|
+
return {
|
|
2249
|
+
enabled: sessionResumptionOptions,
|
|
2250
|
+
maxAcceptedTicketTtlMs: DEFAULT_SESSION_TICKET_TTL_MS
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
const maxAcceptedTicketTtlMs = sessionResumptionOptions?.maxAcceptedTicketTtlMs ?? DEFAULT_SESSION_TICKET_TTL_MS;
|
|
2254
|
+
if (!Number.isFinite(maxAcceptedTicketTtlMs) || maxAcceptedTicketTtlMs <= 0) {
|
|
2255
|
+
throw new Error(
|
|
2256
|
+
"Client sessionResumption maxAcceptedTicketTtlMs must be a positive number."
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
return {
|
|
2260
|
+
enabled: sessionResumptionOptions?.enabled ?? DEFAULT_SESSION_RESUMPTION_ENABLED,
|
|
2261
|
+
maxAcceptedTicketTtlMs
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
1899
2264
|
scheduleReconnect() {
|
|
1900
2265
|
if (!this.reconnectConfig.enabled || this.reconnectTimer) {
|
|
1901
2266
|
return;
|
|
@@ -1943,7 +2308,6 @@ var SecureClient = class {
|
|
|
1943
2308
|
socket.on("open", () => {
|
|
1944
2309
|
this.clearReconnectTimer();
|
|
1945
2310
|
this.reconnectAttemptCount = 0;
|
|
1946
|
-
this.sendInternalHandshake();
|
|
1947
2311
|
this.notifyConnect();
|
|
1948
2312
|
});
|
|
1949
2313
|
socket.on("message", (rawData) => {
|
|
@@ -1999,6 +2363,10 @@ var SecureClient = class {
|
|
|
1999
2363
|
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
2000
2364
|
return;
|
|
2001
2365
|
}
|
|
2366
|
+
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2367
|
+
this.handleSessionTicket(decryptedEnvelope.data);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2002
2370
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data);
|
|
2003
2371
|
} catch (error) {
|
|
2004
2372
|
this.notifyError(normalizeToError(error, "Failed to process incoming client message."));
|
|
@@ -2212,11 +2580,45 @@ var SecureClient = class {
|
|
|
2212
2580
|
}
|
|
2213
2581
|
this.pendingRpcRequests.clear();
|
|
2214
2582
|
}
|
|
2583
|
+
handleSessionTicket(data) {
|
|
2584
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
const ticketPayload = parseSessionTicketPayload(data);
|
|
2589
|
+
const now = Date.now();
|
|
2590
|
+
if (ticketPayload.expiresAt <= now) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const ticketTtlMs = ticketPayload.expiresAt - ticketPayload.issuedAt;
|
|
2594
|
+
if (ticketTtlMs > this.sessionResumptionConfig.maxAcceptedTicketTtlMs) {
|
|
2595
|
+
throw new Error("Session ticket TTL exceeds client trust policy.");
|
|
2596
|
+
}
|
|
2597
|
+
const sessionSecret = decodeBase64ToBuffer(
|
|
2598
|
+
ticketPayload.secret,
|
|
2599
|
+
"Session ticket secret"
|
|
2600
|
+
);
|
|
2601
|
+
if (sessionSecret.length !== ENCRYPTION_KEY_LENGTH) {
|
|
2602
|
+
throw new Error("Session ticket secret has invalid length.");
|
|
2603
|
+
}
|
|
2604
|
+
this.sessionTicket = {
|
|
2605
|
+
sessionId: ticketPayload.sessionId,
|
|
2606
|
+
secret: sessionSecret,
|
|
2607
|
+
issuedAt: ticketPayload.issuedAt,
|
|
2608
|
+
expiresAt: ticketPayload.expiresAt
|
|
2609
|
+
};
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
this.notifyError(normalizeToError(error, "Failed to process session ticket payload."));
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2215
2614
|
createClientHandshakeState() {
|
|
2216
2615
|
const { ecdh, localPublicKey } = createEphemeralHandshakeState();
|
|
2217
2616
|
return {
|
|
2218
2617
|
ecdh,
|
|
2219
2618
|
localPublicKey,
|
|
2619
|
+
clientHelloSent: false,
|
|
2620
|
+
pendingServerPublicKey: null,
|
|
2621
|
+
resumeAttempt: null,
|
|
2220
2622
|
isReady: false,
|
|
2221
2623
|
sharedSecret: null,
|
|
2222
2624
|
encryptionKey: null
|
|
@@ -2230,15 +2632,171 @@ var SecureClient = class {
|
|
|
2230
2632
|
if (!this.handshakeState) {
|
|
2231
2633
|
throw new Error("Missing client handshake state.");
|
|
2232
2634
|
}
|
|
2635
|
+
if (this.handshakeState.clientHelloSent) {
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2233
2638
|
this.socket.send(
|
|
2234
2639
|
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
2640
|
+
type: "hello",
|
|
2641
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
2235
2642
|
publicKey: this.handshakeState.localPublicKey
|
|
2236
2643
|
})
|
|
2237
2644
|
);
|
|
2645
|
+
this.handshakeState.clientHelloSent = true;
|
|
2238
2646
|
} catch (error) {
|
|
2239
2647
|
this.notifyError(normalizeToError(error, "Failed to send client handshake payload."));
|
|
2240
2648
|
}
|
|
2241
2649
|
}
|
|
2650
|
+
shouldAttemptSessionResumption() {
|
|
2651
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
2652
|
+
return false;
|
|
2653
|
+
}
|
|
2654
|
+
const sessionTicket = this.sessionTicket;
|
|
2655
|
+
if (!sessionTicket) {
|
|
2656
|
+
return false;
|
|
2657
|
+
}
|
|
2658
|
+
const now = Date.now();
|
|
2659
|
+
if (sessionTicket.expiresAt <= now) {
|
|
2660
|
+
this.sessionTicket = null;
|
|
2661
|
+
return false;
|
|
2662
|
+
}
|
|
2663
|
+
const ticketTtlMs = sessionTicket.expiresAt - sessionTicket.issuedAt;
|
|
2664
|
+
if (ticketTtlMs > this.sessionResumptionConfig.maxAcceptedTicketTtlMs) {
|
|
2665
|
+
this.sessionTicket = null;
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
return true;
|
|
2669
|
+
}
|
|
2670
|
+
sendResumeHandshake() {
|
|
2671
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
if (!this.handshakeState || !this.sessionTicket) {
|
|
2675
|
+
return false;
|
|
2676
|
+
}
|
|
2677
|
+
if (this.handshakeState.clientHelloSent) {
|
|
2678
|
+
return false;
|
|
2679
|
+
}
|
|
2680
|
+
if (this.handshakeState.resumeAttempt?.status === "pending") {
|
|
2681
|
+
return true;
|
|
2682
|
+
}
|
|
2683
|
+
try {
|
|
2684
|
+
const clientNonce = randomBytes(RESUMPTION_NONCE_LENGTH);
|
|
2685
|
+
const resumedKey = deriveResumedEncryptionKey(this.sessionTicket.secret, clientNonce);
|
|
2686
|
+
const clientProof = createResumeClientProof(
|
|
2687
|
+
this.sessionTicket.secret,
|
|
2688
|
+
this.sessionTicket.sessionId,
|
|
2689
|
+
clientNonce
|
|
2690
|
+
);
|
|
2691
|
+
this.socket.send(
|
|
2692
|
+
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
2693
|
+
type: "resume",
|
|
2694
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
2695
|
+
sessionId: this.sessionTicket.sessionId,
|
|
2696
|
+
clientNonce: clientNonce.toString("base64"),
|
|
2697
|
+
clientProof: clientProof.toString("base64")
|
|
2698
|
+
})
|
|
2699
|
+
);
|
|
2700
|
+
this.handshakeState.resumeAttempt = {
|
|
2701
|
+
status: "pending",
|
|
2702
|
+
sessionId: this.sessionTicket.sessionId,
|
|
2703
|
+
clientNonce,
|
|
2704
|
+
resumedKey
|
|
2705
|
+
};
|
|
2706
|
+
return true;
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
this.notifyError(normalizeToError(error, "Failed to dispatch resume handshake payload."));
|
|
2709
|
+
this.sessionTicket = null;
|
|
2710
|
+
this.handshakeState.resumeAttempt = null;
|
|
2711
|
+
return false;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
completeFullHandshake(serverPublicKey) {
|
|
2715
|
+
if (!this.handshakeState) {
|
|
2716
|
+
throw new Error("Missing client handshake state.");
|
|
2717
|
+
}
|
|
2718
|
+
if (this.handshakeState.isReady) {
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
this.sendInternalHandshake();
|
|
2722
|
+
const remotePublicKey = Buffer.from(serverPublicKey, "base64");
|
|
2723
|
+
const sharedSecret = this.handshakeState.ecdh.computeSecret(remotePublicKey);
|
|
2724
|
+
this.handshakeState.sharedSecret = sharedSecret;
|
|
2725
|
+
this.handshakeState.encryptionKey = deriveEncryptionKey(sharedSecret);
|
|
2726
|
+
this.handshakeState.resumeAttempt = null;
|
|
2727
|
+
this.handshakeState.pendingServerPublicKey = null;
|
|
2728
|
+
this.handshakeState.isReady = true;
|
|
2729
|
+
void this.flushPendingPayloadQueue();
|
|
2730
|
+
this.notifyReady();
|
|
2731
|
+
}
|
|
2732
|
+
fallbackToFullHandshake() {
|
|
2733
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
if (this.handshakeState.resumeAttempt) {
|
|
2737
|
+
this.handshakeState.resumeAttempt.status = "failed";
|
|
2738
|
+
}
|
|
2739
|
+
const pendingServerPublicKey = this.handshakeState.pendingServerPublicKey;
|
|
2740
|
+
if (pendingServerPublicKey) {
|
|
2741
|
+
this.completeFullHandshake(pendingServerPublicKey);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
this.sendInternalHandshake();
|
|
2745
|
+
}
|
|
2746
|
+
handleServerHelloHandshake(payload) {
|
|
2747
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
this.handshakeState.pendingServerPublicKey = payload.publicKey;
|
|
2751
|
+
if (this.shouldAttemptSessionResumption() && this.sendResumeHandshake()) {
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
this.completeFullHandshake(payload.publicKey);
|
|
2755
|
+
}
|
|
2756
|
+
handleResumeAckHandshake(payload) {
|
|
2757
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
const resumeAttempt = this.handshakeState.resumeAttempt;
|
|
2761
|
+
if (!resumeAttempt || resumeAttempt.status !== "pending") {
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
if (!payload.ok) {
|
|
2765
|
+
this.sessionTicket = null;
|
|
2766
|
+
this.fallbackToFullHandshake();
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
if (payload.sessionId !== resumeAttempt.sessionId || !payload.serverProof) {
|
|
2770
|
+
this.sessionTicket = null;
|
|
2771
|
+
this.fallbackToFullHandshake();
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
try {
|
|
2775
|
+
const receivedServerProof = decodeBase64ToBuffer(
|
|
2776
|
+
payload.serverProof,
|
|
2777
|
+
"Handshake resume-ack serverProof"
|
|
2778
|
+
);
|
|
2779
|
+
const expectedServerProof = createResumeServerProof(
|
|
2780
|
+
resumeAttempt.resumedKey,
|
|
2781
|
+
resumeAttempt.sessionId,
|
|
2782
|
+
resumeAttempt.clientNonce
|
|
2783
|
+
);
|
|
2784
|
+
if (!equalsConstantTime(receivedServerProof, expectedServerProof)) {
|
|
2785
|
+
throw new Error("Resume server proof validation failed.");
|
|
2786
|
+
}
|
|
2787
|
+
this.handshakeState.sharedSecret = resumeAttempt.resumedKey;
|
|
2788
|
+
this.handshakeState.encryptionKey = resumeAttempt.resumedKey;
|
|
2789
|
+
this.handshakeState.pendingServerPublicKey = null;
|
|
2790
|
+
resumeAttempt.status = "accepted";
|
|
2791
|
+
this.handshakeState.isReady = true;
|
|
2792
|
+
void this.flushPendingPayloadQueue();
|
|
2793
|
+
this.notifyReady();
|
|
2794
|
+
} catch (error) {
|
|
2795
|
+
this.notifyError(normalizeToError(error, "Failed to verify resume server proof."));
|
|
2796
|
+
this.sessionTicket = null;
|
|
2797
|
+
this.fallbackToFullHandshake();
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2242
2800
|
handleInternalHandshake(data) {
|
|
2243
2801
|
try {
|
|
2244
2802
|
const payload = parseHandshakePayload(data);
|
|
@@ -2248,13 +2806,15 @@ var SecureClient = class {
|
|
|
2248
2806
|
if (this.handshakeState.isReady) {
|
|
2249
2807
|
return;
|
|
2250
2808
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2809
|
+
if (payload.type === "hello") {
|
|
2810
|
+
this.handleServerHelloHandshake(payload);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
if (payload.type === "resume-ack") {
|
|
2814
|
+
this.handleResumeAckHandshake(payload);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
throw new Error("SecureClient received unexpected resume request handshake payload.");
|
|
2258
2818
|
} catch (error) {
|
|
2259
2819
|
this.notifyError(normalizeToError(error, "Failed to complete client handshake."));
|
|
2260
2820
|
}
|