@floegence/flowersec-core 3.2.1 → 5.0.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 +57 -200
- package/THIRD_PARTY_NOTICES.md +11 -11
- package/dist/browser/index.d.ts +0 -5
- package/dist/browser/index.js +0 -3
- package/dist/cli.js +93 -64
- package/dist/facade.d.ts +0 -17
- package/dist/facade.js +0 -11
- package/dist/generated/unicode151.d.ts +2 -0
- package/dist/generated/unicode151.js +83 -0
- package/dist/interop/proxyServerPeer.js +34 -20
- package/dist/interop/serverParityPeer.js +11 -252
- package/dist/node/acceptor.d.ts +1 -57
- package/dist/node/acceptor.js +3 -420
- package/dist/node/acceptorV3.js +2 -2
- package/dist/node/connectSessionV3.js +2 -2
- package/dist/node/index.d.ts +2 -17
- package/dist/node/index.js +1 -9
- package/dist/node/nativeTransportAddon.d.ts +7 -36
- package/dist/node/nativeTransportAddon.js +7 -27
- package/dist/node/proxyServer.d.ts +2 -2
- package/dist/node/proxyServer.js +2 -2
- package/dist/node/rawQuicAdapterV3.d.ts +2 -2
- package/dist/node/rawQuicAdapterV3.js +1 -1
- package/dist/node/rawQuicServerV3.d.ts +2 -2
- package/dist/node/rawQuicServerV3.js +1 -1
- package/dist/node/rawQuicTls.d.ts +2 -0
- package/dist/node/{rawQuicAdapter.js → rawQuicTls.js} +30 -45
- package/dist/node/tunnelRuntimeV3.js +2 -2
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.js +1 -1
- package/dist/proxy/integration.d.ts +5 -5
- package/dist/proxy/integration.js +2 -2
- package/dist/proxy/runtime.js +11 -26
- package/dist/proxy/serviceWorkerRuntime.d.ts +7 -0
- package/dist/proxy/serviceWorkerRuntime.js +75 -0
- package/dist/proxy/types.d.ts +0 -1
- package/dist/proxy/windowBridge.d.ts +2 -0
- package/dist/proxy/windowBridge.js +53 -6
- package/dist/public/contract.d.ts +1 -3
- package/dist/public/streamHandlers.d.ts +0 -3
- package/dist/public/streamHandlers.js +2 -4
- package/dist/public/streamMetadata.js +2 -2
- package/dist/runtime/errors.d.ts +2 -2
- package/dist/testSupport/tlsFixture.d.ts +2 -0
- package/dist/testSupport/tlsFixture.js +8 -0
- package/dist/utils/errors.d.ts +0 -2
- package/dist/utils/errors.js +0 -80
- package/dist/v3/artifact.js +17 -3
- package/dist/v3/carrier.d.ts +2 -2
- package/dist/v3/controller.js +2 -6
- package/dist/v3/protocol.js +2 -103
- package/dist/v3/retryDisposition.d.ts +2 -2
- package/dist/v3/retryDisposition.js +0 -7
- package/dist/v3/security.d.ts +1 -10
- package/dist/v3/security.js +2 -9
- package/dist/vendor/tr46.d.ts +4 -0
- package/dist/vendor/tr46.js +37 -32
- package/package.json +19 -17
- package/sbom/cyclonedx.json +62 -62
- package/sbom/spdx.json +65 -65
- package/dist/browser/connectSession.d.ts +0 -13
- package/dist/browser/connectSession.js +0 -40
- package/dist/browser/runtimeCapability.d.ts +0 -13
- package/dist/browser/runtimeCapability.js +0 -27
- package/dist/browser/sessionRuntime.d.ts +0 -2
- package/dist/browser/sessionRuntime.js +0 -21
- package/dist/browser/v2.d.ts +0 -3
- package/dist/browser/v2.js +0 -2
- package/dist/browser/webTransportClient.d.ts +0 -8
- package/dist/browser/webTransportClient.js +0 -55
- package/dist/connectionController.d.ts +0 -55
- package/dist/connectionController.js +0 -441
- package/dist/connector/adapters/rawQuicCandidate.d.ts +0 -5
- package/dist/connector/adapters/rawQuicCandidate.js +0 -71
- package/dist/connector/adapters/webSocketCandidate.d.ts +0 -7
- package/dist/connector/adapters/webSocketCandidate.js +0 -116
- package/dist/connector/adapters/webTransportCandidate.d.ts +0 -5
- package/dist/connector/adapters/webTransportCandidate.js +0 -73
- package/dist/connector/admissionCommit.d.ts +0 -31
- package/dist/connector/admissionCommit.js +0 -239
- package/dist/connector/sessionAcceptor.d.ts +0 -38
- package/dist/connector/sessionAcceptor.js +0 -158
- package/dist/connector/sessionConfig.d.ts +0 -3
- package/dist/connector/sessionConfig.js +0 -30
- package/dist/connector/sessionConnector.d.ts +0 -47
- package/dist/connector/sessionConnector.js +0 -606
- package/dist/node/connectSession.d.ts +0 -23
- package/dist/node/connectSession.js +0 -93
- package/dist/node/controlplane.d.ts +0 -137
- package/dist/node/controlplane.js +0 -443
- package/dist/node/rawQuicAdapter.d.ts +0 -9
- package/dist/node/rawQuicServer.d.ts +0 -24
- package/dist/node/rawQuicServer.js +0 -37
- package/dist/node/runtimeCapability.d.ts +0 -14
- package/dist/node/runtimeCapability.js +0 -24
- package/dist/node/sessionRuntime.d.ts +0 -2
- package/dist/node/sessionRuntime.js +0 -13
- package/dist/node/tunnelRuntime.d.ts +0 -42
- package/dist/node/tunnelRuntime.js +0 -559
- package/dist/node/v2.d.ts +0 -11
- package/dist/node/v2.js +0 -6
- package/dist/node/webSocketServer.d.ts +0 -24
- package/dist/node/webSocketServer.js +0 -135
- package/dist/public/artifact.d.ts +0 -11
- package/dist/public/artifact.js +0 -44
- package/dist/public/artifactLease.d.ts +0 -10
- package/dist/public/artifactLease.js +0 -41
- package/dist/public/connectError.d.ts +0 -6
- package/dist/public/connectError.js +0 -9
- package/dist/transport/webSocketAdapter.d.ts +0 -31
- package/dist/transport/webSocketAdapter.js +0 -370
- package/dist/transport/webTransportAdapter.d.ts +0 -27
- package/dist/transport/webTransportAdapter.js +0 -534
- package/dist/v2/admissionError.d.ts +0 -4
- package/dist/v2/admissionError.js +0 -8
- package/dist/v2/artifact.d.ts +0 -128
- package/dist/v2/artifact.js +0 -1131
- package/dist/v2/artifactLease.d.ts +0 -1
- package/dist/v2/artifactLease.js +0 -7
- package/dist/v2/capability.d.ts +0 -29
- package/dist/v2/capability.js +0 -190
- package/dist/v2/carrier.d.ts +0 -84
- package/dist/v2/carrier.js +0 -411
- package/dist/v2/contract.d.ts +0 -46
- package/dist/v2/contract.js +0 -1
- package/dist/v2/handshake.d.ts +0 -80
- package/dist/v2/handshake.js +0 -470
- package/dist/v2/index.d.ts +0 -14
- package/dist/v2/index.js +0 -7
- package/dist/v2/opaqueArtifact.d.ts +0 -1
- package/dist/v2/opaqueArtifact.js +0 -2
- package/dist/v2/protocol.d.ts +0 -99
- package/dist/v2/protocol.js +0 -737
- package/dist/v2/publicSession.d.ts +0 -1
- package/dist/v2/publicSession.js +0 -281
- package/dist/v2/retryDisposition.d.ts +0 -16
- package/dist/v2/retryDisposition.js +0 -30
- package/dist/v2/session.d.ts +0 -310
- package/dist/v2/session.js +0 -2127
- package/dist/v2/streamLifetimeLedger.d.ts +0 -31
- package/dist/v2/streamLifetimeLedger.js +0 -131
- package/dist/v2/streamMetadata.d.ts +0 -1
- package/dist/v2/streamMetadata.js +0 -7
- package/dist/v2/unreliableMessage.d.ts +0 -43
- package/dist/v2/unreliableMessage.js +0 -254
package/dist/v2/session.js
DELETED
|
@@ -1,2127 +0,0 @@
|
|
|
1
|
-
import { RpcClient } from "../rpc/client.js";
|
|
2
|
-
import { RpcRouter, RpcServer } from "../rpc/server.js";
|
|
3
|
-
import { CarrierError } from "./carrier.js";
|
|
4
|
-
import { computeClientConfirmV2, computeHandshakeH0V2, computeHandshakeH1V2, computeHandshakeH2V2, computeHandshakeH3V2, computeServerConfirmV2, computeSharedSecretV2, decodeClientFinishedV2, decodeClientInitV2, decodeHandshakeFrameV2, decodeServerFinishedV2, deriveHandshakePRKV2, deriveSessionPRKV2, encodeClientFinishedCoreV2, encodeClientFinishedV2, encodeClientInitV2, encodeControlPrefaceV2, encodeServerFinishedCoreV2, encodeServerFinishedV2, generateEphemeralKeyV2, parseControlPrefaceV2, validateClientInitV2, validateServerFinishedV2, } from "./handshake.js";
|
|
5
|
-
import { DirectionV2, InnerTypeV2, computeFSS2HashV2, computeOpenHashV2, computeSetupMAC, decodeInnerRecordV2, decodeOpenACKV2, decodeOpenPayload, decodeOpenRejectV2, decodeRecordHeader, decodeSetupPrefaceV2, decodeStreamKeyUpdateACKV2, deriveControlMaterial, deriveEpochZero, deriveEpochRoots, deriveNextEpoch, deriveStreamMaterial, encodeInnerRecordV2, encodeOpenACKV2, encodeOpenPayload, encodeOpenRejectV2, encodeRecordHeader, encodeSetupPreface, encodeStreamKeyUpdateACKV2, openRecord, sealRecord, verifySetupMAC, } from "./protocol.js";
|
|
6
|
-
import { StreamLifetimeLedgerV2, StreamLifetimeLedgerV2Error, maxLogicalStreamIDV2, } from "./streamLifetimeLedger.js";
|
|
7
|
-
import { UNRELIABLE_MESSAGES_FEATURE_V2, UNRELIABLE_MESSAGE_WIRE_BYTES_V2, createInternalUnreliableMessageChannelV2, } from "./unreliableMessage.js";
|
|
8
|
-
const encoder = new TextEncoder();
|
|
9
|
-
const decoder = new TextDecoder();
|
|
10
|
-
const MAX_DATA_BYTES = 16_384;
|
|
11
|
-
const MAX_BUFFERED_STREAM_BYTES = 4 * 1024 * 1024;
|
|
12
|
-
const RESERVED_RPC_KIND = "flowersec.rpc.v2";
|
|
13
|
-
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
|
|
14
|
-
const DEFAULT_CLOSE_TIMEOUT_MS = 2_000;
|
|
15
|
-
const DEFAULT_CONTROL_CONFIRM_TIMEOUT_MS = 2_000;
|
|
16
|
-
export class SessionV2Error extends Error {
|
|
17
|
-
code;
|
|
18
|
-
constructor(code, message) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.code = code;
|
|
21
|
-
this.name = "SessionV2Error";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
class SessionRpcPeerV2 {
|
|
25
|
-
outbound;
|
|
26
|
-
inbound;
|
|
27
|
-
constructor(outbound, inbound) {
|
|
28
|
-
this.outbound = outbound;
|
|
29
|
-
this.inbound = inbound;
|
|
30
|
-
}
|
|
31
|
-
call(typeId, payload, signal) {
|
|
32
|
-
return this.outbound.call(typeId, payload, signal);
|
|
33
|
-
}
|
|
34
|
-
notify(typeId, payload) {
|
|
35
|
-
return this.outbound.notify(typeId, payload);
|
|
36
|
-
}
|
|
37
|
-
onNotify(typeId, handler) {
|
|
38
|
-
return this.inbound.onNotify(typeId, handler);
|
|
39
|
-
}
|
|
40
|
-
close() {
|
|
41
|
-
this.outbound.close();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
export async function establishSessionV2(carrier, config, options = {}) {
|
|
45
|
-
validateConfig(carrier, config);
|
|
46
|
-
const establishDeadline = createSessionDeadline(config, "establish");
|
|
47
|
-
const establishSignal = combineSignals(options.signal, establishDeadline.signal);
|
|
48
|
-
throwIfAborted(establishSignal.signal);
|
|
49
|
-
let control;
|
|
50
|
-
try {
|
|
51
|
-
control = config.role === "client"
|
|
52
|
-
? await carrier.openStream(signalOptions(establishSignal.signal))
|
|
53
|
-
: await carrier.acceptStream(signalOptions(establishSignal.signal));
|
|
54
|
-
const reader = new ExactReader(control);
|
|
55
|
-
const localFeatures = supportedFeatures(carrier.unreliableDatagrams);
|
|
56
|
-
const material = config.role === "client"
|
|
57
|
-
? await clientHandshake(control, reader, config, localFeatures, establishSignal.signal)
|
|
58
|
-
: await serverHandshake(control, reader, config, localFeatures, establishSignal.signal);
|
|
59
|
-
const session = new SessionV2(carrier, control, reader, config, material);
|
|
60
|
-
session.start();
|
|
61
|
-
return session;
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
control?.abort(asError(error));
|
|
65
|
-
carrier.abort({ code: 6, reason: "handshake failed" });
|
|
66
|
-
if (error instanceof SessionV2Error)
|
|
67
|
-
throw error;
|
|
68
|
-
throw new SessionV2Error("handshake", `Flowersec v2 handshake failed: ${errorMessage(error)}`);
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
establishSignal.cancel();
|
|
72
|
-
establishDeadline.cancel();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
export class SessionV2 {
|
|
76
|
-
carrier;
|
|
77
|
-
control;
|
|
78
|
-
controlReader;
|
|
79
|
-
config;
|
|
80
|
-
path;
|
|
81
|
-
endpointInstanceId;
|
|
82
|
-
rpc;
|
|
83
|
-
termination;
|
|
84
|
-
unreliableMessages;
|
|
85
|
-
terminalError;
|
|
86
|
-
role;
|
|
87
|
-
sendDirection;
|
|
88
|
-
receiveDirection;
|
|
89
|
-
h3;
|
|
90
|
-
sendRoots = new Map();
|
|
91
|
-
receiveRoots = new Map();
|
|
92
|
-
sendEpoch = 0;
|
|
93
|
-
receiveEpoch = 0;
|
|
94
|
-
controlSendEpoch = 0;
|
|
95
|
-
controlSendSequence = 0n;
|
|
96
|
-
controlReceiveEpoch = 0;
|
|
97
|
-
controlReceiveSequence = 0n;
|
|
98
|
-
controlWriteTail = Promise.resolve();
|
|
99
|
-
controlTerminalSealed = false;
|
|
100
|
-
nextLogicalID;
|
|
101
|
-
receivedGoAway = false;
|
|
102
|
-
receivedGoAwayLastAccepted = 0n;
|
|
103
|
-
receivedGoAwayReason = 0;
|
|
104
|
-
sentGoAway = false;
|
|
105
|
-
sentGoAwayLastAccepted = 0n;
|
|
106
|
-
sentGoAwayReason = 0;
|
|
107
|
-
closePromise;
|
|
108
|
-
lifecycle = "opening";
|
|
109
|
-
sentSessionClose = false;
|
|
110
|
-
sessionCloseCommitted = false;
|
|
111
|
-
peerSessionClose = deferred();
|
|
112
|
-
streams = new Map();
|
|
113
|
-
releasedStreams = new Map();
|
|
114
|
-
releasedStreamCleanup = releasedStreamCleanup(this.releasedStreams);
|
|
115
|
-
peerLedger;
|
|
116
|
-
outboundLedger;
|
|
117
|
-
incoming = new AsyncQueue();
|
|
118
|
-
outboundPermits;
|
|
119
|
-
inboundPermits;
|
|
120
|
-
pings = new Map();
|
|
121
|
-
backgroundTasks = new Set();
|
|
122
|
-
nextPing = 1n;
|
|
123
|
-
rpcActivation = deferred();
|
|
124
|
-
rpcStreamPromise;
|
|
125
|
-
rpcServing = false;
|
|
126
|
-
rekeyTail = Promise.resolve();
|
|
127
|
-
nextTransition = 1n;
|
|
128
|
-
receiveTransition = 0n;
|
|
129
|
-
pendingSessionRekey;
|
|
130
|
-
lastSessionRekeyACK;
|
|
131
|
-
preparingSendEpoch;
|
|
132
|
-
pendingReceiveEpoch;
|
|
133
|
-
openFrozen = false;
|
|
134
|
-
openGate = deferred();
|
|
135
|
-
outboundFrontierChanged = deferred();
|
|
136
|
-
activeInboundResponders = 0;
|
|
137
|
-
localResponderFrozen = false;
|
|
138
|
-
peerResponderFrozen = false;
|
|
139
|
-
responderChanged = deferred();
|
|
140
|
-
idleWatchdogStarted = false;
|
|
141
|
-
idleTimerCancel;
|
|
142
|
-
terminationState = deferred();
|
|
143
|
-
terminationController = new AbortController();
|
|
144
|
-
rpcRouter;
|
|
145
|
-
constructor(carrier, control, controlReader, config, material) {
|
|
146
|
-
this.carrier = carrier;
|
|
147
|
-
this.control = control;
|
|
148
|
-
this.controlReader = controlReader;
|
|
149
|
-
this.config = config;
|
|
150
|
-
this.path = config.path;
|
|
151
|
-
this.termination = this.terminationState.promise;
|
|
152
|
-
this.endpointInstanceId = config.path === "tunnel" ? config.expectedPeerEndpointInstanceID : undefined;
|
|
153
|
-
this.role = config.role === "client" ? 1 : 2;
|
|
154
|
-
this.sendDirection = this.role === 1 ? DirectionV2.ClientToServer : DirectionV2.ServerToClient;
|
|
155
|
-
this.receiveDirection = this.role === 1 ? DirectionV2.ServerToClient : DirectionV2.ClientToServer;
|
|
156
|
-
this.nextLogicalID = this.role === 1 ? 1n : 2n;
|
|
157
|
-
this.peerLedger = new StreamLifetimeLedgerV2(this.role === 1 ? 2 : 1);
|
|
158
|
-
this.outboundLedger = new StreamLifetimeLedgerV2(this.role);
|
|
159
|
-
this.h3 = material.h3.slice();
|
|
160
|
-
this.sendRoots.set(0, deriveEpochZero(material.sessionPRK, this.sendDirection));
|
|
161
|
-
this.receiveRoots.set(0, deriveEpochZero(material.sessionPRK, this.receiveDirection));
|
|
162
|
-
this.unreliableMessages = (material.selectedFeatures & UNRELIABLE_MESSAGES_FEATURE_V2) !== 0
|
|
163
|
-
? createInternalUnreliableMessageChannelV2({
|
|
164
|
-
transport: requireUnreliableDatagrams(carrier),
|
|
165
|
-
suite: config.suite,
|
|
166
|
-
h3: this.h3,
|
|
167
|
-
sendDirection: this.sendDirection,
|
|
168
|
-
receiveDirection: this.receiveDirection,
|
|
169
|
-
currentSendEpoch: () => ({
|
|
170
|
-
epoch: this.sendEpoch,
|
|
171
|
-
epochSecret: this.rootForSend(this.sendEpoch).epochSecret,
|
|
172
|
-
}),
|
|
173
|
-
receiveEpochSecret: (epoch) => epoch <= this.receiveEpoch && epoch + 1 >= this.receiveEpoch
|
|
174
|
-
? this.receiveRoots.get(epoch)?.epochSecret
|
|
175
|
-
: undefined,
|
|
176
|
-
})
|
|
177
|
-
: undefined;
|
|
178
|
-
this.outboundPermits = new AsyncSemaphore(config.maxInboundStreams);
|
|
179
|
-
this.inboundPermits = new AsyncSemaphore(config.maxInboundStreams);
|
|
180
|
-
this.rpcRouter = config.rpcRouter ?? new RpcRouter();
|
|
181
|
-
const rpcReadState = { reader: undefined };
|
|
182
|
-
const rpcClient = new RpcClient(async (length) => {
|
|
183
|
-
await this.rpcActivation.promise;
|
|
184
|
-
const stream = await this.ensureRPCStream();
|
|
185
|
-
rpcReadState.reader ??= new ExactReader(stream);
|
|
186
|
-
return await rpcReadState.reader.readExactly(length);
|
|
187
|
-
}, async (payload) => {
|
|
188
|
-
this.rpcActivation.resolve();
|
|
189
|
-
const stream = await this.ensureRPCStream();
|
|
190
|
-
await stream.write(payload);
|
|
191
|
-
}, { onTerminal: (error) => this.fail(error) });
|
|
192
|
-
this.rpc = new SessionRpcPeerV2(rpcClient, this.rpcRouter);
|
|
193
|
-
}
|
|
194
|
-
async openStream(kind, options = {}) {
|
|
195
|
-
if (kind === RESERVED_RPC_KIND)
|
|
196
|
-
throw new SessionV2Error("open_rejected", "reserved RPC stream kind");
|
|
197
|
-
return await this.openLogicalStream(kind, options, false);
|
|
198
|
-
}
|
|
199
|
-
async acceptStream(options = {}) {
|
|
200
|
-
this.assertOpen();
|
|
201
|
-
return await this.incoming.shift(options.signal);
|
|
202
|
-
}
|
|
203
|
-
async probeLiveness(options = {}) {
|
|
204
|
-
this.assertOpen();
|
|
205
|
-
throwIfAborted(options.signal);
|
|
206
|
-
const nonce = this.nextPing++;
|
|
207
|
-
const pending = deferred();
|
|
208
|
-
this.pings.set(nonce, pending);
|
|
209
|
-
const started = this.config.runtime.monotonicMilliseconds();
|
|
210
|
-
try {
|
|
211
|
-
await this.sendControl(InnerTypeV2.Ping, u64(nonce), options.signal);
|
|
212
|
-
await raceAbort(pending.promise, options.signal);
|
|
213
|
-
return Math.max(0, this.config.runtime.monotonicMilliseconds() - started);
|
|
214
|
-
}
|
|
215
|
-
finally {
|
|
216
|
-
this.pings.delete(nonce);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
async rekey(options = {}) {
|
|
220
|
-
this.assertOpen();
|
|
221
|
-
throwIfAborted(options.signal);
|
|
222
|
-
const task = this.rekeyTail.then(async () => {
|
|
223
|
-
throwIfAborted(options.signal);
|
|
224
|
-
await this.rekeyOnce(options);
|
|
225
|
-
});
|
|
226
|
-
this.rekeyTail = task.catch(() => undefined);
|
|
227
|
-
await raceAbort(task, options.signal);
|
|
228
|
-
}
|
|
229
|
-
close() {
|
|
230
|
-
if (this.closePromise === undefined) {
|
|
231
|
-
this.beginClosing();
|
|
232
|
-
this.closePromise = this.closeOnce();
|
|
233
|
-
}
|
|
234
|
-
return this.closePromise;
|
|
235
|
-
}
|
|
236
|
-
start() {
|
|
237
|
-
if (this.lifecycle !== "opening")
|
|
238
|
-
return;
|
|
239
|
-
this.lifecycle = "open";
|
|
240
|
-
this.startIdleWatchdog();
|
|
241
|
-
void this.controlLoop();
|
|
242
|
-
void this.acceptCarrierLoop();
|
|
243
|
-
void this.carrier.waitTermination().then(() => this.fail(new CarrierError("closed", "carrier session terminated"), false), (error) => this.fail(asError(error), false));
|
|
244
|
-
}
|
|
245
|
-
rootForSend(epoch) {
|
|
246
|
-
const roots = this.sendRoots.get(epoch);
|
|
247
|
-
if (roots === undefined)
|
|
248
|
-
throw protocolError("missing send epoch roots");
|
|
249
|
-
return roots;
|
|
250
|
-
}
|
|
251
|
-
rootForReceive(epoch) {
|
|
252
|
-
const roots = this.receiveRoots.get(epoch);
|
|
253
|
-
if (roots === undefined)
|
|
254
|
-
throw protocolError("missing receive epoch roots");
|
|
255
|
-
return roots;
|
|
256
|
-
}
|
|
257
|
-
hasReceiveRoots(epoch) {
|
|
258
|
-
return this.receiveRoots.has(epoch);
|
|
259
|
-
}
|
|
260
|
-
installReceiveRoots(epoch, roots) {
|
|
261
|
-
const existing = this.receiveRoots.get(epoch);
|
|
262
|
-
if (existing !== undefined && !bytesEqual(existing.epochSecret, roots.epochSecret)) {
|
|
263
|
-
throw protocolError("conflicting receive epoch roots");
|
|
264
|
-
}
|
|
265
|
-
this.receiveRoots.set(epoch, roots);
|
|
266
|
-
}
|
|
267
|
-
transcriptHash() {
|
|
268
|
-
return this.h3;
|
|
269
|
-
}
|
|
270
|
-
receiveDirectionValue() {
|
|
271
|
-
return this.receiveDirection;
|
|
272
|
-
}
|
|
273
|
-
terminationSignal() {
|
|
274
|
-
return this.terminationController.signal;
|
|
275
|
-
}
|
|
276
|
-
async sendStreamRecord(stream, type, payload, signal) {
|
|
277
|
-
this.assertOpen();
|
|
278
|
-
const inner = encodeInnerRecordV2(type, payload);
|
|
279
|
-
const roots = this.rootForSend(stream.sendEpoch);
|
|
280
|
-
const material = deriveStreamMaterial(roots.streamRoot, this.h3, stream.id, this.sendDirection, stream.sendEpoch);
|
|
281
|
-
const header = {
|
|
282
|
-
epoch: stream.sendEpoch,
|
|
283
|
-
sequence: stream.sendSequence,
|
|
284
|
-
ciphertextLength: inner.length + 16,
|
|
285
|
-
};
|
|
286
|
-
const ciphertext = sealRecord(this.config.suite, material, this.h3, stream.id, this.sendDirection, header, inner);
|
|
287
|
-
stream.sendSequence += 1n;
|
|
288
|
-
await writeAll(stream.carrier, concat(encodeRecordHeader(header), ciphertext), signal);
|
|
289
|
-
this.markAuthenticatedActivity();
|
|
290
|
-
}
|
|
291
|
-
async readStreamRecord(stream) {
|
|
292
|
-
const header = decodeRecordHeader(await stream.reader.readExactly(24));
|
|
293
|
-
const priorACK = stream.priorACK !== undefined &&
|
|
294
|
-
header.epoch === stream.priorACK.epoch && header.sequence === stream.priorACK.sequence;
|
|
295
|
-
if (!priorACK && (header.epoch !== stream.receiveEpoch || header.sequence !== stream.receiveSequence)) {
|
|
296
|
-
throw protocolError("unexpected stream epoch or sequence");
|
|
297
|
-
}
|
|
298
|
-
const roots = this.rootForReceive(header.epoch);
|
|
299
|
-
const material = deriveStreamMaterial(roots.streamRoot, this.h3, stream.id, this.receiveDirection, header.epoch);
|
|
300
|
-
const ciphertext = await stream.reader.readExactly(header.ciphertextLength);
|
|
301
|
-
const inner = decodeInnerRecordV2(openRecord(this.config.suite, material, this.h3, stream.id, this.receiveDirection, header, ciphertext));
|
|
302
|
-
if (priorACK) {
|
|
303
|
-
if (inner.type !== InnerTypeV2.StreamKeyUpdateACK)
|
|
304
|
-
throw protocolError("late old-epoch record is not rekey ACK");
|
|
305
|
-
stream.priorACK = undefined;
|
|
306
|
-
this.cleanupEpochRoots();
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
stream.receiveSequence += 1n;
|
|
310
|
-
}
|
|
311
|
-
this.markAuthenticatedActivity();
|
|
312
|
-
return { ...inner, header };
|
|
313
|
-
}
|
|
314
|
-
async localReset(stream, error) {
|
|
315
|
-
if (!stream.markTerminal(error))
|
|
316
|
-
return;
|
|
317
|
-
this.releaseStream(stream);
|
|
318
|
-
let controlConfirmed = false;
|
|
319
|
-
try {
|
|
320
|
-
if (await this.sendControlCleanup(InnerTypeV2.StreamReset, idReason(stream.id, 6))) {
|
|
321
|
-
this.commitLocalReset(stream.id);
|
|
322
|
-
if (stream.hasRemoteFIN())
|
|
323
|
-
controlConfirmed = await this.confirmControlDelivery();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
catch (cause) {
|
|
327
|
-
this.fail(asError(cause));
|
|
328
|
-
}
|
|
329
|
-
if (controlConfirmed) {
|
|
330
|
-
await stream.carrier.closeWrite().catch(async () => await stream.carrier.reset().catch(() => undefined));
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
await stream.carrier.reset().catch(() => undefined);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
releaseStream(stream) {
|
|
337
|
-
if (stream.terminalError !== undefined) {
|
|
338
|
-
this.releasedStreams.delete(stream.id);
|
|
339
|
-
this.releasedStreamCleanup.unregister(stream);
|
|
340
|
-
}
|
|
341
|
-
if (this.streams.get(stream.id) !== stream)
|
|
342
|
-
return;
|
|
343
|
-
this.streams.delete(stream.id);
|
|
344
|
-
if (stream.terminalError === undefined) {
|
|
345
|
-
this.releasedStreams.set(stream.id, new WeakRef(stream));
|
|
346
|
-
this.releasedStreamCleanup.register(stream, stream.id, stream);
|
|
347
|
-
}
|
|
348
|
-
stream.releasePermit();
|
|
349
|
-
this.cleanupEpochRoots();
|
|
350
|
-
}
|
|
351
|
-
async confirmControlDelivery() {
|
|
352
|
-
if (this.lifecycle !== "open" || this.controlTerminalSealed)
|
|
353
|
-
return false;
|
|
354
|
-
const nonce = this.nextPing++;
|
|
355
|
-
const pending = deferred();
|
|
356
|
-
this.pings.set(nonce, pending);
|
|
357
|
-
try {
|
|
358
|
-
await this.sendControl(InnerTypeV2.Ping, u64(nonce));
|
|
359
|
-
return await resolveWithin(pending.promise, DEFAULT_CONTROL_CONFIRM_TIMEOUT_MS);
|
|
360
|
-
}
|
|
361
|
-
finally {
|
|
362
|
-
this.pings.delete(nonce);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
async openLogicalStream(kind, options, internal) {
|
|
366
|
-
this.assertOpen();
|
|
367
|
-
throwIfAborted(options.signal);
|
|
368
|
-
await this.waitOpenGate(options.signal);
|
|
369
|
-
const releasePermit = internal ? () => undefined : await this.outboundPermits.acquire(options.signal);
|
|
370
|
-
let stream;
|
|
371
|
-
let carrierStream;
|
|
372
|
-
let id;
|
|
373
|
-
let ledgerAllocated = false;
|
|
374
|
-
try {
|
|
375
|
-
this.assertOpen();
|
|
376
|
-
await this.waitOpenGate(options.signal);
|
|
377
|
-
if (this.sentGoAway || this.receivedGoAway)
|
|
378
|
-
throw new SessionV2Error("going_away", "session is going away");
|
|
379
|
-
id = this.nextLogicalID;
|
|
380
|
-
if (id > maxLogicalStreamIDV2(this.role)) {
|
|
381
|
-
await this.sendGoAway(5);
|
|
382
|
-
throw new SessionV2Error("resource_exhausted", "logical stream lifetime ledger exhausted");
|
|
383
|
-
}
|
|
384
|
-
this.nextLogicalID += 2n;
|
|
385
|
-
const setupAction = this.outboundLedger.validFSS2(id);
|
|
386
|
-
ledgerAllocated = true;
|
|
387
|
-
if (setupAction === "reset") {
|
|
388
|
-
this.notifyOutboundFrontierChanged();
|
|
389
|
-
throw new SessionV2Error("closed", "logical stream identity was already reset by peer");
|
|
390
|
-
}
|
|
391
|
-
carrierStream = await this.carrier.openStream(options);
|
|
392
|
-
if (!this.localOpeningAllowedAfterGoAway(id)) {
|
|
393
|
-
throw new SessionV2Error("going_away", "logical stream is beyond the peer GOAWAY boundary");
|
|
394
|
-
}
|
|
395
|
-
const roots = this.rootForSend(this.sendEpoch);
|
|
396
|
-
const unsigned = {
|
|
397
|
-
openerRole: this.role,
|
|
398
|
-
logicalStreamID: id,
|
|
399
|
-
initialSendEpoch: this.sendEpoch,
|
|
400
|
-
setupMAC: new Uint8Array(32),
|
|
401
|
-
};
|
|
402
|
-
const prefaceRaw = encodeSetupPreface({
|
|
403
|
-
...unsigned,
|
|
404
|
-
setupMAC: computeSetupMAC(roots.setupRoot, this.h3, unsigned),
|
|
405
|
-
});
|
|
406
|
-
await writeAll(carrierStream, prefaceRaw, options.signal);
|
|
407
|
-
if (!this.localOpeningAllowedAfterGoAway(id)) {
|
|
408
|
-
throw new SessionV2Error("going_away", "logical stream is beyond the peer GOAWAY boundary");
|
|
409
|
-
}
|
|
410
|
-
const metadata = encodeMetadata(options.metadata ?? {});
|
|
411
|
-
const openRaw = encodeOpenPayload({
|
|
412
|
-
logicalStreamID: id,
|
|
413
|
-
fss2Hash: computeFSS2HashV2(prefaceRaw),
|
|
414
|
-
kind,
|
|
415
|
-
metadata,
|
|
416
|
-
});
|
|
417
|
-
stream = new EncryptedStreamV2(this, carrierStream, id, kind, this.sendEpoch, this.receiveEpoch, releasePermit);
|
|
418
|
-
this.streams.set(id, stream);
|
|
419
|
-
stream.setOpenHash(computeOpenHashV2(openRaw));
|
|
420
|
-
await stream.send(InnerTypeV2.Open, openRaw, options.signal);
|
|
421
|
-
stream.startPump();
|
|
422
|
-
await raceAbort(stream.opened.promise, options.signal);
|
|
423
|
-
if (!this.localOpeningAllowedAfterGoAway(id)) {
|
|
424
|
-
throw new SessionV2Error("going_away", "logical stream is beyond the peer GOAWAY boundary");
|
|
425
|
-
}
|
|
426
|
-
return stream;
|
|
427
|
-
}
|
|
428
|
-
catch (error) {
|
|
429
|
-
if (stream !== undefined)
|
|
430
|
-
await this.localReset(stream, asError(error));
|
|
431
|
-
else {
|
|
432
|
-
await carrierStream?.reset().catch(() => undefined);
|
|
433
|
-
if (id !== undefined && ledgerAllocated) {
|
|
434
|
-
await this.commitOutboundReset(id).catch((cause) => this.fail(asError(cause)));
|
|
435
|
-
}
|
|
436
|
-
releasePermit();
|
|
437
|
-
}
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
ensureRPCStream() {
|
|
442
|
-
this.rpcActivation.resolve();
|
|
443
|
-
this.rpcStreamPromise ??= this.openLogicalStream(RESERVED_RPC_KIND, {}, true);
|
|
444
|
-
return this.rpcStreamPromise;
|
|
445
|
-
}
|
|
446
|
-
async acceptCarrierLoop() {
|
|
447
|
-
try {
|
|
448
|
-
while (this.lifecycle === "open") {
|
|
449
|
-
const carrierStream = await this.carrier.acceptStream();
|
|
450
|
-
void this.acceptCarrierStream(carrierStream).catch((error) => {
|
|
451
|
-
void carrierStream.reset();
|
|
452
|
-
if (error instanceof StreamLifetimeLedgerV2Error && error.code === "duplicate") {
|
|
453
|
-
this.fail(protocolError(error.message));
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
catch (error) {
|
|
459
|
-
if (this.lifecycle !== "closed")
|
|
460
|
-
this.fail(asError(error));
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
async acceptCarrierStream(carrierStream) {
|
|
464
|
-
let responderHeld = false;
|
|
465
|
-
let ledgerID;
|
|
466
|
-
try {
|
|
467
|
-
this.assertOpen();
|
|
468
|
-
await this.enterInboundResponder();
|
|
469
|
-
responderHeld = true;
|
|
470
|
-
const reader = new ExactReader(carrierStream);
|
|
471
|
-
let prefaceRaw;
|
|
472
|
-
let preface;
|
|
473
|
-
prefaceRaw = await reader.readExactly(56);
|
|
474
|
-
preface = decodeSetupPrefaceV2(prefaceRaw);
|
|
475
|
-
const peerRole = this.role === 1 ? 2 : 1;
|
|
476
|
-
if (preface.openerRole !== peerRole || preface.initialSendEpoch !== this.receiveEpoch) {
|
|
477
|
-
await carrierStream.reset().catch(() => undefined);
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const receiveRoots = this.rootForReceive(preface.initialSendEpoch);
|
|
481
|
-
if (!verifySetupMAC(receiveRoots.setupRoot, this.h3, preface) || !this.acceptsPeerStreamAfterGoAway(preface.logicalStreamID)) {
|
|
482
|
-
await carrierStream.reset().catch(() => undefined);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
let setupAction;
|
|
486
|
-
try {
|
|
487
|
-
setupAction = this.peerLedger.validFSS2(preface.logicalStreamID);
|
|
488
|
-
}
|
|
489
|
-
catch (error) {
|
|
490
|
-
if (error instanceof StreamLifetimeLedgerV2Error && error.code === "duplicate")
|
|
491
|
-
throw error;
|
|
492
|
-
await carrierStream.reset().catch(() => undefined);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (setupAction === "reset") {
|
|
496
|
-
await carrierStream.reset().catch(() => undefined);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
ledgerID = preface.logicalStreamID;
|
|
500
|
-
await this.acceptCarrierStreamAfterFSS2(carrierStream, reader, prefaceRaw, preface);
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
if (ledgerID !== undefined)
|
|
504
|
-
await this.resetInboundBeforeDelivery(ledgerID, carrierStream);
|
|
505
|
-
else
|
|
506
|
-
await carrierStream.reset().catch(() => undefined);
|
|
507
|
-
if (error instanceof StreamLifetimeLedgerV2Error && error.code === "duplicate")
|
|
508
|
-
throw error;
|
|
509
|
-
}
|
|
510
|
-
finally {
|
|
511
|
-
if (responderHeld)
|
|
512
|
-
this.leaveInboundResponder();
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
async acceptCarrierStreamAfterFSS2(carrierStream, reader, prefaceRaw, preface) {
|
|
516
|
-
this.assertOpen();
|
|
517
|
-
const temporary = new EncryptedStreamV2(this, carrierStream, preface.logicalStreamID, "", this.sendEpoch, preface.initialSendEpoch, () => undefined, reader);
|
|
518
|
-
const first = await this.readStreamRecord(temporary);
|
|
519
|
-
if (first.type !== InnerTypeV2.Open)
|
|
520
|
-
throw protocolError("OPEN must be first");
|
|
521
|
-
const open = decodeOpenPayload(first.payload);
|
|
522
|
-
if (open.logicalStreamID !== preface.logicalStreamID ||
|
|
523
|
-
!bytesEqual(open.fss2Hash, computeFSS2HashV2(prefaceRaw))) {
|
|
524
|
-
throw protocolError("OPEN binding mismatch");
|
|
525
|
-
}
|
|
526
|
-
this.peerLedger.validOpen(preface.logicalStreamID);
|
|
527
|
-
const internalRPC = open.kind === RESERVED_RPC_KIND;
|
|
528
|
-
if (internalRPC && decoder.decode(open.metadata) !== "{}") {
|
|
529
|
-
await temporary.send(InnerTypeV2.OpenReject, encodeOpenRejectV2(computeOpenHashV2(first.payload), 4));
|
|
530
|
-
await carrierStream.closeWrite();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
const releasePermit = internalRPC ? () => undefined : this.inboundPermits.tryAcquire();
|
|
534
|
-
if (releasePermit === undefined) {
|
|
535
|
-
await temporary.send(InnerTypeV2.OpenReject, encodeOpenRejectV2(computeOpenHashV2(first.payload), 2));
|
|
536
|
-
await carrierStream.closeWrite();
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const stream = new EncryptedStreamV2(this, carrierStream, preface.logicalStreamID, open.kind, this.sendEpoch, preface.initialSendEpoch, releasePermit, reader);
|
|
540
|
-
this.assertOpen();
|
|
541
|
-
stream.receiveSequence = temporary.receiveSequence;
|
|
542
|
-
this.streams.set(stream.id, stream);
|
|
543
|
-
await stream.send(InnerTypeV2.OpenACK, encodeOpenACKV2(computeOpenHashV2(first.payload)));
|
|
544
|
-
stream.markOpen();
|
|
545
|
-
stream.startPump();
|
|
546
|
-
if (!this.acceptsPeerStreamAfterGoAway(stream.id)) {
|
|
547
|
-
await this.localReset(stream, new SessionV2Error("going_away", "peer stream exceeds the sent GOAWAY boundary"));
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
if (internalRPC) {
|
|
551
|
-
if (this.rpcServing) {
|
|
552
|
-
await this.localReset(stream, protocolError("duplicate reserved RPC stream"));
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
this.rpcServing = true;
|
|
556
|
-
const rpcReader = new ExactReader(stream);
|
|
557
|
-
const server = new RpcServer({
|
|
558
|
-
readExactly: async (length) => await rpcReader.readExactly(length),
|
|
559
|
-
write: async (payload) => { await stream.write(payload); },
|
|
560
|
-
close: () => { void stream.reset(); },
|
|
561
|
-
}, this.config.rpcServerOptions, this.rpcRouter);
|
|
562
|
-
void server.serve().catch((error) => {
|
|
563
|
-
if (this.lifecycle !== "closed")
|
|
564
|
-
this.fail(asError(error));
|
|
565
|
-
});
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
this.incoming.push({
|
|
569
|
-
id: stream.id,
|
|
570
|
-
kind: stream.kind,
|
|
571
|
-
metadata: decodeMetadata(open.metadata),
|
|
572
|
-
stream,
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
async resetInboundBeforeDelivery(id, carrierStream) {
|
|
576
|
-
await carrierStream.reset().catch(() => undefined);
|
|
577
|
-
try {
|
|
578
|
-
if (await this.sendControlCleanup(InnerTypeV2.StreamReset, idReason(id, 3))) {
|
|
579
|
-
this.peerLedger.localResetCommitted(id);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
catch (error) {
|
|
583
|
-
this.fail(asError(error));
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
async sendControl(type, payload, signal) {
|
|
587
|
-
if (this.controlTerminalSealed) {
|
|
588
|
-
throw this.terminalError ?? new SessionV2Error("closed", "Flowersec v2 control stream is sealed");
|
|
589
|
-
}
|
|
590
|
-
const task = this.controlWriteTail.then(async () => {
|
|
591
|
-
if (this.lifecycle === "closed")
|
|
592
|
-
throw this.terminalError ?? new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
593
|
-
await this.writeControl(type, payload, signal);
|
|
594
|
-
});
|
|
595
|
-
this.controlWriteTail = task.catch(() => undefined);
|
|
596
|
-
await task;
|
|
597
|
-
}
|
|
598
|
-
async sendControlResponse(type, payload) {
|
|
599
|
-
if (this.lifecycle !== "open" || this.controlTerminalSealed)
|
|
600
|
-
return false;
|
|
601
|
-
const task = this.controlWriteTail.then(async () => {
|
|
602
|
-
if (this.lifecycle !== "open" || this.controlTerminalSealed)
|
|
603
|
-
return false;
|
|
604
|
-
await this.writeControl(type, payload);
|
|
605
|
-
return true;
|
|
606
|
-
});
|
|
607
|
-
this.controlWriteTail = task.then(() => undefined, () => undefined);
|
|
608
|
-
return await task;
|
|
609
|
-
}
|
|
610
|
-
async sendControlCleanup(type, payload) {
|
|
611
|
-
if (this.controlTerminalSealed || this.lifecycle === "closed")
|
|
612
|
-
return false;
|
|
613
|
-
const task = this.controlWriteTail.then(async () => {
|
|
614
|
-
if (this.lifecycle === "closed")
|
|
615
|
-
return false;
|
|
616
|
-
await this.writeControl(type, payload);
|
|
617
|
-
return true;
|
|
618
|
-
});
|
|
619
|
-
this.controlWriteTail = task.then(() => undefined, () => undefined);
|
|
620
|
-
return await task;
|
|
621
|
-
}
|
|
622
|
-
sendTerminalControl(reason) {
|
|
623
|
-
if (this.controlTerminalSealed)
|
|
624
|
-
return this.controlWriteTail;
|
|
625
|
-
this.controlTerminalSealed = true;
|
|
626
|
-
const task = this.controlWriteTail.then(async () => {
|
|
627
|
-
if (this.lifecycle === "closed")
|
|
628
|
-
throw this.terminalError ?? new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
629
|
-
if (!this.sentGoAway) {
|
|
630
|
-
this.sentGoAway = true;
|
|
631
|
-
this.sentGoAwayLastAccepted = this.peerLedger.frontier;
|
|
632
|
-
this.sentGoAwayReason = reason;
|
|
633
|
-
}
|
|
634
|
-
await this.writeControl(InnerTypeV2.GoAway, idReason(this.sentGoAwayLastAccepted, this.sentGoAwayReason));
|
|
635
|
-
if (!this.sentSessionClose) {
|
|
636
|
-
this.sentSessionClose = true;
|
|
637
|
-
await this.writeControl(InnerTypeV2.SessionClose, Uint8Array.of(0, reason));
|
|
638
|
-
this.sessionCloseCommitted = true;
|
|
639
|
-
}
|
|
640
|
-
await this.control.closeWrite();
|
|
641
|
-
});
|
|
642
|
-
this.controlWriteTail = task.catch(() => undefined);
|
|
643
|
-
return task;
|
|
644
|
-
}
|
|
645
|
-
async writeControl(type, payload, signal) {
|
|
646
|
-
const inner = encodeInnerRecordV2(type, payload);
|
|
647
|
-
const roots = this.rootForSend(this.controlSendEpoch);
|
|
648
|
-
const material = deriveControlMaterial(roots.controlRoot, this.h3, this.sendDirection, this.controlSendEpoch);
|
|
649
|
-
const header = {
|
|
650
|
-
epoch: this.controlSendEpoch,
|
|
651
|
-
sequence: this.controlSendSequence,
|
|
652
|
-
ciphertextLength: inner.length + 16,
|
|
653
|
-
};
|
|
654
|
-
const ciphertext = sealRecord(this.config.suite, material, this.h3, 0n, this.sendDirection, header, inner);
|
|
655
|
-
this.controlSendSequence += 1n;
|
|
656
|
-
await writeAll(this.control, concat(encodeRecordHeader(header), ciphertext), signal);
|
|
657
|
-
this.markAuthenticatedActivity();
|
|
658
|
-
}
|
|
659
|
-
async readControl() {
|
|
660
|
-
const header = decodeRecordHeader(await this.controlReader.readExactly(24));
|
|
661
|
-
const cutover = header.epoch === this.controlReceiveEpoch + 1 &&
|
|
662
|
-
header.epoch <= this.receiveEpoch && header.sequence === 0n;
|
|
663
|
-
if (!cutover && (header.epoch !== this.controlReceiveEpoch || header.sequence !== this.controlReceiveSequence)) {
|
|
664
|
-
throw protocolError("unexpected control epoch or sequence");
|
|
665
|
-
}
|
|
666
|
-
const roots = this.rootForReceive(header.epoch);
|
|
667
|
-
const material = deriveControlMaterial(roots.controlRoot, this.h3, this.receiveDirection, header.epoch);
|
|
668
|
-
const ciphertext = await this.controlReader.readExactly(header.ciphertextLength);
|
|
669
|
-
const inner = decodeInnerRecordV2(openRecord(this.config.suite, material, this.h3, 0n, this.receiveDirection, header, ciphertext));
|
|
670
|
-
if (cutover) {
|
|
671
|
-
this.controlReceiveEpoch = header.epoch;
|
|
672
|
-
this.controlReceiveSequence = 1n;
|
|
673
|
-
this.cleanupEpochRoots();
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
this.controlReceiveSequence += 1n;
|
|
677
|
-
}
|
|
678
|
-
this.markAuthenticatedActivity();
|
|
679
|
-
return inner;
|
|
680
|
-
}
|
|
681
|
-
async controlLoop() {
|
|
682
|
-
try {
|
|
683
|
-
while (this.lifecycle !== "closed") {
|
|
684
|
-
const record = await this.readControl();
|
|
685
|
-
switch (record.type) {
|
|
686
|
-
case InnerTypeV2.Ping:
|
|
687
|
-
await this.sendControlResponse(InnerTypeV2.Pong, record.payload);
|
|
688
|
-
break;
|
|
689
|
-
case InnerTypeV2.Pong:
|
|
690
|
-
this.pings.get(readU64(record.payload))?.resolve();
|
|
691
|
-
break;
|
|
692
|
-
case InnerTypeV2.StreamReset: {
|
|
693
|
-
const { id, reason } = parseIDReason(record.payload);
|
|
694
|
-
if (id === 0n || reason === 0)
|
|
695
|
-
throw protocolError("invalid STREAM_RESET");
|
|
696
|
-
const stream = this.streams.get(id);
|
|
697
|
-
if (stream !== undefined) {
|
|
698
|
-
stream.peerReset(new SessionV2Error("stream_reset", "logical stream reset by peer"));
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
const released = this.releasedStreams.get(id);
|
|
702
|
-
if (released !== undefined) {
|
|
703
|
-
this.releasedStreams.delete(id);
|
|
704
|
-
const releasedStream = released.deref();
|
|
705
|
-
if (releasedStream !== undefined) {
|
|
706
|
-
this.releasedStreamCleanup.unregister(releasedStream);
|
|
707
|
-
releasedStream.markTerminal(new SessionV2Error("stream_reset", "logical stream reset by peer"));
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
if (this.isLocalLogicalID(id)) {
|
|
712
|
-
this.outboundLedger.peerReset(id);
|
|
713
|
-
this.notifyOutboundFrontierChanged();
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
this.peerLedger.peerReset(id);
|
|
717
|
-
}
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
case InnerTypeV2.SessionKeyUpdate:
|
|
721
|
-
await this.receiveSessionRekey(record.payload);
|
|
722
|
-
break;
|
|
723
|
-
case InnerTypeV2.SessionKeyUpdateACK:
|
|
724
|
-
this.receiveSessionRekeyACK(record.payload);
|
|
725
|
-
break;
|
|
726
|
-
case InnerTypeV2.GoAway:
|
|
727
|
-
await this.receiveGoAway(record.payload);
|
|
728
|
-
break;
|
|
729
|
-
case InnerTypeV2.SessionClose:
|
|
730
|
-
if (record.payload.length !== 2 || readU16(record.payload, 0) === 0) {
|
|
731
|
-
throw protocolError("invalid SESSION_CLOSE");
|
|
732
|
-
}
|
|
733
|
-
this.peerSessionClose.resolve();
|
|
734
|
-
await this.close();
|
|
735
|
-
throw new SessionV2Error("closed", "Flowersec v2 session closed by peer");
|
|
736
|
-
default:
|
|
737
|
-
throw protocolError(`unexpected control type ${record.type}`);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
catch (error) {
|
|
742
|
-
if (this.lifecycle !== "closed")
|
|
743
|
-
this.fail(asError(error));
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
async closeOnce() {
|
|
747
|
-
if (this.lifecycle === "closed")
|
|
748
|
-
return;
|
|
749
|
-
const closed = new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
750
|
-
const terminal = this.sendTerminalControl(1);
|
|
751
|
-
const work = (async () => {
|
|
752
|
-
try {
|
|
753
|
-
await terminal;
|
|
754
|
-
}
|
|
755
|
-
catch {
|
|
756
|
-
// Closing is best-effort once the bounded shutdown has started.
|
|
757
|
-
}
|
|
758
|
-
await this.peerSessionClose.promise;
|
|
759
|
-
this.fail(closed, false);
|
|
760
|
-
await this.carrier.close({ code: 1, reason: "session closed" }).catch(() => undefined);
|
|
761
|
-
})();
|
|
762
|
-
if (!await settleWithin(work, sessionCloseTimeoutMs(this.config))) {
|
|
763
|
-
this.carrier.abort({ code: 1, reason: "session close deadline exceeded" });
|
|
764
|
-
this.peerSessionClose.resolve();
|
|
765
|
-
void work.catch(() => undefined);
|
|
766
|
-
}
|
|
767
|
-
this.fail(closed, false);
|
|
768
|
-
}
|
|
769
|
-
async waitTermination() {
|
|
770
|
-
return await this.termination;
|
|
771
|
-
}
|
|
772
|
-
async rekeyOnce(options) {
|
|
773
|
-
this.assertOpen();
|
|
774
|
-
throwIfAborted(options.signal);
|
|
775
|
-
this.openFrozen = true;
|
|
776
|
-
this.openGate = deferred();
|
|
777
|
-
let committed = false;
|
|
778
|
-
const prepareDeadline = createSessionDeadline(this.config, "rekey_prepare");
|
|
779
|
-
const prepareSignal = combineSignals(options.signal, prepareDeadline.signal);
|
|
780
|
-
let completionDeadline;
|
|
781
|
-
let completionSignal;
|
|
782
|
-
let respondersFrozen = false;
|
|
783
|
-
try {
|
|
784
|
-
await this.freezeInboundResponders(false, prepareSignal.signal);
|
|
785
|
-
respondersFrozen = true;
|
|
786
|
-
const currentEpoch = this.sendEpoch;
|
|
787
|
-
if (currentEpoch === 0xffffffff)
|
|
788
|
-
throw new SessionV2Error("resource_exhausted", "session epoch exhausted");
|
|
789
|
-
const nextEpoch = currentEpoch + 1;
|
|
790
|
-
const currentRoots = this.rootForSend(currentEpoch);
|
|
791
|
-
const nextRoots = deriveEpochRoots(deriveNextEpoch(currentRoots.rekeyRoot, this.h3, this.sendDirection, nextEpoch));
|
|
792
|
-
this.sendRoots.set(nextEpoch, nextRoots);
|
|
793
|
-
this.preparingSendEpoch = nextEpoch;
|
|
794
|
-
const transition = this.nextTransition++;
|
|
795
|
-
const watermark = this.nextLogicalID > 2n ? this.nextLogicalID - 2n : 0n;
|
|
796
|
-
await this.waitOutboundFrontier(watermark, prepareSignal.signal);
|
|
797
|
-
prepareSignal.cancel();
|
|
798
|
-
prepareDeadline.cancel();
|
|
799
|
-
completionDeadline = createSessionDeadline(this.config, "rekey_completion");
|
|
800
|
-
completionSignal = combineSignals(options.signal, completionDeadline.signal);
|
|
801
|
-
committed = true;
|
|
802
|
-
throwIfAborted(completionSignal.signal);
|
|
803
|
-
const payload = concat(u64(transition), u32(nextEpoch), u64(watermark));
|
|
804
|
-
const active = [...this.streams.values()].filter((stream) => stream.canRekeySend());
|
|
805
|
-
const updates = active.map((stream) => stream.startSendRekey(transition, nextEpoch));
|
|
806
|
-
await Promise.all(updates.map(async (update) => await raceAbort(update.armed.promise, completionSignal.signal)));
|
|
807
|
-
const pending = {
|
|
808
|
-
payload,
|
|
809
|
-
epoch: nextEpoch,
|
|
810
|
-
acknowledged: deferred(),
|
|
811
|
-
committed: { value: false },
|
|
812
|
-
};
|
|
813
|
-
this.pendingSessionRekey = pending;
|
|
814
|
-
this.preparingSendEpoch = undefined;
|
|
815
|
-
await this.sendControl(InnerTypeV2.SessionKeyUpdate, payload);
|
|
816
|
-
await raceAbort(pending.acknowledged.promise, completionSignal.signal);
|
|
817
|
-
await Promise.all(updates.map(async (update) => await raceAbort(update.done.promise, completionSignal.signal)));
|
|
818
|
-
if (this.pendingSessionRekey === pending)
|
|
819
|
-
this.pendingSessionRekey = undefined;
|
|
820
|
-
this.cleanupEpochRoots();
|
|
821
|
-
}
|
|
822
|
-
catch (error) {
|
|
823
|
-
if (committed)
|
|
824
|
-
this.fail(asError(error));
|
|
825
|
-
throw error;
|
|
826
|
-
}
|
|
827
|
-
finally {
|
|
828
|
-
prepareSignal.cancel();
|
|
829
|
-
prepareDeadline.cancel();
|
|
830
|
-
completionSignal?.cancel();
|
|
831
|
-
completionDeadline?.cancel();
|
|
832
|
-
this.openFrozen = false;
|
|
833
|
-
this.openGate.resolve();
|
|
834
|
-
if (respondersFrozen)
|
|
835
|
-
this.unfreezeInboundResponders(false);
|
|
836
|
-
this.preparingSendEpoch = undefined;
|
|
837
|
-
this.cleanupEpochRoots();
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
async receiveSessionRekey(payload) {
|
|
841
|
-
const completionDeadline = createSessionDeadline(this.config, "rekey_completion");
|
|
842
|
-
let respondersFrozen = false;
|
|
843
|
-
try {
|
|
844
|
-
await this.freezeInboundResponders(true, completionDeadline.signal);
|
|
845
|
-
respondersFrozen = true;
|
|
846
|
-
await this.receiveSessionRekeyBeforeDeadline(payload, completionDeadline.signal);
|
|
847
|
-
}
|
|
848
|
-
finally {
|
|
849
|
-
if (respondersFrozen)
|
|
850
|
-
this.unfreezeInboundResponders(true);
|
|
851
|
-
completionDeadline.cancel();
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
async enterInboundResponder() {
|
|
855
|
-
while (this.localResponderFrozen || this.peerResponderFrozen) {
|
|
856
|
-
this.assertOpen();
|
|
857
|
-
await this.responderChanged.promise;
|
|
858
|
-
}
|
|
859
|
-
this.assertOpen();
|
|
860
|
-
this.activeInboundResponders++;
|
|
861
|
-
this.notifyResponderChanged();
|
|
862
|
-
}
|
|
863
|
-
leaveInboundResponder() {
|
|
864
|
-
if (this.activeInboundResponders === 0)
|
|
865
|
-
return;
|
|
866
|
-
this.activeInboundResponders--;
|
|
867
|
-
this.notifyResponderChanged();
|
|
868
|
-
}
|
|
869
|
-
async freezeInboundResponders(peer, signal) {
|
|
870
|
-
if (peer)
|
|
871
|
-
this.peerResponderFrozen = true;
|
|
872
|
-
else
|
|
873
|
-
this.localResponderFrozen = true;
|
|
874
|
-
this.notifyResponderChanged();
|
|
875
|
-
while (this.activeInboundResponders !== 0) {
|
|
876
|
-
await raceAbort(this.responderChanged.promise, signal);
|
|
877
|
-
this.assertOpen();
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
unfreezeInboundResponders(peer) {
|
|
881
|
-
if (peer)
|
|
882
|
-
this.peerResponderFrozen = false;
|
|
883
|
-
else
|
|
884
|
-
this.localResponderFrozen = false;
|
|
885
|
-
this.notifyResponderChanged();
|
|
886
|
-
}
|
|
887
|
-
notifyResponderChanged() {
|
|
888
|
-
this.responderChanged.resolve();
|
|
889
|
-
this.responderChanged = deferred();
|
|
890
|
-
}
|
|
891
|
-
async receiveSessionRekeyBeforeDeadline(payload, signal) {
|
|
892
|
-
throwIfAborted(signal);
|
|
893
|
-
const transition = readU64(payload);
|
|
894
|
-
const nextEpoch = readU32(payload, 8);
|
|
895
|
-
const watermark = readU64At(payload, 12);
|
|
896
|
-
if (payload.length !== 20 || transition !== this.receiveTransition + 1n || nextEpoch !== this.receiveEpoch + 1) {
|
|
897
|
-
throw protocolError("invalid SESSION_KEY_UPDATE");
|
|
898
|
-
}
|
|
899
|
-
if (watermark !== this.peerLedger.frontier)
|
|
900
|
-
throw protocolError("SESSION_KEY_UPDATE watermark mismatch");
|
|
901
|
-
this.pendingReceiveEpoch = nextEpoch;
|
|
902
|
-
const current = this.rootForReceive(this.receiveEpoch);
|
|
903
|
-
if (!this.receiveRoots.has(nextEpoch)) {
|
|
904
|
-
this.receiveRoots.set(nextEpoch, deriveEpochRoots(deriveNextEpoch(current.rekeyRoot, this.h3, this.receiveDirection, nextEpoch)));
|
|
905
|
-
}
|
|
906
|
-
const streams = [...this.streams.values()];
|
|
907
|
-
await Promise.all(streams.map(async (stream) => await stream.waitReceiveRekey(transition, nextEpoch, signal)));
|
|
908
|
-
const acknowledged = await raceAbort(this.sendControlResponse(InnerTypeV2.SessionKeyUpdateACK, payload), signal);
|
|
909
|
-
if (!acknowledged) {
|
|
910
|
-
this.pendingReceiveEpoch = undefined;
|
|
911
|
-
this.cleanupEpochRoots();
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
throwIfAborted(signal);
|
|
915
|
-
this.receiveEpoch = nextEpoch;
|
|
916
|
-
this.receiveTransition = transition;
|
|
917
|
-
this.pendingReceiveEpoch = undefined;
|
|
918
|
-
for (const stream of streams)
|
|
919
|
-
stream.publishReceiveRekey(transition, nextEpoch);
|
|
920
|
-
this.cleanupEpochRoots();
|
|
921
|
-
}
|
|
922
|
-
receiveSessionRekeyACK(payload) {
|
|
923
|
-
const pending = this.pendingSessionRekey;
|
|
924
|
-
if (pending === undefined) {
|
|
925
|
-
if (this.lastSessionRekeyACK !== undefined && bytesEqual(payload, this.lastSessionRekeyACK))
|
|
926
|
-
return;
|
|
927
|
-
throw protocolError("unexpected SESSION_KEY_UPDATE_ACK");
|
|
928
|
-
}
|
|
929
|
-
if (!bytesEqual(payload, pending.payload))
|
|
930
|
-
throw protocolError("unexpected SESSION_KEY_UPDATE_ACK");
|
|
931
|
-
if (pending.committed.value)
|
|
932
|
-
return;
|
|
933
|
-
pending.committed.value = true;
|
|
934
|
-
this.sendEpoch = pending.epoch;
|
|
935
|
-
this.controlSendEpoch = pending.epoch;
|
|
936
|
-
this.controlSendSequence = 0n;
|
|
937
|
-
this.lastSessionRekeyACK = payload.slice();
|
|
938
|
-
pending.acknowledged.resolve();
|
|
939
|
-
this.cleanupEpochRoots();
|
|
940
|
-
}
|
|
941
|
-
async waitOpenGate(signal) {
|
|
942
|
-
while (this.openFrozen)
|
|
943
|
-
await raceAbort(this.openGate.promise, signal);
|
|
944
|
-
}
|
|
945
|
-
resolveOutboundOpen(id) {
|
|
946
|
-
this.outboundLedger.validOpen(id);
|
|
947
|
-
this.notifyOutboundFrontierChanged();
|
|
948
|
-
}
|
|
949
|
-
async waitOutboundFrontier(watermark, signal) {
|
|
950
|
-
while (this.outboundLedger.frontier !== watermark) {
|
|
951
|
-
if (this.outboundLedger.frontier > watermark)
|
|
952
|
-
throw protocolError("outbound frontier exceeded watermark");
|
|
953
|
-
await raceAbort(this.outboundFrontierChanged.promise, signal);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
async commitOutboundReset(id) {
|
|
957
|
-
if (await this.sendControlCleanup(InnerTypeV2.StreamReset, idReason(id, 6))) {
|
|
958
|
-
this.outboundLedger.localResetCommitted(id);
|
|
959
|
-
this.notifyOutboundFrontierChanged();
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
commitLocalReset(id) {
|
|
963
|
-
if (this.isLocalLogicalID(id)) {
|
|
964
|
-
this.outboundLedger.localResetCommitted(id);
|
|
965
|
-
this.notifyOutboundFrontierChanged();
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
this.peerLedger.localResetCommitted(id);
|
|
969
|
-
}
|
|
970
|
-
notifyOutboundFrontierChanged() {
|
|
971
|
-
this.outboundFrontierChanged.resolve();
|
|
972
|
-
this.outboundFrontierChanged = deferred();
|
|
973
|
-
}
|
|
974
|
-
isLocalLogicalID(id) {
|
|
975
|
-
return id > 0n && (this.role === 1 ? (id & 1n) === 1n : (id & 1n) === 0n);
|
|
976
|
-
}
|
|
977
|
-
localOpenHighWatermark() {
|
|
978
|
-
const first = this.role === 1 ? 1n : 2n;
|
|
979
|
-
return this.nextLogicalID === first ? 0n : this.nextLogicalID - 2n;
|
|
980
|
-
}
|
|
981
|
-
validGoAwayBoundary(lastAccepted) {
|
|
982
|
-
if (lastAccepted === 0n)
|
|
983
|
-
return true;
|
|
984
|
-
return this.isLocalLogicalID(lastAccepted) && lastAccepted <= this.localOpenHighWatermark();
|
|
985
|
-
}
|
|
986
|
-
async sendGoAway(reason) {
|
|
987
|
-
if (!Number.isInteger(reason) || reason < 1 || reason > 0xffff)
|
|
988
|
-
throw protocolError("invalid GOAWAY reason");
|
|
989
|
-
if (this.sentGoAway) {
|
|
990
|
-
if (this.sentGoAwayReason !== reason)
|
|
991
|
-
throw protocolError("conflicting local GOAWAY reason");
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
const lastAccepted = this.peerLedger.frontier;
|
|
995
|
-
this.sentGoAway = true;
|
|
996
|
-
this.sentGoAwayLastAccepted = lastAccepted;
|
|
997
|
-
this.sentGoAwayReason = reason;
|
|
998
|
-
await this.sendControl(InnerTypeV2.GoAway, idReason(lastAccepted, reason));
|
|
999
|
-
}
|
|
1000
|
-
async receiveGoAway(payload) {
|
|
1001
|
-
const { id: lastAccepted, reason } = parseIDReason(payload);
|
|
1002
|
-
if (reason === 0 || !this.validGoAwayBoundary(lastAccepted))
|
|
1003
|
-
throw protocolError("invalid GOAWAY boundary");
|
|
1004
|
-
if (this.receivedGoAway) {
|
|
1005
|
-
if (this.receivedGoAwayLastAccepted !== lastAccepted || this.receivedGoAwayReason !== reason) {
|
|
1006
|
-
throw protocolError("conflicting GOAWAY");
|
|
1007
|
-
}
|
|
1008
|
-
return;
|
|
1009
|
-
}
|
|
1010
|
-
this.receivedGoAway = true;
|
|
1011
|
-
this.receivedGoAwayLastAccepted = lastAccepted;
|
|
1012
|
-
this.receivedGoAwayReason = reason;
|
|
1013
|
-
const excluded = [...this.streams.values()].filter((stream) => this.isLocalLogicalID(stream.id) && stream.id > lastAccepted);
|
|
1014
|
-
for (const stream of excluded) {
|
|
1015
|
-
this.trackBackground(this.localReset(stream, new SessionV2Error("going_away", "logical stream exceeds peer GOAWAY boundary")));
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
trackBackground(task) {
|
|
1019
|
-
this.backgroundTasks.add(task);
|
|
1020
|
-
void task.catch((error) => {
|
|
1021
|
-
if (this.lifecycle !== "closed")
|
|
1022
|
-
this.fail(asError(error));
|
|
1023
|
-
}).finally(() => this.backgroundTasks.delete(task));
|
|
1024
|
-
}
|
|
1025
|
-
localOpeningAllowedAfterGoAway(id) {
|
|
1026
|
-
return !this.receivedGoAway || id <= this.receivedGoAwayLastAccepted;
|
|
1027
|
-
}
|
|
1028
|
-
acceptsPeerStreamAfterGoAway(id) {
|
|
1029
|
-
return !this.sentGoAway || id <= this.sentGoAwayLastAccepted;
|
|
1030
|
-
}
|
|
1031
|
-
cleanupEpochRoots() {
|
|
1032
|
-
const sendInUse = new Set([this.sendEpoch, this.controlSendEpoch]);
|
|
1033
|
-
const receiveInUse = new Set([this.receiveEpoch, this.controlReceiveEpoch]);
|
|
1034
|
-
if (this.preparingSendEpoch !== undefined)
|
|
1035
|
-
sendInUse.add(this.preparingSendEpoch);
|
|
1036
|
-
if (this.pendingReceiveEpoch !== undefined)
|
|
1037
|
-
receiveInUse.add(this.pendingReceiveEpoch);
|
|
1038
|
-
if (this.pendingSessionRekey !== undefined)
|
|
1039
|
-
sendInUse.add(this.pendingSessionRekey.epoch);
|
|
1040
|
-
for (const stream of this.streams.values()) {
|
|
1041
|
-
for (const epoch of stream.sendEpochsInUse())
|
|
1042
|
-
sendInUse.add(epoch);
|
|
1043
|
-
for (const epoch of stream.receiveEpochsInUse())
|
|
1044
|
-
receiveInUse.add(epoch);
|
|
1045
|
-
}
|
|
1046
|
-
this.cleanupRootMap(this.sendRoots, sendInUse);
|
|
1047
|
-
this.cleanupRootMap(this.receiveRoots, receiveInUse);
|
|
1048
|
-
}
|
|
1049
|
-
streamEpochStateChanged() {
|
|
1050
|
-
this.cleanupEpochRoots();
|
|
1051
|
-
}
|
|
1052
|
-
cleanupRootMap(roots, inUse) {
|
|
1053
|
-
for (const [epoch, value] of roots) {
|
|
1054
|
-
if (inUse.has(epoch))
|
|
1055
|
-
continue;
|
|
1056
|
-
wipeEpochRoots(value);
|
|
1057
|
-
roots.delete(epoch);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
wipeAllRoots() {
|
|
1061
|
-
for (const roots of this.sendRoots.values())
|
|
1062
|
-
wipeEpochRoots(roots);
|
|
1063
|
-
for (const roots of this.receiveRoots.values())
|
|
1064
|
-
wipeEpochRoots(roots);
|
|
1065
|
-
this.sendRoots.clear();
|
|
1066
|
-
this.receiveRoots.clear();
|
|
1067
|
-
}
|
|
1068
|
-
startIdleWatchdog() {
|
|
1069
|
-
this.idleWatchdogStarted = true;
|
|
1070
|
-
this.markAuthenticatedActivity();
|
|
1071
|
-
}
|
|
1072
|
-
markAuthenticatedActivity() {
|
|
1073
|
-
if (!this.idleWatchdogStarted || this.lifecycle !== "open")
|
|
1074
|
-
return;
|
|
1075
|
-
const timeoutMs = sessionIdleTimeoutMs(this.config);
|
|
1076
|
-
if (timeoutMs === 0)
|
|
1077
|
-
return;
|
|
1078
|
-
this.idleTimerCancel?.();
|
|
1079
|
-
this.idleTimerCancel = scheduleLongTimeout(() => {
|
|
1080
|
-
this.idleTimerCancel = undefined;
|
|
1081
|
-
this.fail(new SessionV2Error("timeout", "Flowersec v2 session idle timeout exceeded"));
|
|
1082
|
-
}, timeoutMs);
|
|
1083
|
-
}
|
|
1084
|
-
fail(error, abortCarrier = true) {
|
|
1085
|
-
if (this.lifecycle === "closed")
|
|
1086
|
-
return;
|
|
1087
|
-
this.terminationController.abort(error);
|
|
1088
|
-
this.controlTerminalSealed = true;
|
|
1089
|
-
this.beginClosing();
|
|
1090
|
-
const normalPeerCarrierClose = this.sessionCloseCommitted && error instanceof CarrierError && error.code === "closed";
|
|
1091
|
-
if (normalPeerCarrierClose)
|
|
1092
|
-
this.peerSessionClose.resolve();
|
|
1093
|
-
this.terminalError = error;
|
|
1094
|
-
this.lifecycle = "closed";
|
|
1095
|
-
this.terminationState.resolve({ error });
|
|
1096
|
-
this.idleTimerCancel?.();
|
|
1097
|
-
this.idleTimerCancel = undefined;
|
|
1098
|
-
this.incoming.fail(error);
|
|
1099
|
-
this.outboundPermits.fail(error);
|
|
1100
|
-
this.inboundPermits.fail(error);
|
|
1101
|
-
this.notifyResponderChanged();
|
|
1102
|
-
for (const ping of this.pings.values())
|
|
1103
|
-
ping.reject(error);
|
|
1104
|
-
this.pings.clear();
|
|
1105
|
-
this.rpc.close();
|
|
1106
|
-
for (const stream of [...this.streams.values()])
|
|
1107
|
-
stream.peerReset(error);
|
|
1108
|
-
for (const released of this.releasedStreams.values()) {
|
|
1109
|
-
const stream = released.deref();
|
|
1110
|
-
if (stream !== undefined)
|
|
1111
|
-
this.releasedStreamCleanup.unregister(stream);
|
|
1112
|
-
}
|
|
1113
|
-
this.releasedStreams.clear();
|
|
1114
|
-
this.wipeAllRoots();
|
|
1115
|
-
this.h3.fill(0);
|
|
1116
|
-
if (abortCarrier && !normalPeerCarrierClose)
|
|
1117
|
-
this.carrier.abort({ code: 6, reason: "session terminated" });
|
|
1118
|
-
}
|
|
1119
|
-
assertOpen() {
|
|
1120
|
-
if (this.lifecycle === "open")
|
|
1121
|
-
return;
|
|
1122
|
-
if (this.terminalError !== undefined)
|
|
1123
|
-
throw this.terminalError;
|
|
1124
|
-
throw new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
1125
|
-
}
|
|
1126
|
-
beginClosing() {
|
|
1127
|
-
if (this.lifecycle === "opening" || this.lifecycle === "open")
|
|
1128
|
-
this.lifecycle = "closing";
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
class EncryptedStreamV2 {
|
|
1132
|
-
session;
|
|
1133
|
-
carrier;
|
|
1134
|
-
id;
|
|
1135
|
-
kind;
|
|
1136
|
-
sendEpoch;
|
|
1137
|
-
receiveEpoch;
|
|
1138
|
-
permitRelease;
|
|
1139
|
-
reader;
|
|
1140
|
-
opened = deferred();
|
|
1141
|
-
data = new ByteQueue(MAX_BUFFERED_STREAM_BYTES);
|
|
1142
|
-
terminalError;
|
|
1143
|
-
sendSequence = 0n;
|
|
1144
|
-
receiveSequence = 0n;
|
|
1145
|
-
priorACK;
|
|
1146
|
-
sendTail = Promise.resolve();
|
|
1147
|
-
localFIN = false;
|
|
1148
|
-
remoteFIN = false;
|
|
1149
|
-
pumpStarted = false;
|
|
1150
|
-
permitReleased = false;
|
|
1151
|
-
closeWriteTask;
|
|
1152
|
-
pendingSendRekey;
|
|
1153
|
-
lastSendRekeyACK;
|
|
1154
|
-
receiveRekey;
|
|
1155
|
-
receiveRekeyChanged = deferred();
|
|
1156
|
-
constructor(session, carrier, id, kind, sendEpoch, receiveEpoch, permitRelease, reader) {
|
|
1157
|
-
this.session = session;
|
|
1158
|
-
this.carrier = carrier;
|
|
1159
|
-
this.id = id;
|
|
1160
|
-
this.kind = kind;
|
|
1161
|
-
this.sendEpoch = sendEpoch;
|
|
1162
|
-
this.receiveEpoch = receiveEpoch;
|
|
1163
|
-
this.permitRelease = permitRelease;
|
|
1164
|
-
this.reader = reader ?? new ExactReader(carrier);
|
|
1165
|
-
}
|
|
1166
|
-
async read(options = {}) {
|
|
1167
|
-
if (this.terminalError !== undefined)
|
|
1168
|
-
throw this.terminalError;
|
|
1169
|
-
return await this.data.read(options.signal);
|
|
1170
|
-
}
|
|
1171
|
-
async write(payload, options = {}) {
|
|
1172
|
-
if (!(payload instanceof Uint8Array))
|
|
1173
|
-
throw new TypeError("stream write requires Uint8Array");
|
|
1174
|
-
if (payload.length === 0)
|
|
1175
|
-
return 0;
|
|
1176
|
-
throwIfAborted(options.signal);
|
|
1177
|
-
await this.opened.promise;
|
|
1178
|
-
await this.waitSendRekey(options.signal);
|
|
1179
|
-
if (this.localFIN)
|
|
1180
|
-
throw new SessionV2Error("closed", "write after FIN");
|
|
1181
|
-
if (this.terminalError !== undefined)
|
|
1182
|
-
throw this.terminalError;
|
|
1183
|
-
const copy = payload.slice();
|
|
1184
|
-
await this.enqueueSend(async () => {
|
|
1185
|
-
try {
|
|
1186
|
-
for (let offset = 0; offset < copy.length; offset += MAX_DATA_BYTES) {
|
|
1187
|
-
throwIfAborted(options.signal);
|
|
1188
|
-
await this.session.sendStreamRecord(this, InnerTypeV2.Data, copy.subarray(offset, Math.min(copy.length, offset + MAX_DATA_BYTES)), options.signal);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
catch (error) {
|
|
1192
|
-
const normalized = asError(error);
|
|
1193
|
-
void this.session.localReset(this, normalized);
|
|
1194
|
-
throw normalized;
|
|
1195
|
-
}
|
|
1196
|
-
});
|
|
1197
|
-
return copy.length;
|
|
1198
|
-
}
|
|
1199
|
-
async closeWrite() {
|
|
1200
|
-
await this.opened.promise;
|
|
1201
|
-
await this.waitSendRekey();
|
|
1202
|
-
if (this.localFIN)
|
|
1203
|
-
return;
|
|
1204
|
-
if (this.terminalError !== undefined)
|
|
1205
|
-
throw this.terminalError;
|
|
1206
|
-
this.closeWriteTask ??= this.enqueueSend(async () => {
|
|
1207
|
-
try {
|
|
1208
|
-
if (this.localFIN)
|
|
1209
|
-
return;
|
|
1210
|
-
await this.session.sendStreamRecord(this, InnerTypeV2.FIN, new Uint8Array());
|
|
1211
|
-
await this.carrier.closeWrite();
|
|
1212
|
-
this.localFIN = true;
|
|
1213
|
-
this.releaseIfClean();
|
|
1214
|
-
}
|
|
1215
|
-
catch (error) {
|
|
1216
|
-
const normalized = asError(error);
|
|
1217
|
-
void this.session.localReset(this, normalized);
|
|
1218
|
-
throw normalized;
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
await this.closeWriteTask;
|
|
1222
|
-
}
|
|
1223
|
-
async reset() {
|
|
1224
|
-
await this.session.localReset(this, new SessionV2Error("stream_reset", "logical stream reset"));
|
|
1225
|
-
}
|
|
1226
|
-
async close() {
|
|
1227
|
-
await this.reset();
|
|
1228
|
-
}
|
|
1229
|
-
send(type, payload, signal) {
|
|
1230
|
-
return this.enqueueSend(async () => await this.session.sendStreamRecord(this, type, payload, signal));
|
|
1231
|
-
}
|
|
1232
|
-
canRekeySend() {
|
|
1233
|
-
return this.terminalError === undefined && this.opened.settled && !this.localFIN;
|
|
1234
|
-
}
|
|
1235
|
-
hasRemoteFIN() {
|
|
1236
|
-
return this.remoteFIN;
|
|
1237
|
-
}
|
|
1238
|
-
startSendRekey(transition, epoch) {
|
|
1239
|
-
if (this.pendingSendRekey !== undefined)
|
|
1240
|
-
throw protocolError("overlapping stream rekey");
|
|
1241
|
-
const pending = { transition, epoch, armed: deferred(), done: deferred() };
|
|
1242
|
-
this.pendingSendRekey = pending;
|
|
1243
|
-
void this.enqueueSend(async () => {
|
|
1244
|
-
await this.session.sendStreamRecord(this, InnerTypeV2.StreamKeyUpdate, concat(u64(transition), u32(epoch)));
|
|
1245
|
-
pending.armed.resolve();
|
|
1246
|
-
}).catch((error) => {
|
|
1247
|
-
pending.armed.reject(error);
|
|
1248
|
-
pending.done.reject(error);
|
|
1249
|
-
void this.session.localReset(this, asError(error));
|
|
1250
|
-
});
|
|
1251
|
-
return pending;
|
|
1252
|
-
}
|
|
1253
|
-
async waitReceiveRekey(transition, epoch, signal) {
|
|
1254
|
-
throwIfAborted(signal);
|
|
1255
|
-
if (this.terminalError !== undefined || this.remoteFIN)
|
|
1256
|
-
return;
|
|
1257
|
-
while (true) {
|
|
1258
|
-
const pending = this.receiveRekey;
|
|
1259
|
-
if (pending !== undefined) {
|
|
1260
|
-
if (pending.transition !== transition || pending.epoch !== epoch)
|
|
1261
|
-
throw protocolError("stream rekey mismatch");
|
|
1262
|
-
await raceAbort(pending.acknowledged.promise, signal);
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
await raceAbort(this.receiveRekeyChanged.promise, signal);
|
|
1266
|
-
if (this.terminalError !== undefined || this.remoteFIN)
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
publishReceiveRekey(transition, epoch) {
|
|
1271
|
-
if (this.receiveRekey?.transition === transition && this.receiveRekey.epoch === epoch) {
|
|
1272
|
-
this.receiveRekey = undefined;
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
notifyReceiveRekeyChanged() {
|
|
1276
|
-
const changed = this.receiveRekeyChanged;
|
|
1277
|
-
this.receiveRekeyChanged = deferred();
|
|
1278
|
-
changed.resolve();
|
|
1279
|
-
}
|
|
1280
|
-
startPump() {
|
|
1281
|
-
if (this.pumpStarted)
|
|
1282
|
-
return;
|
|
1283
|
-
this.pumpStarted = true;
|
|
1284
|
-
void this.pump();
|
|
1285
|
-
}
|
|
1286
|
-
markOpen() {
|
|
1287
|
-
this.opened.resolve();
|
|
1288
|
-
}
|
|
1289
|
-
markTerminal(error) {
|
|
1290
|
-
if (this.terminalError !== undefined)
|
|
1291
|
-
return false;
|
|
1292
|
-
this.terminalError = error;
|
|
1293
|
-
const pendingSendRekey = this.pendingSendRekey;
|
|
1294
|
-
this.pendingSendRekey = undefined;
|
|
1295
|
-
pendingSendRekey?.armed.reject(error);
|
|
1296
|
-
pendingSendRekey?.done.reject(error);
|
|
1297
|
-
this.notifyReceiveRekeyChanged();
|
|
1298
|
-
this.opened.reject(error);
|
|
1299
|
-
this.data.fail(error);
|
|
1300
|
-
return true;
|
|
1301
|
-
}
|
|
1302
|
-
peerReset(error) {
|
|
1303
|
-
if (!this.markTerminal(error))
|
|
1304
|
-
return;
|
|
1305
|
-
this.carrier.abort(error);
|
|
1306
|
-
this.session.releaseStream(this);
|
|
1307
|
-
}
|
|
1308
|
-
carrierReset() {
|
|
1309
|
-
if (!this.markTerminal(new SessionV2Error("stream_reset", "logical stream reset by peer")))
|
|
1310
|
-
return;
|
|
1311
|
-
this.session.releaseStream(this);
|
|
1312
|
-
}
|
|
1313
|
-
abort(error = new SessionV2Error("closed", "logical stream aborted")) {
|
|
1314
|
-
this.peerReset(error);
|
|
1315
|
-
}
|
|
1316
|
-
releasePermit() {
|
|
1317
|
-
if (this.permitReleased)
|
|
1318
|
-
return;
|
|
1319
|
-
this.permitReleased = true;
|
|
1320
|
-
this.permitRelease();
|
|
1321
|
-
}
|
|
1322
|
-
async pump() {
|
|
1323
|
-
try {
|
|
1324
|
-
while (this.terminalError === undefined) {
|
|
1325
|
-
const record = await this.session.readStreamRecord(this);
|
|
1326
|
-
if (!this.opened.settled) {
|
|
1327
|
-
if (record.type === InnerTypeV2.OpenACK) {
|
|
1328
|
-
const got = decodeOpenACKV2(record.payload);
|
|
1329
|
-
if (!bytesEqual(got, this.openedHash()))
|
|
1330
|
-
throw protocolError("OPEN ACK hash mismatch");
|
|
1331
|
-
this.session.resolveOutboundOpen(this.id);
|
|
1332
|
-
this.markOpen();
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
if (record.type === InnerTypeV2.OpenReject) {
|
|
1336
|
-
const reject = decodeOpenRejectV2(record.payload);
|
|
1337
|
-
if (!bytesEqual(reject.openHash, this.openedHash()))
|
|
1338
|
-
throw protocolError("OPEN REJECT hash mismatch");
|
|
1339
|
-
this.session.resolveOutboundOpen(this.id);
|
|
1340
|
-
const error = new SessionV2Error("open_rejected", `logical stream rejected (${reject.reason})`);
|
|
1341
|
-
this.markTerminal(error);
|
|
1342
|
-
await this.carrier.closeWrite().catch(() => undefined);
|
|
1343
|
-
this.session.releaseStream(this);
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
throw protocolError("expected OPEN ACK or REJECT");
|
|
1347
|
-
}
|
|
1348
|
-
switch (record.type) {
|
|
1349
|
-
case InnerTypeV2.Data:
|
|
1350
|
-
if (this.remoteFIN)
|
|
1351
|
-
throw protocolError("DATA after FIN");
|
|
1352
|
-
await this.data.waitForCapacity(record.payload.length);
|
|
1353
|
-
this.data.push(record.payload);
|
|
1354
|
-
break;
|
|
1355
|
-
case InnerTypeV2.FIN:
|
|
1356
|
-
if (this.remoteFIN)
|
|
1357
|
-
throw protocolError("duplicate FIN");
|
|
1358
|
-
this.remoteFIN = true;
|
|
1359
|
-
this.data.close();
|
|
1360
|
-
this.releaseIfClean();
|
|
1361
|
-
break;
|
|
1362
|
-
case InnerTypeV2.StreamKeyUpdate:
|
|
1363
|
-
await this.receiveStreamKeyUpdate(record.payload, record.header);
|
|
1364
|
-
break;
|
|
1365
|
-
case InnerTypeV2.StreamKeyUpdateACK:
|
|
1366
|
-
this.receiveStreamKeyUpdateACK(record.payload);
|
|
1367
|
-
break;
|
|
1368
|
-
default:
|
|
1369
|
-
throw protocolError(`unexpected stream type ${record.type}`);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
catch (error) {
|
|
1374
|
-
const normalized = asError(error);
|
|
1375
|
-
if (normalized instanceof CarrierError && normalized.code === "reset") {
|
|
1376
|
-
this.carrierReset();
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
if (this.remoteFIN && /closed|EOF/i.test(normalized.message)) {
|
|
1380
|
-
this.releaseIfClean();
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
await this.session.localReset(this, normalized);
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
openedHash() {
|
|
1387
|
-
const value = this.openHash;
|
|
1388
|
-
if (value === undefined)
|
|
1389
|
-
throw protocolError("missing OPEN hash");
|
|
1390
|
-
return value;
|
|
1391
|
-
}
|
|
1392
|
-
setOpenHash(value) {
|
|
1393
|
-
this.openHash = value.slice();
|
|
1394
|
-
}
|
|
1395
|
-
enqueueSend(operation) {
|
|
1396
|
-
const task = this.sendTail.then(async () => {
|
|
1397
|
-
if (this.terminalError !== undefined)
|
|
1398
|
-
throw this.terminalError;
|
|
1399
|
-
await operation();
|
|
1400
|
-
});
|
|
1401
|
-
this.sendTail = task.catch(() => undefined);
|
|
1402
|
-
return task;
|
|
1403
|
-
}
|
|
1404
|
-
async waitSendRekey(signal) {
|
|
1405
|
-
while (this.pendingSendRekey !== undefined) {
|
|
1406
|
-
await raceAbort(this.pendingSendRekey.done.promise, signal);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
async receiveStreamKeyUpdate(payload, header) {
|
|
1410
|
-
if (payload.length !== 12 || this.receiveRekey !== undefined)
|
|
1411
|
-
throw protocolError("invalid STREAM_KEY_UPDATE");
|
|
1412
|
-
const transition = readU64(payload);
|
|
1413
|
-
const nextEpoch = readU32(payload, 8);
|
|
1414
|
-
if (transition === 0n || nextEpoch !== this.receiveEpoch + 1)
|
|
1415
|
-
throw protocolError("invalid stream rekey transition");
|
|
1416
|
-
const current = this.session.rootForReceive(this.receiveEpoch);
|
|
1417
|
-
if (!this.session.hasReceiveRoots(nextEpoch)) {
|
|
1418
|
-
this.session.installReceiveRoots(nextEpoch, deriveEpochRoots(deriveNextEpoch(current.rekeyRoot, this.session.transcriptHash(), this.session.receiveDirectionValue(), nextEpoch)));
|
|
1419
|
-
}
|
|
1420
|
-
if (this.pendingSendRekey?.transition === transition && this.pendingSendRekey.epoch === nextEpoch) {
|
|
1421
|
-
this.priorACK = { epoch: header.epoch, sequence: this.receiveSequence };
|
|
1422
|
-
}
|
|
1423
|
-
this.receiveEpoch = nextEpoch;
|
|
1424
|
-
this.receiveSequence = 0n;
|
|
1425
|
-
const pending = { transition, epoch: nextEpoch, acknowledged: deferred() };
|
|
1426
|
-
this.receiveRekey = pending;
|
|
1427
|
-
this.notifyReceiveRekeyChanged();
|
|
1428
|
-
await this.send(InnerTypeV2.StreamKeyUpdateACK, encodeStreamKeyUpdateACKV2({
|
|
1429
|
-
logicalStreamID: this.id,
|
|
1430
|
-
transition,
|
|
1431
|
-
epoch: nextEpoch,
|
|
1432
|
-
}), this.session.terminationSignal());
|
|
1433
|
-
pending.acknowledged.resolve();
|
|
1434
|
-
}
|
|
1435
|
-
receiveStreamKeyUpdateACK(payload) {
|
|
1436
|
-
const decoded = decodeStreamKeyUpdateACKV2(payload);
|
|
1437
|
-
if (decoded.logicalStreamID !== this.id)
|
|
1438
|
-
throw protocolError("invalid STREAM_KEY_UPDATE_ACK");
|
|
1439
|
-
const { transition, epoch } = decoded;
|
|
1440
|
-
const pending = this.pendingSendRekey;
|
|
1441
|
-
if (pending === undefined) {
|
|
1442
|
-
if (this.lastSendRekeyACK?.transition === transition && this.lastSendRekeyACK.epoch === epoch)
|
|
1443
|
-
return;
|
|
1444
|
-
throw protocolError("unexpected STREAM_KEY_UPDATE_ACK");
|
|
1445
|
-
}
|
|
1446
|
-
if (pending.transition !== transition || pending.epoch !== epoch)
|
|
1447
|
-
throw protocolError("unexpected STREAM_KEY_UPDATE_ACK");
|
|
1448
|
-
this.sendEpoch = epoch;
|
|
1449
|
-
this.sendSequence = 0n;
|
|
1450
|
-
this.pendingSendRekey = undefined;
|
|
1451
|
-
this.lastSendRekeyACK = { transition, epoch };
|
|
1452
|
-
pending.done.resolve();
|
|
1453
|
-
this.session.streamEpochStateChanged();
|
|
1454
|
-
}
|
|
1455
|
-
sendEpochsInUse() {
|
|
1456
|
-
return this.pendingSendRekey === undefined ? [this.sendEpoch] : [this.sendEpoch, this.pendingSendRekey.epoch];
|
|
1457
|
-
}
|
|
1458
|
-
receiveEpochsInUse() {
|
|
1459
|
-
const epochs = new Set([this.receiveEpoch]);
|
|
1460
|
-
if (this.priorACK !== undefined)
|
|
1461
|
-
epochs.add(this.priorACK.epoch);
|
|
1462
|
-
if (this.receiveRekey !== undefined)
|
|
1463
|
-
epochs.add(this.receiveRekey.epoch);
|
|
1464
|
-
return [...epochs];
|
|
1465
|
-
}
|
|
1466
|
-
releaseIfClean() {
|
|
1467
|
-
if (!this.localFIN || !this.remoteFIN || this.terminalError !== undefined)
|
|
1468
|
-
return;
|
|
1469
|
-
this.session.releaseStream(this);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
async function clientHandshake(control, reader, config, offeredFeatures, signal) {
|
|
1473
|
-
const key = generateEphemeralKeyV2(config.suite, config.runtime.entropy);
|
|
1474
|
-
const fsc2 = encodeControlPrefaceV2();
|
|
1475
|
-
const initRaw = encodeClientInitV2({
|
|
1476
|
-
profile: "flowersec/2",
|
|
1477
|
-
channelID: config.channelID,
|
|
1478
|
-
sessionContractHash: config.sessionContractHash,
|
|
1479
|
-
clientRole: 1,
|
|
1480
|
-
suite: config.suite,
|
|
1481
|
-
clientEphemeralPublic: key.publicKey,
|
|
1482
|
-
nonceC: sessionRandomBytes(config, 32),
|
|
1483
|
-
selectedFeatures: offeredFeatures,
|
|
1484
|
-
maxInboundStreams: config.maxInboundStreams,
|
|
1485
|
-
clientAdmissionBinding: config.localAdmissionBinding,
|
|
1486
|
-
clientEndpointInstanceID: config.localEndpointInstanceID,
|
|
1487
|
-
});
|
|
1488
|
-
await writeAll(control, fsc2, signal);
|
|
1489
|
-
await writeAll(control, initRaw, signal);
|
|
1490
|
-
const serverRaw = await readHandshakeFrame(reader, signal);
|
|
1491
|
-
const server = decodeServerFinishedV2(serverRaw, config.suite);
|
|
1492
|
-
validateServerFinishedV2(server, expectations(config, false));
|
|
1493
|
-
if ((server.core.selectedFeatures & ~offeredFeatures) !== 0) {
|
|
1494
|
-
throw protocolError("server selected an unoffered feature");
|
|
1495
|
-
}
|
|
1496
|
-
const shared = computeSharedSecretV2(config.suite, key.privateKey, server.core.serverEphemeralPublic);
|
|
1497
|
-
const handshakePRK = deriveHandshakePRKV2(config.psk, shared);
|
|
1498
|
-
const h0 = computeHandshakeH0V2(fsc2, initRaw);
|
|
1499
|
-
const h1 = computeHandshakeH1V2(h0, encodeServerFinishedCoreV2(server.core, config.suite));
|
|
1500
|
-
if (!bytesEqual(server.serverConfirm, computeServerConfirmV2(handshakePRK, h1))) {
|
|
1501
|
-
throw protocolError("server confirm mismatch");
|
|
1502
|
-
}
|
|
1503
|
-
const clientCore = encodeClientFinishedCoreV2(server.core.handshakeID);
|
|
1504
|
-
const h2 = computeHandshakeH2V2(h1, serverRaw, clientCore);
|
|
1505
|
-
const clientRaw = encodeClientFinishedV2({
|
|
1506
|
-
handshakeID: server.core.handshakeID,
|
|
1507
|
-
clientConfirm: computeClientConfirmV2(handshakePRK, h2),
|
|
1508
|
-
});
|
|
1509
|
-
await writeAll(control, clientRaw, signal);
|
|
1510
|
-
const h3 = computeHandshakeH3V2(h2, clientRaw);
|
|
1511
|
-
return {
|
|
1512
|
-
h3,
|
|
1513
|
-
sessionPRK: deriveSessionPRKV2(h3, handshakePRK),
|
|
1514
|
-
selectedFeatures: server.core.selectedFeatures,
|
|
1515
|
-
};
|
|
1516
|
-
}
|
|
1517
|
-
async function serverHandshake(control, reader, config, localFeatures, signal) {
|
|
1518
|
-
const fsc2 = await reader.readExactly(16, signal);
|
|
1519
|
-
parseControlPrefaceV2(fsc2);
|
|
1520
|
-
const clientRaw = await readHandshakeFrame(reader, signal);
|
|
1521
|
-
const client = decodeClientInitV2(clientRaw);
|
|
1522
|
-
validateClientInitV2(client, expectations(config, true));
|
|
1523
|
-
const key = generateEphemeralKeyV2(config.suite, config.runtime.entropy);
|
|
1524
|
-
const shared = computeSharedSecretV2(config.suite, key.privateKey, client.clientEphemeralPublic);
|
|
1525
|
-
const handshakePRK = deriveHandshakePRKV2(config.psk, shared);
|
|
1526
|
-
const core = {
|
|
1527
|
-
suite: config.suite,
|
|
1528
|
-
handshakeID: sessionRandomBytes(config, 16),
|
|
1529
|
-
serverEphemeralPublic: key.publicKey,
|
|
1530
|
-
nonceS: sessionRandomBytes(config, 32),
|
|
1531
|
-
sessionContractHash: config.sessionContractHash,
|
|
1532
|
-
selectedFeatures: client.selectedFeatures & localFeatures,
|
|
1533
|
-
maxInboundStreams: config.maxInboundStreams,
|
|
1534
|
-
serverAdmissionBinding: config.localAdmissionBinding,
|
|
1535
|
-
serverEndpointInstanceID: config.localEndpointInstanceID,
|
|
1536
|
-
};
|
|
1537
|
-
const h0 = computeHandshakeH0V2(fsc2, clientRaw);
|
|
1538
|
-
const h1 = computeHandshakeH1V2(h0, encodeServerFinishedCoreV2(core, config.suite));
|
|
1539
|
-
const serverRaw = encodeServerFinishedV2({
|
|
1540
|
-
core,
|
|
1541
|
-
serverConfirm: computeServerConfirmV2(handshakePRK, h1),
|
|
1542
|
-
}, config.suite);
|
|
1543
|
-
await writeAll(control, serverRaw, signal);
|
|
1544
|
-
const clientFinishedRaw = await readHandshakeFrame(reader, signal);
|
|
1545
|
-
const clientFinished = decodeClientFinishedV2(clientFinishedRaw);
|
|
1546
|
-
if (!bytesEqual(clientFinished.handshakeID, core.handshakeID))
|
|
1547
|
-
throw protocolError("handshake ID mismatch");
|
|
1548
|
-
const clientCore = encodeClientFinishedCoreV2(clientFinished.handshakeID);
|
|
1549
|
-
const h2 = computeHandshakeH2V2(h1, serverRaw, clientCore);
|
|
1550
|
-
if (!bytesEqual(clientFinished.clientConfirm, computeClientConfirmV2(handshakePRK, h2))) {
|
|
1551
|
-
throw protocolError("client confirm mismatch");
|
|
1552
|
-
}
|
|
1553
|
-
const h3 = computeHandshakeH3V2(h2, clientFinishedRaw);
|
|
1554
|
-
return {
|
|
1555
|
-
h3,
|
|
1556
|
-
sessionPRK: deriveSessionPRKV2(h3, handshakePRK),
|
|
1557
|
-
selectedFeatures: core.selectedFeatures,
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
function expectations(config, peerIsClient) {
|
|
1561
|
-
return {
|
|
1562
|
-
path: config.path,
|
|
1563
|
-
channelID: peerIsClient ? config.channelID : "",
|
|
1564
|
-
sessionContractHash: config.sessionContractHash,
|
|
1565
|
-
suite: config.suite,
|
|
1566
|
-
maxInboundStreams: config.maxInboundStreams,
|
|
1567
|
-
admissionBinding: config.peerAdmissionBinding,
|
|
1568
|
-
expectedEndpointInstanceID: config.expectedPeerEndpointInstanceID,
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
async function readHandshakeFrame(reader, signal) {
|
|
1572
|
-
const header = await reader.readExactly(12, signal);
|
|
1573
|
-
const length = new DataView(header.buffer, header.byteOffset, header.byteLength).getUint32(8, false);
|
|
1574
|
-
if (length < 1 || length > 8_192)
|
|
1575
|
-
throw protocolError("invalid handshake payload length");
|
|
1576
|
-
const raw = concat(header, await reader.readExactly(length, signal));
|
|
1577
|
-
decodeHandshakeFrameV2(raw);
|
|
1578
|
-
return raw;
|
|
1579
|
-
}
|
|
1580
|
-
class ExactReader {
|
|
1581
|
-
stream;
|
|
1582
|
-
chunks = [];
|
|
1583
|
-
offset = 0;
|
|
1584
|
-
bytes = 0;
|
|
1585
|
-
constructor(stream) {
|
|
1586
|
-
this.stream = stream;
|
|
1587
|
-
}
|
|
1588
|
-
async readExactly(length, signal) {
|
|
1589
|
-
while (this.bytes < length) {
|
|
1590
|
-
const chunk = await this.stream.read(signal === undefined ? {} : { signal });
|
|
1591
|
-
if (chunk === null)
|
|
1592
|
-
throw new SessionV2Error("closed", "unexpected carrier EOF");
|
|
1593
|
-
if (chunk.length === 0)
|
|
1594
|
-
continue;
|
|
1595
|
-
this.chunks.push(chunk);
|
|
1596
|
-
this.bytes += chunk.length;
|
|
1597
|
-
}
|
|
1598
|
-
const out = new Uint8Array(length);
|
|
1599
|
-
let written = 0;
|
|
1600
|
-
while (written < length) {
|
|
1601
|
-
const chunk = this.chunks[0];
|
|
1602
|
-
const take = Math.min(length - written, chunk.length - this.offset);
|
|
1603
|
-
out.set(chunk.subarray(this.offset, this.offset + take), written);
|
|
1604
|
-
written += take;
|
|
1605
|
-
this.offset += take;
|
|
1606
|
-
this.bytes -= take;
|
|
1607
|
-
if (this.offset === chunk.length) {
|
|
1608
|
-
this.chunks.shift();
|
|
1609
|
-
this.offset = 0;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return out;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
class ByteQueue {
|
|
1616
|
-
limit;
|
|
1617
|
-
chunks = [];
|
|
1618
|
-
head = 0;
|
|
1619
|
-
bytes = 0;
|
|
1620
|
-
closed = false;
|
|
1621
|
-
error;
|
|
1622
|
-
waiters = new Set();
|
|
1623
|
-
capacityWaiters = new Set();
|
|
1624
|
-
constructor(limit) {
|
|
1625
|
-
this.limit = limit;
|
|
1626
|
-
}
|
|
1627
|
-
push(chunk) {
|
|
1628
|
-
if (this.closed || this.error !== undefined)
|
|
1629
|
-
throw protocolError("DATA after terminal stream state");
|
|
1630
|
-
if (this.bytes + chunk.length > this.limit)
|
|
1631
|
-
throw new SessionV2Error("resource_exhausted", "stream receive buffer exceeded");
|
|
1632
|
-
this.chunks.push(chunk.slice());
|
|
1633
|
-
this.bytes += chunk.length;
|
|
1634
|
-
this.wake();
|
|
1635
|
-
}
|
|
1636
|
-
close() {
|
|
1637
|
-
this.closed = true;
|
|
1638
|
-
this.wake();
|
|
1639
|
-
}
|
|
1640
|
-
fail(error) {
|
|
1641
|
-
if (this.error !== undefined)
|
|
1642
|
-
return;
|
|
1643
|
-
this.error = error;
|
|
1644
|
-
this.chunks.length = 0;
|
|
1645
|
-
this.head = 0;
|
|
1646
|
-
this.bytes = 0;
|
|
1647
|
-
this.wake(error);
|
|
1648
|
-
this.wakeCapacity(error);
|
|
1649
|
-
}
|
|
1650
|
-
async waitForCapacity(required) {
|
|
1651
|
-
if (!Number.isInteger(required) || required < 0 || required > this.limit) {
|
|
1652
|
-
throw new RangeError("invalid stream receive capacity request");
|
|
1653
|
-
}
|
|
1654
|
-
while (this.bytes + required > this.limit) {
|
|
1655
|
-
if (this.error !== undefined)
|
|
1656
|
-
throw this.error;
|
|
1657
|
-
if (this.closed)
|
|
1658
|
-
return;
|
|
1659
|
-
const waiter = deferred();
|
|
1660
|
-
this.capacityWaiters.add(waiter);
|
|
1661
|
-
try {
|
|
1662
|
-
await waiter.promise;
|
|
1663
|
-
}
|
|
1664
|
-
finally {
|
|
1665
|
-
this.capacityWaiters.delete(waiter);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
async read(signal) {
|
|
1670
|
-
while (true) {
|
|
1671
|
-
if (this.error !== undefined)
|
|
1672
|
-
throw this.error;
|
|
1673
|
-
if (this.head < this.chunks.length) {
|
|
1674
|
-
const chunk = this.chunks[this.head++];
|
|
1675
|
-
this.bytes -= chunk.length;
|
|
1676
|
-
this.wakeCapacity();
|
|
1677
|
-
if (this.head > 1_024 && this.head * 2 > this.chunks.length) {
|
|
1678
|
-
this.chunks.splice(0, this.head);
|
|
1679
|
-
this.head = 0;
|
|
1680
|
-
}
|
|
1681
|
-
return chunk;
|
|
1682
|
-
}
|
|
1683
|
-
if (this.closed)
|
|
1684
|
-
return null;
|
|
1685
|
-
const waiter = deferred();
|
|
1686
|
-
this.waiters.add(waiter);
|
|
1687
|
-
try {
|
|
1688
|
-
await raceAbort(waiter.promise, signal);
|
|
1689
|
-
}
|
|
1690
|
-
finally {
|
|
1691
|
-
this.waiters.delete(waiter);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
wake(error) {
|
|
1696
|
-
for (const waiter of [...this.waiters])
|
|
1697
|
-
error === undefined ? waiter.resolve() : waiter.reject(error);
|
|
1698
|
-
this.waiters.clear();
|
|
1699
|
-
}
|
|
1700
|
-
wakeCapacity(error) {
|
|
1701
|
-
for (const waiter of [...this.capacityWaiters])
|
|
1702
|
-
error === undefined ? waiter.resolve() : waiter.reject(error);
|
|
1703
|
-
this.capacityWaiters.clear();
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
class AsyncSemaphore {
|
|
1707
|
-
available;
|
|
1708
|
-
waiters = new Set();
|
|
1709
|
-
error;
|
|
1710
|
-
constructor(count) { this.available = count; }
|
|
1711
|
-
async acquire(signal) {
|
|
1712
|
-
if (this.error !== undefined)
|
|
1713
|
-
throw this.error;
|
|
1714
|
-
if (this.available > 0) {
|
|
1715
|
-
this.available--;
|
|
1716
|
-
return this.releaser();
|
|
1717
|
-
}
|
|
1718
|
-
return await new Promise((resolve, reject) => {
|
|
1719
|
-
let settled = false;
|
|
1720
|
-
const cleanup = () => {
|
|
1721
|
-
this.waiters.delete(waiter);
|
|
1722
|
-
signal?.removeEventListener("abort", abort);
|
|
1723
|
-
};
|
|
1724
|
-
const waiter = {
|
|
1725
|
-
deliver: (release) => {
|
|
1726
|
-
if (settled) {
|
|
1727
|
-
release();
|
|
1728
|
-
return false;
|
|
1729
|
-
}
|
|
1730
|
-
settled = true;
|
|
1731
|
-
cleanup();
|
|
1732
|
-
resolve(release);
|
|
1733
|
-
return true;
|
|
1734
|
-
},
|
|
1735
|
-
fail: (error) => {
|
|
1736
|
-
if (settled)
|
|
1737
|
-
return;
|
|
1738
|
-
settled = true;
|
|
1739
|
-
cleanup();
|
|
1740
|
-
reject(error);
|
|
1741
|
-
},
|
|
1742
|
-
};
|
|
1743
|
-
const abort = () => waiter.fail(abortReason(signal));
|
|
1744
|
-
this.waiters.add(waiter);
|
|
1745
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
1746
|
-
if (signal?.aborted === true)
|
|
1747
|
-
abort();
|
|
1748
|
-
});
|
|
1749
|
-
}
|
|
1750
|
-
tryAcquire() {
|
|
1751
|
-
if (this.error !== undefined || this.available === 0)
|
|
1752
|
-
return undefined;
|
|
1753
|
-
this.available--;
|
|
1754
|
-
return this.releaser();
|
|
1755
|
-
}
|
|
1756
|
-
fail(error) {
|
|
1757
|
-
this.error = error;
|
|
1758
|
-
for (const waiter of [...this.waiters])
|
|
1759
|
-
waiter.fail(error);
|
|
1760
|
-
}
|
|
1761
|
-
releaser() {
|
|
1762
|
-
let released = false;
|
|
1763
|
-
return () => {
|
|
1764
|
-
if (released)
|
|
1765
|
-
return;
|
|
1766
|
-
released = true;
|
|
1767
|
-
const waiter = this.waiters.values().next().value;
|
|
1768
|
-
if (waiter === undefined)
|
|
1769
|
-
this.available++;
|
|
1770
|
-
else
|
|
1771
|
-
waiter.deliver(this.releaser());
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
class AsyncQueue {
|
|
1776
|
-
values = [];
|
|
1777
|
-
waiters = new Set();
|
|
1778
|
-
error;
|
|
1779
|
-
push(value) {
|
|
1780
|
-
if (this.error !== undefined)
|
|
1781
|
-
return;
|
|
1782
|
-
const waiter = this.waiters.values().next().value;
|
|
1783
|
-
if (waiter === undefined || !waiter.deliver(value))
|
|
1784
|
-
this.values.push(value);
|
|
1785
|
-
}
|
|
1786
|
-
async shift(signal) {
|
|
1787
|
-
if (this.error !== undefined)
|
|
1788
|
-
throw this.error;
|
|
1789
|
-
const value = this.values.shift();
|
|
1790
|
-
if (value !== undefined)
|
|
1791
|
-
return value;
|
|
1792
|
-
return await new Promise((resolve, reject) => {
|
|
1793
|
-
let settled = false;
|
|
1794
|
-
const cleanup = () => {
|
|
1795
|
-
this.waiters.delete(waiter);
|
|
1796
|
-
signal?.removeEventListener("abort", abort);
|
|
1797
|
-
};
|
|
1798
|
-
const waiter = {
|
|
1799
|
-
deliver: (next) => {
|
|
1800
|
-
if (settled)
|
|
1801
|
-
return false;
|
|
1802
|
-
settled = true;
|
|
1803
|
-
cleanup();
|
|
1804
|
-
resolve(next);
|
|
1805
|
-
return true;
|
|
1806
|
-
},
|
|
1807
|
-
fail: (error) => {
|
|
1808
|
-
if (settled)
|
|
1809
|
-
return;
|
|
1810
|
-
settled = true;
|
|
1811
|
-
cleanup();
|
|
1812
|
-
reject(error);
|
|
1813
|
-
},
|
|
1814
|
-
};
|
|
1815
|
-
const abort = () => waiter.fail(abortReason(signal));
|
|
1816
|
-
this.waiters.add(waiter);
|
|
1817
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
1818
|
-
if (signal?.aborted === true)
|
|
1819
|
-
abort();
|
|
1820
|
-
});
|
|
1821
|
-
}
|
|
1822
|
-
fail(error) {
|
|
1823
|
-
this.error = error;
|
|
1824
|
-
this.values.length = 0;
|
|
1825
|
-
for (const waiter of [...this.waiters])
|
|
1826
|
-
waiter.fail(error);
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
function deferred() {
|
|
1830
|
-
let resolvePromise;
|
|
1831
|
-
let rejectPromise;
|
|
1832
|
-
let settled = false;
|
|
1833
|
-
const promise = new Promise((resolve, reject) => {
|
|
1834
|
-
resolvePromise = resolve;
|
|
1835
|
-
rejectPromise = reject;
|
|
1836
|
-
});
|
|
1837
|
-
return {
|
|
1838
|
-
promise,
|
|
1839
|
-
resolve: (value) => { if (!settled) {
|
|
1840
|
-
settled = true;
|
|
1841
|
-
resolvePromise(value);
|
|
1842
|
-
} },
|
|
1843
|
-
reject: (reason) => { if (!settled) {
|
|
1844
|
-
settled = true;
|
|
1845
|
-
rejectPromise(reason);
|
|
1846
|
-
} },
|
|
1847
|
-
get settled() { return settled; },
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
|
-
function validateConfig(carrier, config) {
|
|
1851
|
-
if (config.runtime === undefined ||
|
|
1852
|
-
typeof config.runtime.entropy !== "function" ||
|
|
1853
|
-
typeof config.runtime.monotonicMilliseconds !== "function") {
|
|
1854
|
-
throw new SessionV2Error("handshake", "session protocol runtime is required");
|
|
1855
|
-
}
|
|
1856
|
-
if (carrier.path !== config.path || (config.role !== "client" && config.role !== "server")) {
|
|
1857
|
-
throw new SessionV2Error("handshake", "invalid session carrier/config binding");
|
|
1858
|
-
}
|
|
1859
|
-
if (!Number.isInteger(config.maxInboundStreams) || config.maxInboundStreams < 1 || config.maxInboundStreams > 128) {
|
|
1860
|
-
throw new SessionV2Error("handshake", "invalid maxInboundStreams");
|
|
1861
|
-
}
|
|
1862
|
-
if (carrier.inboundBidirectionalStreamCapacity !== config.maxInboundStreams + 2) {
|
|
1863
|
-
throw new SessionV2Error("handshake", "carrier inbound bidirectional stream capacity mismatch");
|
|
1864
|
-
}
|
|
1865
|
-
sessionIdleTimeoutMs(config);
|
|
1866
|
-
sessionCloseTimeoutMs(config);
|
|
1867
|
-
for (const [name, value] of [
|
|
1868
|
-
["session contract hash", config.sessionContractHash],
|
|
1869
|
-
["PSK", config.psk],
|
|
1870
|
-
["local admission binding", config.localAdmissionBinding],
|
|
1871
|
-
["peer admission binding", config.peerAdmissionBinding],
|
|
1872
|
-
]) {
|
|
1873
|
-
if (value.length !== 32)
|
|
1874
|
-
throw new SessionV2Error("handshake", `${name} must be 32 bytes`);
|
|
1875
|
-
}
|
|
1876
|
-
if (config.path === "direct") {
|
|
1877
|
-
if (config.localEndpointInstanceID !== "" || config.expectedPeerEndpointInstanceID !== "") {
|
|
1878
|
-
throw new SessionV2Error("handshake", "direct session endpoint IDs must be empty");
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
else if (config.localEndpointInstanceID === "" || config.expectedPeerEndpointInstanceID === "") {
|
|
1882
|
-
throw new SessionV2Error("handshake", "tunnel session endpoint IDs are required");
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
function supportedFeatures(datagrams) {
|
|
1886
|
-
return datagrams !== undefined && datagrams.maxDatagramSize >= UNRELIABLE_MESSAGE_WIRE_BYTES_V2
|
|
1887
|
-
? UNRELIABLE_MESSAGES_FEATURE_V2
|
|
1888
|
-
: 0;
|
|
1889
|
-
}
|
|
1890
|
-
function requireUnreliableDatagrams(carrier) {
|
|
1891
|
-
const datagrams = carrier.unreliableDatagrams;
|
|
1892
|
-
if (datagrams === undefined || datagrams.maxDatagramSize < UNRELIABLE_MESSAGE_WIRE_BYTES_V2) {
|
|
1893
|
-
throw new SessionV2Error("handshake", "negotiated unreliable messages require native DATAGRAM support");
|
|
1894
|
-
}
|
|
1895
|
-
return datagrams;
|
|
1896
|
-
}
|
|
1897
|
-
function encodeMetadata(value) {
|
|
1898
|
-
return encoder.encode(canonicalJSON(value));
|
|
1899
|
-
}
|
|
1900
|
-
function decodeMetadata(raw) {
|
|
1901
|
-
return JSON.parse(decoder.decode(raw));
|
|
1902
|
-
}
|
|
1903
|
-
function canonicalJSON(value) {
|
|
1904
|
-
if (value === null || typeof value === "boolean" || typeof value === "number")
|
|
1905
|
-
return JSON.stringify(value);
|
|
1906
|
-
if (typeof value === "string")
|
|
1907
|
-
return JSON.stringify(value);
|
|
1908
|
-
if (Array.isArray(value))
|
|
1909
|
-
return `[${value.map(canonicalJSON).join(",")}]`;
|
|
1910
|
-
if (typeof value !== "object" || value === null)
|
|
1911
|
-
throw new TypeError("metadata must be JSON");
|
|
1912
|
-
const object = value;
|
|
1913
|
-
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJSON(object[key])}`).join(",")}}`;
|
|
1914
|
-
}
|
|
1915
|
-
async function writeAll(stream, value, signal) {
|
|
1916
|
-
let offset = 0;
|
|
1917
|
-
while (offset < value.length) {
|
|
1918
|
-
throwIfAborted(signal);
|
|
1919
|
-
const written = await stream.write(value.subarray(offset), signal === undefined ? {} : { signal });
|
|
1920
|
-
if (written < 1 || written > value.length - offset)
|
|
1921
|
-
throw protocolError("short carrier write");
|
|
1922
|
-
offset += written;
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
async function settleWithin(promise, timeoutMs) {
|
|
1926
|
-
return await new Promise((resolve) => {
|
|
1927
|
-
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
1928
|
-
void promise.then(() => { clearTimeout(timer); resolve(true); }, () => { clearTimeout(timer); resolve(true); });
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
async function resolveWithin(promise, timeoutMs) {
|
|
1932
|
-
return await new Promise((resolve) => {
|
|
1933
|
-
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
1934
|
-
void promise.then(() => { clearTimeout(timer); resolve(true); }, () => { clearTimeout(timer); resolve(false); });
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
function sessionIdleTimeoutMs(config) {
|
|
1938
|
-
const timeoutMs = config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
1939
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
|
|
1940
|
-
throw new RangeError("idleTimeoutMs must be a non-negative integer");
|
|
1941
|
-
}
|
|
1942
|
-
return timeoutMs;
|
|
1943
|
-
}
|
|
1944
|
-
function sessionCloseTimeoutMs(config) {
|
|
1945
|
-
const timeoutMs = config.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
1946
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
|
|
1947
|
-
throw new RangeError("closeTimeoutMs must be an integer from 1 to 60000");
|
|
1948
|
-
}
|
|
1949
|
-
return timeoutMs;
|
|
1950
|
-
}
|
|
1951
|
-
function sessionRandomBytes(config, length) {
|
|
1952
|
-
const output = config.runtime.entropy(length);
|
|
1953
|
-
if (!(output instanceof Uint8Array) || output.length !== length) {
|
|
1954
|
-
throw new SessionV2Error("handshake", `session entropy must return exactly ${length} bytes`);
|
|
1955
|
-
}
|
|
1956
|
-
return output.slice();
|
|
1957
|
-
}
|
|
1958
|
-
function idReason(id, reason) {
|
|
1959
|
-
return concat(u64(id), Uint8Array.of((reason >>> 8) & 0xff, reason & 0xff));
|
|
1960
|
-
}
|
|
1961
|
-
function parseIDReason(payload) {
|
|
1962
|
-
if (payload.length !== 10)
|
|
1963
|
-
throw protocolError("invalid ID/reason payload");
|
|
1964
|
-
return { id: readU64(payload), reason: readU16(payload, 8) };
|
|
1965
|
-
}
|
|
1966
|
-
function u64(value) {
|
|
1967
|
-
const out = new Uint8Array(8);
|
|
1968
|
-
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
1969
|
-
return out;
|
|
1970
|
-
}
|
|
1971
|
-
function u32(value) {
|
|
1972
|
-
const out = new Uint8Array(4);
|
|
1973
|
-
new DataView(out.buffer).setUint32(0, value, false);
|
|
1974
|
-
return out;
|
|
1975
|
-
}
|
|
1976
|
-
function readU64(value) {
|
|
1977
|
-
if (value.length < 8)
|
|
1978
|
-
throw protocolError("truncated uint64");
|
|
1979
|
-
return new DataView(value.buffer, value.byteOffset, value.byteLength).getBigUint64(0, false);
|
|
1980
|
-
}
|
|
1981
|
-
function readU64At(value, offset) {
|
|
1982
|
-
if (value.length < offset + 8)
|
|
1983
|
-
throw protocolError("truncated uint64");
|
|
1984
|
-
return new DataView(value.buffer, value.byteOffset, value.byteLength).getBigUint64(offset, false);
|
|
1985
|
-
}
|
|
1986
|
-
function readU32(value, offset) {
|
|
1987
|
-
if (value.length < offset + 4)
|
|
1988
|
-
throw protocolError("truncated uint32");
|
|
1989
|
-
return new DataView(value.buffer, value.byteOffset, value.byteLength).getUint32(offset, false);
|
|
1990
|
-
}
|
|
1991
|
-
function readU16(value, offset) {
|
|
1992
|
-
if (value.length < offset + 2)
|
|
1993
|
-
throw protocolError("truncated uint16");
|
|
1994
|
-
return new DataView(value.buffer, value.byteOffset, value.byteLength).getUint16(offset, false);
|
|
1995
|
-
}
|
|
1996
|
-
function concat(...values) {
|
|
1997
|
-
const out = new Uint8Array(values.reduce((sum, value) => sum + value.length, 0));
|
|
1998
|
-
let offset = 0;
|
|
1999
|
-
for (const value of values) {
|
|
2000
|
-
out.set(value, offset);
|
|
2001
|
-
offset += value.length;
|
|
2002
|
-
}
|
|
2003
|
-
return out;
|
|
2004
|
-
}
|
|
2005
|
-
function bytesEqual(left, right) {
|
|
2006
|
-
if (left.length !== right.length)
|
|
2007
|
-
return false;
|
|
2008
|
-
let difference = 0;
|
|
2009
|
-
for (let index = 0; index < left.length; index++)
|
|
2010
|
-
difference |= left[index] ^ right[index];
|
|
2011
|
-
return difference === 0;
|
|
2012
|
-
}
|
|
2013
|
-
function wipeEpochRoots(roots) {
|
|
2014
|
-
roots.epochSecret.fill(0);
|
|
2015
|
-
roots.controlRoot.fill(0);
|
|
2016
|
-
roots.streamRoot.fill(0);
|
|
2017
|
-
roots.setupRoot.fill(0);
|
|
2018
|
-
roots.rekeyRoot.fill(0);
|
|
2019
|
-
}
|
|
2020
|
-
async function raceAbort(promise, signal) {
|
|
2021
|
-
if (signal === undefined)
|
|
2022
|
-
return await promise;
|
|
2023
|
-
throwIfAborted(signal);
|
|
2024
|
-
return await new Promise((resolve, reject) => {
|
|
2025
|
-
const abort = () => reject(abortReason(signal));
|
|
2026
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
2027
|
-
void promise.then((value) => { signal.removeEventListener("abort", abort); resolve(value); }, (error) => { signal.removeEventListener("abort", abort); reject(error); });
|
|
2028
|
-
});
|
|
2029
|
-
}
|
|
2030
|
-
function throwIfAborted(signal) {
|
|
2031
|
-
if (signal?.aborted === true)
|
|
2032
|
-
throw abortReason(signal);
|
|
2033
|
-
}
|
|
2034
|
-
function abortedError() {
|
|
2035
|
-
return new SessionV2Error("aborted", "operation aborted");
|
|
2036
|
-
}
|
|
2037
|
-
function abortReason(signal) {
|
|
2038
|
-
return signal.reason instanceof Error ? signal.reason : abortedError();
|
|
2039
|
-
}
|
|
2040
|
-
function releasedStreamCleanup(streams) {
|
|
2041
|
-
return new FinalizationRegistry((id) => {
|
|
2042
|
-
const stream = streams.get(id);
|
|
2043
|
-
if (stream !== undefined && stream.deref() === undefined)
|
|
2044
|
-
streams.delete(id);
|
|
2045
|
-
});
|
|
2046
|
-
}
|
|
2047
|
-
function createSessionDeadline(config, phase) {
|
|
2048
|
-
const deadlines = config.deadlines;
|
|
2049
|
-
const timeoutMs = phase === "establish"
|
|
2050
|
-
? deadlines?.establishTimeoutMs ?? 30_000
|
|
2051
|
-
: phase === "rekey_prepare"
|
|
2052
|
-
? deadlines?.rekeyPrepareTimeoutMs ?? 10_000
|
|
2053
|
-
: deadlines?.rekeyCompletionTimeoutMs ?? 30_000;
|
|
2054
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
2055
|
-
throw new RangeError(`${phase} timeout must be positive`);
|
|
2056
|
-
return (deadlines?.factory ?? defaultDeadlineFactory)(timeoutMs, phase);
|
|
2057
|
-
}
|
|
2058
|
-
function defaultDeadlineFactory(timeoutMs, phase) {
|
|
2059
|
-
const controller = new AbortController();
|
|
2060
|
-
const cancelTimer = scheduleLongTimeout(() => {
|
|
2061
|
-
controller.abort(new SessionV2Error("timeout", `${phase} deadline exceeded`));
|
|
2062
|
-
}, timeoutMs);
|
|
2063
|
-
return {
|
|
2064
|
-
signal: controller.signal,
|
|
2065
|
-
cancel: cancelTimer,
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
const MAX_NATIVE_TIMEOUT_MS = 2_147_483_647;
|
|
2069
|
-
// Node and browsers clamp setTimeout delays above a signed 32-bit integer.
|
|
2070
|
-
// Keep the public timeout range intact by scheduling the remaining duration in
|
|
2071
|
-
// bounded segments against a monotonic wall-clock deadline.
|
|
2072
|
-
function scheduleLongTimeout(callback, timeoutMs) {
|
|
2073
|
-
const clock = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
2074
|
-
const deadline = clock() + timeoutMs;
|
|
2075
|
-
let timer;
|
|
2076
|
-
let canceled = false;
|
|
2077
|
-
const schedule = () => {
|
|
2078
|
-
if (canceled)
|
|
2079
|
-
return;
|
|
2080
|
-
const remaining = deadline - clock();
|
|
2081
|
-
if (remaining <= 0) {
|
|
2082
|
-
callback();
|
|
2083
|
-
return;
|
|
2084
|
-
}
|
|
2085
|
-
timer = setTimeout(schedule, Math.min(remaining, MAX_NATIVE_TIMEOUT_MS));
|
|
2086
|
-
};
|
|
2087
|
-
schedule();
|
|
2088
|
-
return () => {
|
|
2089
|
-
canceled = true;
|
|
2090
|
-
if (timer !== undefined)
|
|
2091
|
-
clearTimeout(timer);
|
|
2092
|
-
};
|
|
2093
|
-
}
|
|
2094
|
-
function combineSignals(...signals) {
|
|
2095
|
-
const controller = new AbortController();
|
|
2096
|
-
const cleanups = [];
|
|
2097
|
-
const abortFrom = (signal) => {
|
|
2098
|
-
if (!controller.signal.aborted)
|
|
2099
|
-
controller.abort(abortReason(signal));
|
|
2100
|
-
};
|
|
2101
|
-
for (const signal of signals) {
|
|
2102
|
-
if (signal === undefined)
|
|
2103
|
-
continue;
|
|
2104
|
-
const abort = () => abortFrom(signal);
|
|
2105
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
2106
|
-
cleanups.push(() => signal.removeEventListener("abort", abort));
|
|
2107
|
-
if (signal.aborted)
|
|
2108
|
-
abortFrom(signal);
|
|
2109
|
-
}
|
|
2110
|
-
return {
|
|
2111
|
-
signal: controller.signal,
|
|
2112
|
-
cancel: () => { for (const cleanup of cleanups.splice(0))
|
|
2113
|
-
cleanup(); },
|
|
2114
|
-
};
|
|
2115
|
-
}
|
|
2116
|
-
function signalOptions(signal) {
|
|
2117
|
-
return signal === undefined ? {} : { signal };
|
|
2118
|
-
}
|
|
2119
|
-
function protocolError(message) {
|
|
2120
|
-
return new SessionV2Error("protocol", message);
|
|
2121
|
-
}
|
|
2122
|
-
function asError(error) {
|
|
2123
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
2124
|
-
}
|
|
2125
|
-
function errorMessage(error) {
|
|
2126
|
-
return error instanceof Error ? error.message : String(error);
|
|
2127
|
-
}
|