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