@floegence/flowersec-core 2.5.4 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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,56 @@
|
|
|
1
|
+
/** Version-bound transport identifiers shared by the v3 production paths. */
|
|
2
|
+
export declare const FLOWERSEC_V3_PROFILE: "flowersec/3";
|
|
3
|
+
export declare const FLOWERSEC_V3_WIRE_PROFILES: Readonly<{
|
|
4
|
+
readonly direct: "flowersec-direct/3";
|
|
5
|
+
readonly tunnel: "flowersec-tunnel/3";
|
|
6
|
+
}>;
|
|
7
|
+
export declare const FLOWERSEC_V3_PATHS: Readonly<{
|
|
8
|
+
readonly websocket: Readonly<{
|
|
9
|
+
direct: "/flowersec/v3/direct";
|
|
10
|
+
tunnel: "/flowersec/v3/tunnel";
|
|
11
|
+
}>;
|
|
12
|
+
readonly webtransport: Readonly<{
|
|
13
|
+
direct: "/flowersec/webtransport/v3/direct";
|
|
14
|
+
tunnel: "/flowersec/webtransport/v3/tunnel";
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
17
|
+
export declare const FLOWERSEC_V3_WEBSOCKET_SUBPROTOCOLS: Readonly<{
|
|
18
|
+
readonly direct: "flowersec.direct.v3";
|
|
19
|
+
readonly tunnel: "flowersec.tunnel.v3";
|
|
20
|
+
}>;
|
|
21
|
+
export declare const FLOWERSEC_V3_ALPN: Readonly<{
|
|
22
|
+
readonly direct: "flowersec-direct/3";
|
|
23
|
+
readonly tunnel: "flowersec-tunnel/3";
|
|
24
|
+
}>;
|
|
25
|
+
/** The strings below are the exact v3 inputs to the protocol KDF/MAC helpers. */
|
|
26
|
+
export declare const FLOWERSEC_V3_CRYPTO_LABELS: Readonly<{
|
|
27
|
+
readonly "session-contract": "flowersec-v3-session-contract\0";
|
|
28
|
+
readonly candidates: "flowersec-v3-candidates\0";
|
|
29
|
+
readonly admission: "flowersec-v3-admission\0";
|
|
30
|
+
readonly "runtime-capability": "flowersec-v3-runtime-capability\0";
|
|
31
|
+
readonly handshake: "flowersec-v3-handshake\0";
|
|
32
|
+
readonly "server-finished": "flowersec v3 server finished";
|
|
33
|
+
readonly "client-finished": "flowersec v3 client finished";
|
|
34
|
+
readonly "epoch-zero": "flowersec v3 epoch zero";
|
|
35
|
+
readonly "control-root": "flowersec v3 control root";
|
|
36
|
+
readonly "stream-root": "flowersec v3 stream root";
|
|
37
|
+
readonly "setup-root": "flowersec v3 setup root";
|
|
38
|
+
readonly "rekey-root": "flowersec v3 rekey root";
|
|
39
|
+
readonly "next-epoch": "flowersec v3 next epoch";
|
|
40
|
+
readonly stream: "flowersec v3 stream";
|
|
41
|
+
readonly control: "flowersec v3 control";
|
|
42
|
+
readonly "record-key": "flowersec v3 record key";
|
|
43
|
+
readonly nonce: "flowersec v3 nonce";
|
|
44
|
+
readonly "unreliable-root": "flowersec v3 unreliable root";
|
|
45
|
+
readonly unreliable: "flowersec v3 unreliable";
|
|
46
|
+
readonly "unreliable-key": "flowersec v3 unreliable key";
|
|
47
|
+
readonly "unreliable-nonce": "flowersec v3 unreliable nonce";
|
|
48
|
+
readonly "unreliable-aad": "flowersec-v3-unreliable";
|
|
49
|
+
readonly "setup-mac": "flowersec-v3-setup\0";
|
|
50
|
+
readonly "record-aad": "flowersec-v3-record\0";
|
|
51
|
+
readonly open: "flowersec-v3-open\0";
|
|
52
|
+
readonly "acceptor-admissions": "flowersec-v3-acceptor-admissions\0";
|
|
53
|
+
}>;
|
|
54
|
+
export declare function wireProfileForPathV3(path: "direct" | "tunnel"): string;
|
|
55
|
+
export declare function websocketSubprotocolForPathV3(path: "direct" | "tunnel"): string;
|
|
56
|
+
export declare function alpnForPathV3(path: "direct" | "tunnel"): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Version-bound transport identifiers shared by the v3 production paths. */
|
|
2
|
+
export const FLOWERSEC_V3_PROFILE = "flowersec/3";
|
|
3
|
+
export const FLOWERSEC_V3_WIRE_PROFILES = Object.freeze({
|
|
4
|
+
direct: "flowersec-direct/3",
|
|
5
|
+
tunnel: "flowersec-tunnel/3",
|
|
6
|
+
});
|
|
7
|
+
export const FLOWERSEC_V3_PATHS = Object.freeze({
|
|
8
|
+
websocket: Object.freeze({
|
|
9
|
+
direct: "/flowersec/v3/direct",
|
|
10
|
+
tunnel: "/flowersec/v3/tunnel",
|
|
11
|
+
}),
|
|
12
|
+
webtransport: Object.freeze({
|
|
13
|
+
direct: "/flowersec/webtransport/v3/direct",
|
|
14
|
+
tunnel: "/flowersec/webtransport/v3/tunnel",
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
export const FLOWERSEC_V3_WEBSOCKET_SUBPROTOCOLS = Object.freeze({
|
|
18
|
+
direct: "flowersec.direct.v3",
|
|
19
|
+
tunnel: "flowersec.tunnel.v3",
|
|
20
|
+
});
|
|
21
|
+
export const FLOWERSEC_V3_ALPN = FLOWERSEC_V3_WIRE_PROFILES;
|
|
22
|
+
/** The strings below are the exact v3 inputs to the protocol KDF/MAC helpers. */
|
|
23
|
+
export const FLOWERSEC_V3_CRYPTO_LABELS = Object.freeze({
|
|
24
|
+
"session-contract": "flowersec-v3-session-contract\0",
|
|
25
|
+
candidates: "flowersec-v3-candidates\0",
|
|
26
|
+
admission: "flowersec-v3-admission\0",
|
|
27
|
+
"runtime-capability": "flowersec-v3-runtime-capability\0",
|
|
28
|
+
handshake: "flowersec-v3-handshake\0",
|
|
29
|
+
"server-finished": "flowersec v3 server finished",
|
|
30
|
+
"client-finished": "flowersec v3 client finished",
|
|
31
|
+
"epoch-zero": "flowersec v3 epoch zero",
|
|
32
|
+
"control-root": "flowersec v3 control root",
|
|
33
|
+
"stream-root": "flowersec v3 stream root",
|
|
34
|
+
"setup-root": "flowersec v3 setup root",
|
|
35
|
+
"rekey-root": "flowersec v3 rekey root",
|
|
36
|
+
"next-epoch": "flowersec v3 next epoch",
|
|
37
|
+
stream: "flowersec v3 stream",
|
|
38
|
+
control: "flowersec v3 control",
|
|
39
|
+
"record-key": "flowersec v3 record key",
|
|
40
|
+
nonce: "flowersec v3 nonce",
|
|
41
|
+
"unreliable-root": "flowersec v3 unreliable root",
|
|
42
|
+
unreliable: "flowersec v3 unreliable",
|
|
43
|
+
"unreliable-key": "flowersec v3 unreliable key",
|
|
44
|
+
"unreliable-nonce": "flowersec v3 unreliable nonce",
|
|
45
|
+
"unreliable-aad": "flowersec-v3-unreliable",
|
|
46
|
+
"setup-mac": "flowersec-v3-setup\0",
|
|
47
|
+
"record-aad": "flowersec-v3-record\0",
|
|
48
|
+
open: "flowersec-v3-open\0",
|
|
49
|
+
"acceptor-admissions": "flowersec-v3-acceptor-admissions\0",
|
|
50
|
+
});
|
|
51
|
+
export function wireProfileForPathV3(path) {
|
|
52
|
+
return FLOWERSEC_V3_WIRE_PROFILES[path];
|
|
53
|
+
}
|
|
54
|
+
export function websocketSubprotocolForPathV3(path) {
|
|
55
|
+
return FLOWERSEC_V3_WEBSOCKET_SUBPROTOCOLS[path];
|
|
56
|
+
}
|
|
57
|
+
export function alpnForPathV3(path) {
|
|
58
|
+
return FLOWERSEC_V3_ALPN[path];
|
|
59
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CarrierUnreliableDatagramsV3 } from "./carrier.js";
|
|
2
|
+
import { CipherSuiteV3 } from "./protocol.js";
|
|
3
|
+
import type { DirectionV3 } from "./protocol.js";
|
|
4
|
+
export declare const UNRELIABLE_MESSAGES_FEATURE_V3 = 1;
|
|
5
|
+
export declare const UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V3: 976;
|
|
6
|
+
export declare const UNRELIABLE_MESSAGE_WIRE_BYTES_V3: 1024;
|
|
7
|
+
export type InternalUnreliableMessageChannelV3Options = Readonly<{
|
|
8
|
+
transport: CarrierUnreliableDatagramsV3;
|
|
9
|
+
suite: CipherSuiteV3;
|
|
10
|
+
h3: Uint8Array;
|
|
11
|
+
sendDirection: DirectionV3;
|
|
12
|
+
receiveDirection: DirectionV3;
|
|
13
|
+
currentSendEpoch(): Readonly<{
|
|
14
|
+
epoch: number;
|
|
15
|
+
epochSecret: Uint8Array;
|
|
16
|
+
}>;
|
|
17
|
+
receiveEpochSecret(epoch: number): Uint8Array | undefined;
|
|
18
|
+
onProtocolFailure(): void;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
}>;
|
|
21
|
+
export type UnreliableMessageMaterialV3 = Readonly<{
|
|
22
|
+
unreliableRoot: Uint8Array;
|
|
23
|
+
materialSecret: Uint8Array;
|
|
24
|
+
recordKey: Uint8Array;
|
|
25
|
+
noncePrefix: Uint8Array;
|
|
26
|
+
}>;
|
|
27
|
+
export type SealUnreliableMessageDatagramV3Options = Readonly<{
|
|
28
|
+
suite: CipherSuiteV3;
|
|
29
|
+
epochSecret: Uint8Array;
|
|
30
|
+
h3: Uint8Array;
|
|
31
|
+
direction: DirectionV3;
|
|
32
|
+
epoch: number;
|
|
33
|
+
sequence: bigint;
|
|
34
|
+
expiresAtUnixMs: bigint;
|
|
35
|
+
plaintext: Uint8Array;
|
|
36
|
+
}>;
|
|
37
|
+
export type SealedUnreliableMessageDatagramV3 = Readonly<{
|
|
38
|
+
material: UnreliableMessageMaterialV3;
|
|
39
|
+
nonce: Uint8Array;
|
|
40
|
+
header: Uint8Array;
|
|
41
|
+
aad: Uint8Array;
|
|
42
|
+
ciphertext: Uint8Array;
|
|
43
|
+
wire: Uint8Array;
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { gcm } from "@noble/ciphers/aes.js";
|
|
2
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
3
|
+
import { expand } from "@noble/hashes/hkdf.js";
|
|
4
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
5
|
+
import { CipherSuiteV3 } from "./protocol.js";
|
|
6
|
+
import { UnreliableMessageError } from "../public/contract.js";
|
|
7
|
+
import { FLOWERSEC_V3_CRYPTO_LABELS } from "./transportConstants.js";
|
|
8
|
+
export const UNRELIABLE_MESSAGES_FEATURE_V3 = 0x00000001;
|
|
9
|
+
export const UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V3 = 976;
|
|
10
|
+
export const UNRELIABLE_MESSAGE_WIRE_BYTES_V3 = 1_024;
|
|
11
|
+
const HEADER_BYTES = 32;
|
|
12
|
+
const TAG_BYTES = 16;
|
|
13
|
+
const MIN_CIPHERTEXT_BYTES = TAG_BYTES + 1;
|
|
14
|
+
const MAX_CIPHERTEXT_BYTES = UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V3 + TAG_BYTES;
|
|
15
|
+
const MAX_PENDING_SENDS = 64;
|
|
16
|
+
const MAX_UINT32 = 0xffffffff;
|
|
17
|
+
const MAX_UINT64 = (1n << 64n) - 1n;
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
/** @internal */ export { UnreliableMessageError } from "../public/contract.js";
|
|
20
|
+
/** @internal */
|
|
21
|
+
export function createInternalUnreliableMessageChannelV3(options) {
|
|
22
|
+
return new InternalUnreliableMessageChannelV3(options);
|
|
23
|
+
}
|
|
24
|
+
class InternalUnreliableMessageChannelV3 {
|
|
25
|
+
options;
|
|
26
|
+
maxMessageSize = UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V3;
|
|
27
|
+
replay = new Map();
|
|
28
|
+
now;
|
|
29
|
+
nextSequence = 0n;
|
|
30
|
+
sendEpoch = -1;
|
|
31
|
+
pendingSends = 0;
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
if (options.transport.maxDatagramSize < UNRELIABLE_MESSAGE_WIRE_BYTES_V3 || options.h3.byteLength !== 32) {
|
|
35
|
+
throw new UnreliableMessageError("operation_failed");
|
|
36
|
+
}
|
|
37
|
+
this.now = options.now ?? Date.now;
|
|
38
|
+
}
|
|
39
|
+
async send(message, options) {
|
|
40
|
+
throwIfAborted(options.signal);
|
|
41
|
+
if (!(message instanceof Uint8Array) || message.byteLength < 1 ||
|
|
42
|
+
message.byteLength > UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V3) {
|
|
43
|
+
throw new UnreliableMessageError("invalid_message");
|
|
44
|
+
}
|
|
45
|
+
const payload = message.slice();
|
|
46
|
+
const expiresAt = requireFutureExpiry(options.expiresAtUnixMs, this.now());
|
|
47
|
+
if (expiresAt === undefined)
|
|
48
|
+
return "dropped_expired";
|
|
49
|
+
if (this.pendingSends >= MAX_PENDING_SENDS)
|
|
50
|
+
return "dropped_budget";
|
|
51
|
+
const roots = this.options.currentSendEpoch();
|
|
52
|
+
if (roots.epoch !== this.sendEpoch) {
|
|
53
|
+
this.sendEpoch = roots.epoch;
|
|
54
|
+
this.nextSequence = 0n;
|
|
55
|
+
}
|
|
56
|
+
if (this.nextSequence > MAX_UINT64)
|
|
57
|
+
throw new UnreliableMessageError("closed");
|
|
58
|
+
const sequence = this.nextSequence++;
|
|
59
|
+
const sealed = sealUnreliableMessageDatagramV3({
|
|
60
|
+
suite: this.options.suite,
|
|
61
|
+
epochSecret: roots.epochSecret,
|
|
62
|
+
h3: this.options.h3,
|
|
63
|
+
direction: this.options.sendDirection,
|
|
64
|
+
epoch: roots.epoch,
|
|
65
|
+
sequence,
|
|
66
|
+
expiresAtUnixMs: BigInt(expiresAt),
|
|
67
|
+
plaintext: payload,
|
|
68
|
+
});
|
|
69
|
+
this.pendingSends++;
|
|
70
|
+
try {
|
|
71
|
+
return await this.options.transport.send(sealed.wire, {
|
|
72
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
73
|
+
expiresAt,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
78
|
+
throw error;
|
|
79
|
+
throw new UnreliableMessageError("operation_failed");
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
this.pendingSends--;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async receive(options = {}) {
|
|
86
|
+
for (;;) {
|
|
87
|
+
throwIfAborted(options.signal);
|
|
88
|
+
let wire;
|
|
89
|
+
try {
|
|
90
|
+
wire = await this.options.transport.receive(options);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (isAbortError(error))
|
|
94
|
+
throw error;
|
|
95
|
+
throw new UnreliableMessageError("closed");
|
|
96
|
+
}
|
|
97
|
+
if (isPreviousVersionDatagram(wire)) {
|
|
98
|
+
this.options.onProtocolFailure();
|
|
99
|
+
throw new UnreliableMessageError("closed");
|
|
100
|
+
}
|
|
101
|
+
const decoded = decodeHeader(wire);
|
|
102
|
+
if (decoded === undefined || decoded.expiresAtUnixMs <= BigInt(Math.floor(this.now())))
|
|
103
|
+
continue;
|
|
104
|
+
const epochSecret = this.options.receiveEpochSecret(decoded.epoch);
|
|
105
|
+
if (epochSecret === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const window = this.replay.get(decoded.epoch) ?? new ReplayWindow();
|
|
108
|
+
if (window.seen(decoded.sequence))
|
|
109
|
+
continue;
|
|
110
|
+
const material = deriveUnreliableMessageMaterialV3(epochSecret, this.options.h3, this.options.receiveDirection, decoded.epoch);
|
|
111
|
+
const nonce = concat(material.noncePrefix, u64be(decoded.sequence));
|
|
112
|
+
const aad = labelWith(FLOWERSEC_V3_CRYPTO_LABELS["unreliable-aad"], this.options.h3, byte(this.options.receiveDirection), wire.subarray(0, HEADER_BYTES));
|
|
113
|
+
let plaintext;
|
|
114
|
+
try {
|
|
115
|
+
plaintext = cipher(this.options.suite, material.recordKey, nonce, aad).decrypt(wire.subarray(HEADER_BYTES));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (plaintext.byteLength < 1 || plaintext.byteLength > UNRELIABLE_MESSAGE_MAX_PLAINTEXT_BYTES_V3)
|
|
121
|
+
continue;
|
|
122
|
+
window.mark(decoded.sequence);
|
|
123
|
+
this.replay.set(decoded.epoch, window);
|
|
124
|
+
pruneReplayEpochs(this.replay, decoded.epoch);
|
|
125
|
+
return plaintext.slice();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isPreviousVersionDatagram(wire) {
|
|
130
|
+
if (!(wire instanceof Uint8Array) || wire.byteLength < 5 ||
|
|
131
|
+
wire[0] !== 0x46 || wire[1] !== 0x53 || wire[2] !== 0x44)
|
|
132
|
+
return false;
|
|
133
|
+
return wire[3] === 0x32 || (wire[3] === 0x33 && wire[4] === 2);
|
|
134
|
+
}
|
|
135
|
+
/** @internal */
|
|
136
|
+
export function encodeUnreliableMessageHeaderV3(epoch, sequence, expiresAtUnixMs, ciphertextLength) {
|
|
137
|
+
if (!isUint32(epoch) || !isUint64(sequence) || !isUint64(expiresAtUnixMs) ||
|
|
138
|
+
expiresAtUnixMs === 0n || !Number.isInteger(ciphertextLength) ||
|
|
139
|
+
ciphertextLength < MIN_CIPHERTEXT_BYTES || ciphertextLength > MAX_CIPHERTEXT_BYTES) {
|
|
140
|
+
throw new Error("invalid FSD3 header");
|
|
141
|
+
}
|
|
142
|
+
const header = new Uint8Array(HEADER_BYTES);
|
|
143
|
+
header.set(encoder.encode("FSD3"));
|
|
144
|
+
header[4] = 3;
|
|
145
|
+
header[5] = 0;
|
|
146
|
+
const view = new DataView(header.buffer);
|
|
147
|
+
view.setUint16(6, HEADER_BYTES, false);
|
|
148
|
+
view.setUint32(8, epoch, false);
|
|
149
|
+
view.setBigUint64(12, sequence, false);
|
|
150
|
+
view.setBigUint64(20, expiresAtUnixMs, false);
|
|
151
|
+
view.setUint32(28, ciphertextLength, false);
|
|
152
|
+
return header;
|
|
153
|
+
}
|
|
154
|
+
function decodeHeader(wire) {
|
|
155
|
+
if (!(wire instanceof Uint8Array) || wire.byteLength < HEADER_BYTES + TAG_BYTES ||
|
|
156
|
+
wire.byteLength > UNRELIABLE_MESSAGE_WIRE_BYTES_V3 ||
|
|
157
|
+
wire[0] !== 0x46 || wire[1] !== 0x53 || wire[2] !== 0x44 || wire[3] !== 0x33 ||
|
|
158
|
+
wire[4] !== 3 || wire[5] !== 0)
|
|
159
|
+
return undefined;
|
|
160
|
+
const view = new DataView(wire.buffer, wire.byteOffset, HEADER_BYTES);
|
|
161
|
+
if (view.getUint16(6, false) !== HEADER_BYTES || view.getUint32(28, false) !== wire.byteLength - HEADER_BYTES) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
const expiresAtUnixMs = view.getBigUint64(20, false);
|
|
165
|
+
if (expiresAtUnixMs === 0n)
|
|
166
|
+
return undefined;
|
|
167
|
+
return {
|
|
168
|
+
epoch: view.getUint32(8, false),
|
|
169
|
+
sequence: view.getBigUint64(12, false),
|
|
170
|
+
expiresAtUnixMs,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** @internal */
|
|
174
|
+
export function deriveUnreliableMessageMaterialV3(epochSecret, h3, direction, epoch) {
|
|
175
|
+
const unreliableRoot = expand(sha256, epochSecret, labelWith(FLOWERSEC_V3_CRYPTO_LABELS["unreliable-root"]), 32);
|
|
176
|
+
const materialSecret = expand(sha256, unreliableRoot, labelWith(FLOWERSEC_V3_CRYPTO_LABELS.unreliable, h3, byte(direction), u32be(epoch)), 32);
|
|
177
|
+
return {
|
|
178
|
+
unreliableRoot,
|
|
179
|
+
materialSecret,
|
|
180
|
+
recordKey: expand(sha256, materialSecret, labelWith(FLOWERSEC_V3_CRYPTO_LABELS["unreliable-key"]), 32),
|
|
181
|
+
noncePrefix: expand(sha256, materialSecret, labelWith(FLOWERSEC_V3_CRYPTO_LABELS["unreliable-nonce"]), 4),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** @internal */
|
|
185
|
+
export function sealUnreliableMessageDatagramV3(options) {
|
|
186
|
+
const header = encodeUnreliableMessageHeaderV3(options.epoch, options.sequence, options.expiresAtUnixMs, options.plaintext.byteLength + TAG_BYTES);
|
|
187
|
+
const material = deriveUnreliableMessageMaterialV3(options.epochSecret, options.h3, options.direction, options.epoch);
|
|
188
|
+
const nonce = concat(material.noncePrefix, u64be(options.sequence));
|
|
189
|
+
const aad = labelWith(FLOWERSEC_V3_CRYPTO_LABELS["unreliable-aad"], options.h3, byte(options.direction), header);
|
|
190
|
+
const ciphertext = cipher(options.suite, material.recordKey, nonce, aad).encrypt(options.plaintext);
|
|
191
|
+
return {
|
|
192
|
+
material,
|
|
193
|
+
nonce,
|
|
194
|
+
header,
|
|
195
|
+
aad,
|
|
196
|
+
ciphertext,
|
|
197
|
+
wire: concat(header, ciphertext),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function cipher(suite, key, nonce, aad) {
|
|
201
|
+
if (suite === CipherSuiteV3.ChaCha20Poly1305)
|
|
202
|
+
return chacha20poly1305(key, nonce, aad);
|
|
203
|
+
if (suite === CipherSuiteV3.AES256GCM)
|
|
204
|
+
return gcm(key, nonce, aad);
|
|
205
|
+
throw new UnreliableMessageError("operation_failed");
|
|
206
|
+
}
|
|
207
|
+
class ReplayWindow {
|
|
208
|
+
highest = -1n;
|
|
209
|
+
bitmap = 0n;
|
|
210
|
+
seen(sequence) {
|
|
211
|
+
if (this.highest < 0n || sequence > this.highest)
|
|
212
|
+
return false;
|
|
213
|
+
const distance = this.highest - sequence;
|
|
214
|
+
return distance >= 64n || (this.bitmap & (1n << distance)) !== 0n;
|
|
215
|
+
}
|
|
216
|
+
mark(sequence) {
|
|
217
|
+
if (this.highest < 0n) {
|
|
218
|
+
this.highest = sequence;
|
|
219
|
+
this.bitmap = 1n;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (sequence > this.highest) {
|
|
223
|
+
const shift = sequence - this.highest;
|
|
224
|
+
this.bitmap = shift >= 64n ? 1n : ((this.bitmap << shift) | 1n) & ((1n << 64n) - 1n);
|
|
225
|
+
this.highest = sequence;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.bitmap |= 1n << (this.highest - sequence);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function pruneReplayEpochs(windows, current) {
|
|
232
|
+
for (const epoch of windows.keys()) {
|
|
233
|
+
if (epoch + 1 < current || epoch > current + 1)
|
|
234
|
+
windows.delete(epoch);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function requireFutureExpiry(value, now) {
|
|
238
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
239
|
+
throw new UnreliableMessageError("invalid_message");
|
|
240
|
+
return value <= now ? undefined : value;
|
|
241
|
+
}
|
|
242
|
+
function throwIfAborted(signal) {
|
|
243
|
+
if (signal?.aborted === true)
|
|
244
|
+
throw new DOMException("operation aborted", "AbortError");
|
|
245
|
+
}
|
|
246
|
+
function isAbortError(error) {
|
|
247
|
+
return error instanceof DOMException && error.name === "AbortError" ||
|
|
248
|
+
error instanceof Error && error.name === "AbortError";
|
|
249
|
+
}
|
|
250
|
+
function labelWith(label, ...parts) {
|
|
251
|
+
return concat(encoder.encode(label), byte(0), ...parts);
|
|
252
|
+
}
|
|
253
|
+
function concat(...parts) {
|
|
254
|
+
const out = new Uint8Array(parts.reduce((size, part) => size + part.byteLength, 0));
|
|
255
|
+
let offset = 0;
|
|
256
|
+
for (const part of parts) {
|
|
257
|
+
out.set(part, offset);
|
|
258
|
+
offset += part.byteLength;
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function byte(value) {
|
|
263
|
+
return Uint8Array.of(value);
|
|
264
|
+
}
|
|
265
|
+
function u32be(value) {
|
|
266
|
+
if (!isUint32(value))
|
|
267
|
+
throw new Error("invalid FSD3 uint32");
|
|
268
|
+
const out = new Uint8Array(4);
|
|
269
|
+
new DataView(out.buffer).setUint32(0, value, false);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
function u64be(value) {
|
|
273
|
+
if (!isUint64(value))
|
|
274
|
+
throw new Error("invalid FSD3 uint64");
|
|
275
|
+
const out = new Uint8Array(8);
|
|
276
|
+
new DataView(out.buffer).setBigUint64(0, value, false);
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
function isUint32(value) {
|
|
280
|
+
return Number.isInteger(value) && value >= 0 && value <= MAX_UINT32;
|
|
281
|
+
}
|
|
282
|
+
function isUint64(value) {
|
|
283
|
+
return typeof value === "bigint" && value >= 0n && value <= MAX_UINT64;
|
|
284
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type CarrierSessionV3 } from "./carrier.js";
|
|
2
|
+
import type { OperationOptionsV3, PathKind } from "./contract.js";
|
|
3
|
+
export type WebSocketBinaryTransportV3 = Readonly<{
|
|
4
|
+
readBinary(options?: Readonly<{
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}>): Promise<Uint8Array>;
|
|
8
|
+
writeBinary(data: Uint8Array, options?: OperationOptionsV3): Promise<void>;
|
|
9
|
+
flush(options?: OperationOptionsV3): Promise<void>;
|
|
10
|
+
close(): void;
|
|
11
|
+
}>;
|
|
12
|
+
export type WebSocketResourcePolicyV3 = Readonly<{
|
|
13
|
+
maxConcurrentStreams?: number;
|
|
14
|
+
maxFrameBytes?: number;
|
|
15
|
+
preferredWriteBytes?: number;
|
|
16
|
+
maxStreamWriteQueueBytes?: number;
|
|
17
|
+
maxStreamReceiveBytes?: number;
|
|
18
|
+
maxSessionReceiveBytes?: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function createWebSocketCarrierSessionV3(transport: WebSocketBinaryTransportV3, options: Readonly<{
|
|
21
|
+
path: PathKind;
|
|
22
|
+
client: boolean;
|
|
23
|
+
inboundBidirectionalStreamCapacity: number;
|
|
24
|
+
resourcePolicy?: WebSocketResourcePolicyV3;
|
|
25
|
+
}>): CarrierSessionV3;
|
|
26
|
+
export declare function createServerWebSocketCarrierSessionV3(transport: WebSocketBinaryTransportV3, options: Readonly<{
|
|
27
|
+
path: PathKind;
|
|
28
|
+
inboundBidirectionalStreamCapacity: number;
|
|
29
|
+
resourcePolicy?: WebSocketResourcePolicyV3;
|
|
30
|
+
}>): CarrierSessionV3;
|
|
31
|
+
export declare function configureServerWebSocketCarrierRoleV3(carrier: CarrierSessionV3, client: boolean): void;
|