@floegence/flowersec-core 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -62
- package/dist/client-connect/connectCore.d.ts +3 -3
- package/dist/client-connect/connectCore.js +7 -5
- package/dist/client-connect/transportSecurity.js +1 -1
- package/dist/controlplane/issuer.d.ts +5 -2
- package/dist/controlplane/issuer.js +76 -16
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +2 -0
- package/dist/e2ee/handshake.d.ts +2 -2
- package/dist/e2ee/secureChannel.d.ts +2 -2
- package/dist/e2ee/secureChannel.js +22 -10
- package/dist/endpoint/index.js +203 -38
- package/dist/proxy/appWindow.js +93 -7
- package/dist/proxy/controllerWindow.js +219 -78
- package/dist/proxy/portStream.d.ts +2 -1
- package/dist/proxy/portStream.js +173 -74
- package/dist/proxy/server.d.ts +2 -0
- package/dist/proxy/server.js +346 -105
- package/dist/proxy/windowBridgeProtocol.d.ts +4 -3
- package/dist/proxy/windowBridgeProtocol.js +1 -1
- package/dist/reconnect/index.js +62 -25
- package/dist/yamux/byteReader.d.ts +2 -0
- package/dist/yamux/byteReader.js +28 -24
- package/dist/yamux/session.js +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @floegence/flowersec-core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Status: experimental; not audited.
|
|
3
|
+
The TypeScript SDK for Flowersec end-to-end encrypted direct and tunneled sessions. It implements the portable Node.js client and endpoint contract plus the browser and Service Worker runtime owned by TypeScript.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
@@ -10,73 +8,33 @@ Status: experimental; not audited.
|
|
|
10
8
|
npm install @floegence/flowersec-core
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Browser:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { connectBrowser } from "@floegence/flowersec-core/browser";
|
|
19
|
-
import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
|
|
11
|
+
The package is ESM-only and exposes environment-specific entrypoints for browser and Node.js applications.
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
endpointId: "env_demo",
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const client = await connectBrowser(artifact);
|
|
26
|
-
await client.ping();
|
|
27
|
-
client.close();
|
|
28
|
-
```
|
|
13
|
+
## Cookbook
|
|
29
14
|
|
|
30
|
-
Node.js
|
|
15
|
+
Start with the [TypeScript cookbook](https://github.com/floegence/flowersec/tree/main/examples/ts). It contains runnable browser direct/tunnel pages, Node.js clients, the shared demo server, the Service Worker proxy runtime, and manual protocol-stack references.
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
import { connectNode, createNodeReconnectConfig } from "@floegence/flowersec-core/node";
|
|
34
|
-
import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
|
|
35
|
-
import { createControlplaneArtifactSource } from "@floegence/flowersec-core/reconnect";
|
|
36
|
-
|
|
37
|
-
const artifact = await requestConnectArtifact({
|
|
38
|
-
baseUrl: "https://your-app.example/api/flowersec",
|
|
39
|
-
endpointId: "env_demo",
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const client = await connectNode(artifact, {
|
|
43
|
-
origin: "https://your-app.example",
|
|
44
|
-
});
|
|
45
|
-
await client.ping();
|
|
46
|
-
const rttMs = await client.probeLiveness();
|
|
47
|
-
client.close();
|
|
48
|
-
|
|
49
|
-
const reconnectConfig = createNodeReconnectConfig({
|
|
50
|
-
source: createControlplaneArtifactSource({
|
|
51
|
-
baseUrl: "https://your-app.example/api/flowersec",
|
|
52
|
-
endpointId: "env_demo",
|
|
53
|
-
}),
|
|
54
|
-
connect: {
|
|
55
|
-
origin: "https://your-app.example",
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
```
|
|
17
|
+
## Entrypoints
|
|
59
18
|
|
|
60
|
-
Browser
|
|
19
|
+
- Browser client: `@floegence/flowersec-core/browser`
|
|
20
|
+
- Node.js client and endpoint transport: `@floegence/flowersec-core/node`
|
|
21
|
+
- Controlplane artifact helpers: `@floegence/flowersec-core/controlplane`
|
|
22
|
+
- Endpoint session serving: `@floegence/flowersec-core/endpoint`
|
|
23
|
+
- RPC: `@floegence/flowersec-core/rpc`
|
|
24
|
+
- Reconnect: `@floegence/flowersec-core/reconnect`
|
|
25
|
+
- HTTP/WebSocket proxy and browser runtime: `@floegence/flowersec-core/proxy`
|
|
26
|
+
- Observability: `@floegence/flowersec-core/observability`
|
|
61
27
|
|
|
62
|
-
High-level
|
|
63
|
-
targets without DNS resolution. Deliberate non-loopback `ws://` connections must use
|
|
64
|
-
`createNetworkPlaintextPolicy(...)` with exact canonical IP literals and
|
|
65
|
-
`PlaintextRiskAcceptance.acceptPreE2ECredentialExposure`. The unrestricted `AllowPlaintext` preset is deprecated.
|
|
28
|
+
High-level WebSocket connections require TLS by default. Use `AllowPlaintextForLoopback` only for literal local development targets.
|
|
66
29
|
|
|
67
|
-
## Node
|
|
30
|
+
## Node Proxy Server
|
|
68
31
|
|
|
69
|
-
|
|
32
|
+
The Node entrypoint exports `serveProxySession(...)`, `serveProxyStream(...)`, and `ProxyServerOptions`. Request bodies remain bounded per request by `maxBodyBytes`; `maxBufferedRequestBodyBytes` additionally caps the total buffered request-body bytes owned by one proxy session. WebSocket frames remain bounded by `maxWsFrameBytes`; `maxWsQueuedBytes` additionally caps upstream-to-Yamux queued bytes per connection, while the reverse direction waits for each Node `ws` send callback.
|
|
70
33
|
|
|
71
|
-
|
|
34
|
+
The defaults keep one session request-body budget equal to `maxBodyBytes` and one queued WebSocket frame plus its proxy header. Raise either limit only when the deployment has a matching memory budget.
|
|
72
35
|
|
|
73
|
-
|
|
36
|
+
## Runtime Boundaries
|
|
74
37
|
|
|
75
|
-
|
|
38
|
+
TypeScript owns browser and Service Worker integration. Shared tunnel, proxy gateway, and helper binaries remain Go-owned.
|
|
76
39
|
|
|
77
|
-
|
|
78
|
-
- Integration guide: `docs/INTEGRATION_GUIDE.md`
|
|
79
|
-
- API surface contract: `docs/API_SURFACE.md`
|
|
80
|
-
- Controlplane artifact fetch: `docs/CONTROLPLANE_ARTIFACT_FETCH.md`
|
|
81
|
-
- Error model: `docs/ERROR_MODEL.md`
|
|
82
|
-
- Migration guide: `docs/V0_20_MIGRATION.md`
|
|
40
|
+
Review the shared [API contract](../docs/API_CONTRACT.md), [protocol](../docs/PROTOCOL.md), [threat model](../docs/THREAT_MODEL.md), and [error model](../docs/ERROR_MODEL.md).
|
|
@@ -23,7 +23,7 @@ export type ConnectOptionsBase = Readonly<{
|
|
|
23
23
|
maxHandshakePayload?: number;
|
|
24
24
|
/** Maximum encrypted record size on the wire (0 uses default). */
|
|
25
25
|
maxRecordBytes?: number;
|
|
26
|
-
/** Maximum
|
|
26
|
+
/** Maximum queued inbound plaintext bytes in the secure channel (0 uses default). */
|
|
27
27
|
maxBufferedBytes?: number;
|
|
28
28
|
/** Maximum queued outbound plaintext bytes in the secure channel (default 4 MiB; 0 uses default). */
|
|
29
29
|
maxOutboundBufferedBytes?: number;
|
|
@@ -39,9 +39,9 @@ export type ConnectOptionsBase = Readonly<{
|
|
|
39
39
|
observer?: ClientObserverLike;
|
|
40
40
|
/** Policy evaluated before any WebSocket network activity. */
|
|
41
41
|
transportSecurityPolicy?: TransportSecurityPolicy;
|
|
42
|
-
/**
|
|
42
|
+
/** Scope validators keyed by scope name. */
|
|
43
43
|
scopeResolvers?: ConnectScopeResolverMap;
|
|
44
|
-
/**
|
|
44
|
+
/** Explicit compatibility switch for optional scope failures. */
|
|
45
45
|
relaxedOptionalScopeValidation?: boolean;
|
|
46
46
|
/** Acknowledged Yamux liveness checks, or false to disable automatic checks. */
|
|
47
47
|
liveness?: false | LivenessOptions;
|
|
@@ -303,10 +303,11 @@ export async function connectCore(args) {
|
|
|
303
303
|
return await mux.probeLiveness(liveness.timeoutMs);
|
|
304
304
|
}
|
|
305
305
|
catch (e) {
|
|
306
|
-
|
|
306
|
+
const timedOut = isYamuxPingTimeoutError(e);
|
|
307
|
+
if (timedOut) {
|
|
307
308
|
emitObserverDiagnostic(args.opts.observer, { path: args.path, stage: "yamux", code_domain: "event", code: "liveness_timeout", result: "fail" });
|
|
308
309
|
}
|
|
309
|
-
const error = new FlowersecError({ path: args.path, stage: "yamux", code: "ping_failed", message: "liveness probe failed", cause: e });
|
|
310
|
+
const error = new FlowersecError({ path: args.path, stage: "yamux", code: timedOut ? "timeout" : "ping_failed", message: "liveness probe failed", cause: e });
|
|
310
311
|
reportTermination(error);
|
|
311
312
|
throw error;
|
|
312
313
|
}
|
|
@@ -355,8 +356,9 @@ export async function connectCore(args) {
|
|
|
355
356
|
},
|
|
356
357
|
probeLiveness,
|
|
357
358
|
openStream: async (kind, opts = {}) => {
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
const streamKind = kind?.trim() ?? "";
|
|
360
|
+
if (streamKind === "")
|
|
361
|
+
throw new FlowersecError({ path: args.path, stage: "rpc", code: "missing_stream_kind", message: "missing stream kind" });
|
|
360
362
|
if (opts.signal?.aborted) {
|
|
361
363
|
throw new FlowersecError({
|
|
362
364
|
path: args.path,
|
|
@@ -415,7 +417,7 @@ export async function connectCore(args) {
|
|
|
415
417
|
});
|
|
416
418
|
}
|
|
417
419
|
try {
|
|
418
|
-
await writeStreamHello((b) => s.write(b),
|
|
420
|
+
await writeStreamHello((b) => s.write(b), streamKind);
|
|
419
421
|
}
|
|
420
422
|
catch (err) {
|
|
421
423
|
if (signal?.aborted) {
|
|
@@ -35,7 +35,7 @@ export async function enforceTransportSecurity(args) {
|
|
|
35
35
|
const policy = args.policy ?? RequireTLS;
|
|
36
36
|
try {
|
|
37
37
|
if (typeof policy === "function") {
|
|
38
|
-
allowed = await policy(input);
|
|
38
|
+
allowed = await policy(input) === true;
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
41
|
allowed = evaluatePreset(policy, target);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TokenPayload } from "./token.js";
|
|
2
2
|
export declare class IssuerKeyset {
|
|
3
|
-
private
|
|
4
|
-
private
|
|
3
|
+
private active;
|
|
4
|
+
private readonly verificationKeys;
|
|
5
5
|
constructor(kid: string, signingSeed: Uint8Array);
|
|
6
6
|
static random(kid: string): IssuerKeyset;
|
|
7
7
|
currentKID(): string;
|
|
@@ -9,7 +9,10 @@ export declare class IssuerKeyset {
|
|
|
9
9
|
sign(payload: Omit<TokenPayload, "kid"> & Readonly<{
|
|
10
10
|
kid?: string;
|
|
11
11
|
}>): string;
|
|
12
|
+
addVerificationKey(kid: string, publicKey: Uint8Array): void;
|
|
12
13
|
rotate(kid: string, signingSeed: Uint8Array): void;
|
|
14
|
+
retireVerificationKey(kid: string): void;
|
|
13
15
|
exportTunnelKeyset(): Uint8Array;
|
|
14
16
|
dispose(): void;
|
|
17
|
+
private requireActive;
|
|
15
18
|
}
|
|
@@ -2,13 +2,13 @@ import { ed25519 } from "@noble/curves/ed25519";
|
|
|
2
2
|
import { base64urlEncode } from "../utils/base64url.js";
|
|
3
3
|
import { signToken } from "./token.js";
|
|
4
4
|
export class IssuerKeyset {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
active;
|
|
6
|
+
verificationKeys = new Map();
|
|
7
7
|
constructor(kid, signingSeed) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.
|
|
8
|
+
const normalizedKID = normalizeKid(kid);
|
|
9
|
+
const retainedSeed = copySigningSeed(signingSeed);
|
|
10
|
+
this.active = { kid: normalizedKID, signingSeed: retainedSeed };
|
|
11
|
+
this.verificationKeys.set(normalizedKID, ed25519.getPublicKey(retainedSeed));
|
|
12
12
|
}
|
|
13
13
|
static random(kid) {
|
|
14
14
|
const seed = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -20,30 +20,90 @@ export class IssuerKeyset {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
currentKID() {
|
|
23
|
-
return this.kid;
|
|
23
|
+
return this.requireActive().kid;
|
|
24
24
|
}
|
|
25
25
|
publicKeys() {
|
|
26
|
-
|
|
26
|
+
this.requireActive();
|
|
27
|
+
return new Map([...this.verificationKeys.entries()]
|
|
28
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
|
|
29
|
+
.map(([kid, publicKey]) => [kid, publicKey.slice()]));
|
|
27
30
|
}
|
|
28
31
|
sign(payload) {
|
|
29
|
-
|
|
32
|
+
const active = this.requireActive();
|
|
33
|
+
return signToken(active.signingSeed, { ...payload, kid: active.kid });
|
|
34
|
+
}
|
|
35
|
+
addVerificationKey(kid, publicKey) {
|
|
36
|
+
this.requireActive();
|
|
37
|
+
const normalizedKID = normalizeKid(kid);
|
|
38
|
+
const retainedKey = copyPublicKey(publicKey);
|
|
39
|
+
const existing = this.verificationKeys.get(normalizedKID);
|
|
40
|
+
if (existing != null) {
|
|
41
|
+
if (!equalBytes(existing, retainedKey))
|
|
42
|
+
throw new Error(`key ID ${normalizedKID} is already bound to a different public key`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.verificationKeys.set(normalizedKID, retainedKey);
|
|
30
46
|
}
|
|
31
47
|
rotate(kid, signingSeed) {
|
|
48
|
+
const active = this.requireActive();
|
|
32
49
|
const nextKID = normalizeKid(kid);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
const nextSeed = copySigningSeed(signingSeed);
|
|
51
|
+
const nextPublicKey = ed25519.getPublicKey(nextSeed);
|
|
52
|
+
const prepublishedKey = this.verificationKeys.get(nextKID);
|
|
53
|
+
if (prepublishedKey == null) {
|
|
54
|
+
nextSeed.fill(0);
|
|
55
|
+
throw new Error(`key ID ${nextKID} must be prepublished before rotation`);
|
|
56
|
+
}
|
|
57
|
+
if (!equalBytes(prepublishedKey, nextPublicKey)) {
|
|
58
|
+
nextSeed.fill(0);
|
|
59
|
+
throw new Error(`key ID ${nextKID} is bound to a different public key`);
|
|
60
|
+
}
|
|
61
|
+
this.active = { kid: nextKID, signingSeed: nextSeed };
|
|
62
|
+
active.signingSeed.fill(0);
|
|
63
|
+
}
|
|
64
|
+
retireVerificationKey(kid) {
|
|
65
|
+
const active = this.requireActive();
|
|
66
|
+
const normalizedKID = normalizeKid(kid);
|
|
67
|
+
if (normalizedKID === active.kid)
|
|
68
|
+
throw new Error("the active signing key cannot be retired");
|
|
69
|
+
if (!this.verificationKeys.delete(normalizedKID))
|
|
70
|
+
throw new Error(`key ID ${normalizedKID} is not published`);
|
|
39
71
|
}
|
|
40
72
|
exportTunnelKeyset() {
|
|
41
73
|
const keys = [...this.publicKeys()].map(([kid, publicKey]) => ({ kid, pubkey_b64u: base64urlEncode(publicKey) }));
|
|
42
74
|
return new TextEncoder().encode(JSON.stringify({ keys }, null, 2));
|
|
43
75
|
}
|
|
44
76
|
dispose() {
|
|
45
|
-
this.
|
|
77
|
+
const active = this.active;
|
|
78
|
+
if (active == null)
|
|
79
|
+
return;
|
|
80
|
+
active.signingSeed.fill(0);
|
|
81
|
+
this.active = undefined;
|
|
82
|
+
this.verificationKeys.clear();
|
|
46
83
|
}
|
|
84
|
+
requireActive() {
|
|
85
|
+
if (this.active == null)
|
|
86
|
+
throw new Error("issuer keyset is disposed");
|
|
87
|
+
return this.active;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function copySigningSeed(signingSeed) {
|
|
91
|
+
if (signingSeed.length !== 32)
|
|
92
|
+
throw new TypeError("Ed25519 signing seed must be 32 bytes");
|
|
93
|
+
return signingSeed.slice();
|
|
94
|
+
}
|
|
95
|
+
function copyPublicKey(publicKey) {
|
|
96
|
+
if (publicKey.length !== 32)
|
|
97
|
+
throw new TypeError("Ed25519 public key must be 32 bytes");
|
|
98
|
+
return publicKey.slice();
|
|
99
|
+
}
|
|
100
|
+
function equalBytes(left, right) {
|
|
101
|
+
if (left.length !== right.length)
|
|
102
|
+
return false;
|
|
103
|
+
let diff = 0;
|
|
104
|
+
for (let index = 0; index < left.length; index++)
|
|
105
|
+
diff |= left[index] ^ right[index];
|
|
106
|
+
return diff === 0;
|
|
47
107
|
}
|
|
48
108
|
function normalizeKid(kid) {
|
|
49
109
|
const value = kid.trim();
|
package/dist/defaults.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const SDK_DEFAULTS: Readonly<{
|
|
|
8
8
|
maxHandshakePayloadBytes: number;
|
|
9
9
|
maxRecordBytes: number;
|
|
10
10
|
outboundRecordChunkBytes: number;
|
|
11
|
+
maxInboundBufferedBytes: number;
|
|
11
12
|
maxOutboundBufferedBytes: number;
|
|
12
13
|
}>;
|
|
13
14
|
yamux: Readonly<{
|
|
@@ -15,6 +16,7 @@ export declare const SDK_DEFAULTS: Readonly<{
|
|
|
15
16
|
maxInboundStreams: 32;
|
|
16
17
|
maxFrameBytes: number;
|
|
17
18
|
preferredOutboundFrameBytes: number;
|
|
19
|
+
maxStreamWriteQueueBytes: number;
|
|
18
20
|
maxStreamReceiveBytes: number;
|
|
19
21
|
maxSessionReceiveBytes: number;
|
|
20
22
|
}>;
|
package/dist/defaults.js
CHANGED
|
@@ -8,6 +8,7 @@ export const SDK_DEFAULTS = Object.freeze({
|
|
|
8
8
|
maxHandshakePayloadBytes: 8 * 1024,
|
|
9
9
|
maxRecordBytes: 1024 * 1024,
|
|
10
10
|
outboundRecordChunkBytes: 64 * 1024,
|
|
11
|
+
maxInboundBufferedBytes: 4 * 1024 * 1024,
|
|
11
12
|
maxOutboundBufferedBytes: 4 * 1024 * 1024,
|
|
12
13
|
}),
|
|
13
14
|
yamux: Object.freeze({
|
|
@@ -15,6 +16,7 @@ export const SDK_DEFAULTS = Object.freeze({
|
|
|
15
16
|
maxInboundStreams: 32,
|
|
16
17
|
maxFrameBytes: 256 * 1024,
|
|
17
18
|
preferredOutboundFrameBytes: 64 * 1024,
|
|
19
|
+
maxStreamWriteQueueBytes: 4 * 1024 * 1024,
|
|
18
20
|
maxStreamReceiveBytes: 256 * 1024,
|
|
19
21
|
maxSessionReceiveBytes: 16 * 1024 * 1024,
|
|
20
22
|
}),
|
package/dist/e2ee/handshake.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type HandshakeClientOptions = Readonly<{
|
|
|
16
16
|
maxRecordBytes: number;
|
|
17
17
|
/** Preferred plaintext bytes per outbound record. */
|
|
18
18
|
outboundRecordChunkBytes?: number;
|
|
19
|
-
/** Maximum
|
|
19
|
+
/** Maximum queued inbound plaintext bytes for the secure channel. */
|
|
20
20
|
maxBufferedBytes?: number;
|
|
21
21
|
/** Maximum queued outbound plaintext bytes for the secure channel. */
|
|
22
22
|
maxOutboundBufferedBytes?: number;
|
|
@@ -44,7 +44,7 @@ export type HandshakeServerOptions = Readonly<{
|
|
|
44
44
|
maxRecordBytes: number;
|
|
45
45
|
/** Preferred plaintext bytes per outbound record. */
|
|
46
46
|
outboundRecordChunkBytes?: number;
|
|
47
|
-
/** Maximum
|
|
47
|
+
/** Maximum queued inbound plaintext bytes for the secure channel. */
|
|
48
48
|
maxBufferedBytes?: number;
|
|
49
49
|
/** Maximum queued outbound plaintext bytes for the secure channel. */
|
|
50
50
|
maxOutboundBufferedBytes?: number;
|
|
@@ -31,7 +31,7 @@ export declare class SecureChannel {
|
|
|
31
31
|
private readonly transport;
|
|
32
32
|
private readonly maxRecordBytes;
|
|
33
33
|
private readonly outboundRecordChunkBytes;
|
|
34
|
-
private readonly
|
|
34
|
+
private readonly maxInboundBufferedBytes;
|
|
35
35
|
private readonly maxOutboundBufferedBytes;
|
|
36
36
|
private sendKey;
|
|
37
37
|
private recvKey;
|
|
@@ -50,7 +50,7 @@ export declare class SecureChannel {
|
|
|
50
50
|
private sendQueueBytes;
|
|
51
51
|
private sendClosed;
|
|
52
52
|
private sendErr;
|
|
53
|
-
private
|
|
53
|
+
private recvQueue;
|
|
54
54
|
private recvQueueHead;
|
|
55
55
|
private recvQueueBytes;
|
|
56
56
|
private recvWaiters;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RECORD_FLAG_APP, RECORD_FLAG_PING, RECORD_FLAG_REKEY } from "./constants.js";
|
|
2
2
|
import { decryptRecord, encryptRecord, maxPlaintextBytes } from "./record.js";
|
|
3
3
|
import { deriveRekeyKey } from "./kdf.js";
|
|
4
|
+
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
5
|
const maxRecordSeq = (1n << 64n) - 1n;
|
|
5
6
|
class RecordSeqExhaustedError extends Error {
|
|
6
7
|
constructor() {
|
|
@@ -15,8 +16,7 @@ export class SecureChannel {
|
|
|
15
16
|
// Maximum allowed bytes per record frame.
|
|
16
17
|
maxRecordBytes;
|
|
17
18
|
outboundRecordChunkBytes;
|
|
18
|
-
|
|
19
|
-
maxBufferedBytes;
|
|
19
|
+
maxInboundBufferedBytes;
|
|
20
20
|
maxOutboundBufferedBytes;
|
|
21
21
|
// Active encryption keys and nonce prefixes for the current epoch.
|
|
22
22
|
sendKey;
|
|
@@ -52,16 +52,16 @@ export class SecureChannel {
|
|
|
52
52
|
this.transport = args.transport;
|
|
53
53
|
this.maxRecordBytes = args.maxRecordBytes;
|
|
54
54
|
const maxPlain = Math.max(1, maxPlaintextBytes(this.maxRecordBytes));
|
|
55
|
-
this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(
|
|
55
|
+
this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(SDK_DEFAULTS.e2ee.outboundRecordChunkBytes, maxPlain);
|
|
56
56
|
if (!Number.isSafeInteger(this.outboundRecordChunkBytes) || this.outboundRecordChunkBytes <= 0 || this.outboundRecordChunkBytes > maxPlain) {
|
|
57
57
|
throw new RangeError("outboundRecordChunkBytes must be a positive integer within the record plaintext limit");
|
|
58
58
|
}
|
|
59
|
-
this.
|
|
60
|
-
const maxOutboundBufferedBytes = args.maxOutboundBufferedBytes ??
|
|
59
|
+
this.maxInboundBufferedBytes = Math.max(0, args.maxBufferedBytes ?? SDK_DEFAULTS.e2ee.maxInboundBufferedBytes);
|
|
60
|
+
const maxOutboundBufferedBytes = args.maxOutboundBufferedBytes ?? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes;
|
|
61
61
|
if (!Number.isSafeInteger(maxOutboundBufferedBytes) || maxOutboundBufferedBytes < 0) {
|
|
62
62
|
throw new RangeError("maxOutboundBufferedBytes must be a non-negative safe integer");
|
|
63
63
|
}
|
|
64
|
-
this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ?
|
|
64
|
+
this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes : maxOutboundBufferedBytes;
|
|
65
65
|
this.sendKey = args.sendKey;
|
|
66
66
|
this.recvKey = args.recvKey;
|
|
67
67
|
this.sendNoncePrefix = args.sendNoncePrefix;
|
|
@@ -87,7 +87,11 @@ export class SecureChannel {
|
|
|
87
87
|
if (this.recvQueueHead < this.recvQueue.length) {
|
|
88
88
|
const b = this.recvQueue[this.recvQueueHead];
|
|
89
89
|
this.recvQueueHead++;
|
|
90
|
-
if (this.recvQueueHead
|
|
90
|
+
if (this.recvQueueHead === this.recvQueue.length) {
|
|
91
|
+
this.recvQueue = [];
|
|
92
|
+
this.recvQueueHead = 0;
|
|
93
|
+
}
|
|
94
|
+
else if (this.recvQueueHead > 1024 && this.recvQueueHead * 2 > this.recvQueue.length) {
|
|
91
95
|
this.recvQueue.splice(0, this.recvQueueHead);
|
|
92
96
|
this.recvQueueHead = 0;
|
|
93
97
|
}
|
|
@@ -171,7 +175,11 @@ export class SecureChannel {
|
|
|
171
175
|
return null;
|
|
172
176
|
const req = this.sendQueue[this.sendQueueHead];
|
|
173
177
|
this.sendQueueHead++;
|
|
174
|
-
if (this.sendQueueHead
|
|
178
|
+
if (this.sendQueueHead === this.sendQueue.length) {
|
|
179
|
+
this.sendQueue = [];
|
|
180
|
+
this.sendQueueHead = 0;
|
|
181
|
+
}
|
|
182
|
+
else if (this.sendQueueHead > 1024 && this.sendQueueHead * 2 > this.sendQueue.length) {
|
|
175
183
|
this.sendQueue.splice(0, this.sendQueueHead);
|
|
176
184
|
this.sendQueueHead = 0;
|
|
177
185
|
}
|
|
@@ -182,7 +190,11 @@ export class SecureChannel {
|
|
|
182
190
|
return undefined;
|
|
183
191
|
const w = this.sendWaiters[this.sendWaitersHead];
|
|
184
192
|
this.sendWaitersHead++;
|
|
185
|
-
if (this.sendWaitersHead
|
|
193
|
+
if (this.sendWaitersHead === this.sendWaiters.length) {
|
|
194
|
+
this.sendWaiters = [];
|
|
195
|
+
this.sendWaitersHead = 0;
|
|
196
|
+
}
|
|
197
|
+
else if (this.sendWaitersHead > 1024 && this.sendWaitersHead * 2 > this.sendWaiters.length) {
|
|
186
198
|
this.sendWaiters.splice(0, this.sendWaitersHead);
|
|
187
199
|
this.sendWaitersHead = 0;
|
|
188
200
|
}
|
|
@@ -284,7 +296,7 @@ export class SecureChannel {
|
|
|
284
296
|
}
|
|
285
297
|
this.recvSeq = seq + 1n;
|
|
286
298
|
if (flags === RECORD_FLAG_APP) {
|
|
287
|
-
if (this.
|
|
299
|
+
if (this.maxInboundBufferedBytes > 0 && this.recvQueueBytes + plaintext.length > this.maxInboundBufferedBytes) {
|
|
288
300
|
throw new Error("recv buffer exceeded");
|
|
289
301
|
}
|
|
290
302
|
this.recvQueue.push(plaintext);
|