@browsercore/testing 0.1.3 → 0.2.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 +5 -0
- package/bin/coverage-md.mjs +99 -0
- package/captures/DIVERGENCE_REPORT.md +325 -0
- package/captures/IMPERSIONATION_GAPS_REPORT.md +367 -0
- package/captures/README.md +56 -0
- package/captures/REVIEW_FINDINGS.md +71 -0
- package/captures/_probe/analyze.py +67 -0
- package/captures/_probe/capture_bytes.py +127 -0
- package/captures/_probe/chrome131_determinism.py +32 -0
- package/captures/_probe/diagnose_capture.py +59 -0
- package/captures/_probe/golden_analysis.py +91 -0
- package/captures/_probe/inspect_golden.py +55 -0
- package/captures/_probe/inspect_sv.py +16 -0
- package/captures/_probe/make_golden.py +170 -0
- package/captures/_probe/output/chrome100.json +219 -0
- package/captures/_probe/output/chrome101.json +219 -0
- package/captures/_probe/output/chrome104.json +219 -0
- package/captures/_probe/output/chrome107.json +220 -0
- package/captures/_probe/output/chrome110.json +220 -0
- package/captures/_probe/output/chrome116.json +220 -0
- package/captures/_probe/output/chrome119.json +219 -0
- package/captures/_probe/output/chrome120.json +219 -0
- package/captures/_probe/output/chrome123.json +223 -0
- package/captures/_probe/output/chrome124.json +224 -0
- package/captures/_probe/output/chrome131.json +224 -0
- package/captures/_probe/output/chrome131_fresh.json +224 -0
- package/captures/_probe/output/chrome133a.json +224 -0
- package/captures/_probe/output/chrome136.json +224 -0
- package/captures/_probe/output/chrome99.json +219 -0
- package/captures/_probe/output/edge101.json +219 -0
- package/captures/_probe/output/edge99.json +219 -0
- package/captures/_probe/output/firefox133.json +227 -0
- package/captures/_probe/output/firefox135.json +230 -0
- package/captures/_probe/output/safari155.json +210 -0
- package/captures/_probe/output/safari170.json +214 -0
- package/captures/_probe/output/safari170_fresh.json +214 -0
- package/captures/_probe/output/safari180.json +216 -0
- package/captures/_probe/output/safari180_ios.json +216 -0
- package/captures/_probe/output/safari184.json +215 -0
- package/captures/_probe/output/safari260.json +211 -0
- package/captures/_probe/output/summary.json +2520 -0
- package/captures/_probe/parse_existing_settings.py +15 -0
- package/captures/_probe/peetprint.py +39 -0
- package/captures/_probe/probe.py +137 -0
- package/captures/_probe/raw_bytes/chrome-131_client_hello.bin +0 -0
- package/captures/_probe/raw_bytes/chrome-131_client_hello.hex +32 -0
- package/captures/_probe/raw_bytes/firefox-133_client_hello.bin +0 -0
- package/captures/_probe/raw_bytes/firefox-133_client_hello.hex +32 -0
- package/captures/_probe/raw_bytes/safari-17_client_hello.bin +0 -0
- package/captures/_probe/raw_bytes/safari-17_client_hello.hex +32 -0
- package/captures/_probe/relay_capture.py +137 -0
- package/captures/_probe/socks_capture.py +138 -0
- package/captures/_probe/validate_fresh.py +66 -0
- package/captures/_probe/validate_golden.py +97 -0
- package/captures/_probe/verify_capture.py +68 -0
- package/captures/chrome-131/http2/settings.bin +0 -0
- package/captures/chrome-131/http2/settings.meta.json +9 -0
- package/captures/chrome-131/oracle_capture.reference.json +224 -0
- package/captures/chrome-131/tls/client_hello.bin +0 -0
- package/captures/chrome-131/tls/client_hello.meta.json +20 -0
- package/captures/chrome-140/http2/settings.bin +0 -0
- package/captures/chrome-140/http2/settings.meta.json +9 -0
- package/captures/chrome-140/tls/client_hello.bin +0 -0
- package/captures/chrome-140/tls/client_hello.meta.json +12 -0
- package/captures/firefox-128/tls/client_hello.bin +0 -0
- package/captures/firefox-128/tls/client_hello.meta.json +12 -0
- package/captures/firefox-133/http2/settings.bin +0 -0
- package/captures/firefox-133/http2/settings.meta.json +9 -0
- package/captures/firefox-133/oracle_capture.reference.json +227 -0
- package/captures/firefox-133/tls/client_hello.bin +0 -0
- package/captures/firefox-133/tls/client_hello.meta.json +20 -0
- package/captures/safari-17/http2/settings.bin +0 -0
- package/captures/safari-17/http2/settings.meta.json +9 -0
- package/captures/safari-17/oracle_capture.reference.json +214 -0
- package/captures/safari-17/tls/client_hello.bin +0 -0
- package/captures/safari-17/tls/client_hello.meta.json +20 -0
- package/captures/test-malformed/http2/settings.bin +0 -0
- package/captures/test-malformed/http2/settings.meta.json +9 -0
- package/captures/test-no-extensions/tls/client_hello.bin +0 -0
- package/captures/test-no-extensions/tls/client_hello.meta.json +9 -0
- package/captures/test-wrong-type/http2/settings.bin +0 -0
- package/captures/test-wrong-type/http2/settings.meta.json +9 -0
- package/dist/bench/bench.d.ts +24 -6
- package/dist/bench/bench.d.ts.map +1 -1
- package/dist/bench/bench.js +110 -10
- package/dist/bench/bench.js.map +1 -1
- package/dist/captures/manifest.d.ts.map +1 -1
- package/dist/captures/manifest.js +7 -7
- package/dist/captures/manifest.js.map +1 -1
- package/dist/categories/compression.d.ts +6 -4
- package/dist/categories/compression.d.ts.map +1 -1
- package/dist/categories/compression.js +56 -6
- package/dist/categories/compression.js.map +1 -1
- package/dist/categories/cookies.d.ts +3 -2
- package/dist/categories/cookies.d.ts.map +1 -1
- package/dist/categories/cookies.js +216 -11
- package/dist/categories/cookies.js.map +1 -1
- package/dist/categories/redirects.d.ts +8 -2
- package/dist/categories/redirects.d.ts.map +1 -1
- package/dist/categories/redirects.js +97 -12
- package/dist/categories/redirects.js.map +1 -1
- package/dist/e2e/cert-gen.d.ts +31 -0
- package/dist/e2e/cert-gen.d.ts.map +1 -0
- package/dist/e2e/cert-gen.js +158 -0
- package/dist/e2e/cert-gen.js.map +1 -0
- package/dist/e2e/parse-clienthello.d.ts +177 -0
- package/dist/e2e/parse-clienthello.d.ts.map +1 -0
- package/dist/e2e/parse-clienthello.js +343 -0
- package/dist/e2e/parse-clienthello.js.map +1 -0
- package/dist/e2e/sink-server.d.ts +109 -0
- package/dist/e2e/sink-server.d.ts.map +1 -0
- package/dist/e2e/sink-server.js +292 -0
- package/dist/e2e/sink-server.js.map +1 -0
- package/dist/e2e/traffic-server.d.ts +48 -0
- package/dist/e2e/traffic-server.d.ts.map +1 -0
- package/dist/e2e/traffic-server.js +162 -0
- package/dist/e2e/traffic-server.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/fingerprint/index.d.ts +5 -2
- package/dist/fingerprint/index.d.ts.map +1 -1
- package/dist/fingerprint/index.js +3 -2
- package/dist/fingerprint/index.js.map +1 -1
- package/dist/fingerprint/ja3.d.ts.map +1 -1
- package/dist/fingerprint/ja3.js +33 -4
- package/dist/fingerprint/ja3.js.map +1 -1
- package/dist/fingerprint/ja4-errors.d.ts +17 -0
- package/dist/fingerprint/ja4-errors.d.ts.map +1 -0
- package/dist/fingerprint/ja4-errors.js +20 -0
- package/dist/fingerprint/ja4-errors.js.map +1 -0
- package/dist/fingerprint/ja4-reader.d.ts +44 -0
- package/dist/fingerprint/ja4-reader.d.ts.map +1 -0
- package/dist/fingerprint/ja4-reader.js +112 -0
- package/dist/fingerprint/ja4-reader.js.map +1 -0
- package/dist/fingerprint/ja4.d.ts +11 -43
- package/dist/fingerprint/ja4.d.ts.map +1 -1
- package/dist/fingerprint/ja4.js +54 -141
- package/dist/fingerprint/ja4.js.map +1 -1
- package/dist/fingerprint/ja4h.d.ts +46 -0
- package/dist/fingerprint/ja4h.d.ts.map +1 -0
- package/dist/fingerprint/ja4h.js +50 -0
- package/dist/fingerprint/ja4h.js.map +1 -0
- package/dist/golden/golden.d.ts +15 -1
- package/dist/golden/golden.d.ts.map +1 -1
- package/dist/golden/golden.js +42 -28
- package/dist/golden/golden.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/node-provider.d.ts +42 -0
- package/dist/node-provider.d.ts.map +1 -0
- package/dist/node-provider.js +53 -0
- package/dist/node-provider.js.map +1 -0
- package/dist/provider.d.ts +52 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +14 -0
- package/dist/provider.js.map +1 -0
- package/dist/reference/browser-provider.d.ts +33 -0
- package/dist/reference/browser-provider.d.ts.map +1 -0
- package/dist/reference/browser-provider.js +61 -0
- package/dist/reference/browser-provider.js.map +1 -0
- package/dist/reference/curl-provider.d.ts +40 -0
- package/dist/reference/curl-provider.d.ts.map +1 -0
- package/dist/reference/curl-provider.js +96 -0
- package/dist/reference/curl-provider.js.map +1 -0
- package/dist/reference/dump.d.ts +42 -0
- package/dist/reference/dump.d.ts.map +1 -0
- package/dist/reference/dump.js +102 -0
- package/dist/reference/dump.js.map +1 -0
- package/dist/reference/node-crypto-provider.d.ts +31 -0
- package/dist/reference/node-crypto-provider.d.ts.map +1 -0
- package/dist/reference/node-crypto-provider.js +199 -0
- package/dist/reference/node-crypto-provider.js.map +1 -0
- package/dist/reference/node-reference.d.ts.map +1 -1
- package/dist/reference/node-reference.js +2 -5
- package/dist/reference/node-reference.js.map +1 -1
- package/dist/reference/reference-errors.d.ts +36 -0
- package/dist/reference/reference-errors.d.ts.map +1 -0
- package/dist/reference/reference-errors.js +43 -0
- package/dist/reference/reference-errors.js.map +1 -0
- package/dist/reference/reference-types.d.ts +33 -0
- package/dist/reference/reference-types.d.ts.map +1 -0
- package/dist/reference/reference-types.js +9 -0
- package/dist/reference/reference-types.js.map +1 -0
- package/dist/reference/reference.d.ts +14 -77
- package/dist/reference/reference.d.ts.map +1 -1
- package/dist/reference/reference.js +43 -207
- package/dist/reference/reference.js.map +1 -1
- package/dist/rfc/rfcTests.d.ts +46 -8
- package/dist/rfc/rfcTests.d.ts.map +1 -1
- package/dist/rfc/rfcTests.js +203 -16
- package/dist/rfc/rfcTests.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +11 -5
- package/dist/utils.js.map +1 -1
- package/package.json +68 -55
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SECONDARY reference provider — loads pre-recorded captures from `captures/`.
|
|
3
|
+
*
|
|
4
|
+
* `capture()` resolves a stored capture for the profile; `fingerprint()`
|
|
5
|
+
* derives a {@link Fingerprint} from the captured bytes (only TLS ClientHellos
|
|
6
|
+
* are fingerprinted — other protocols throw). Split out of `reference.ts` so
|
|
7
|
+
* the facade stays focused on primary → secondary dispatch.
|
|
8
|
+
*/
|
|
9
|
+
import type { ProfileId } from "@browsercore/profiles";
|
|
10
|
+
import type { GoldenCapture } from "../types.js";
|
|
11
|
+
import type { Fingerprint, ReferenceProvider, ReferenceProviderKind } from "./reference-types.js";
|
|
12
|
+
/** Options for the real-browser (pre-recorded capture) provider. */
|
|
13
|
+
export interface RealBrowserOptions {
|
|
14
|
+
readonly command?: undefined;
|
|
15
|
+
/** Override captures directory. Defaults to the in-repo `captures/` dir. */
|
|
16
|
+
readonly capturesDir?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* SECONDARY provider — loads pre-recorded captures from the `captures/` dir.
|
|
20
|
+
*
|
|
21
|
+
* `capture()` resolves a stored capture for the profile; `fingerprint()`
|
|
22
|
+
* derives a {@link Fingerprint} from the captured bytes (only TLS
|
|
23
|
+
* ClientHellos are fingerprinted — other protocols return a stub).
|
|
24
|
+
*/
|
|
25
|
+
export declare class RealBrowserCaptureProvider implements ReferenceProvider {
|
|
26
|
+
readonly kind: ReferenceProviderKind;
|
|
27
|
+
readonly capturesDir: string;
|
|
28
|
+
constructor(options?: RealBrowserOptions);
|
|
29
|
+
availableProfiles(): ProfileId[];
|
|
30
|
+
capture(profile: ProfileId, _url: string): Promise<GoldenCapture>;
|
|
31
|
+
fingerprint(profile: ProfileId): Promise<Fingerprint>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=browser-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-provider.d.ts","sourceRoot":"","sources":["../../src/reference/browser-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlG,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,YAAW,iBAAiB;IAChE,SAAgB,IAAI,EAAE,qBAAqB,CAAqC;IAChF,SAAgB,WAAW,EAAE,MAAM,CAAC;IAEpC,YAAY,OAAO,CAAC,EAAE,kBAAkB,EAEvC;IAED,iBAAiB,IAAI,SAAS,EAAE,CAM/B;IAEK,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAgBtE;IAEK,WAAW,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAQ1D;CACJ"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SECONDARY reference provider — loads pre-recorded captures from `captures/`.
|
|
3
|
+
*
|
|
4
|
+
* `capture()` resolves a stored capture for the profile; `fingerprint()`
|
|
5
|
+
* derives a {@link Fingerprint} from the captured bytes (only TLS ClientHellos
|
|
6
|
+
* are fingerprinted — other protocols throw). Split out of `reference.ts` so
|
|
7
|
+
* the facade stays focused on primary → secondary dispatch.
|
|
8
|
+
*/
|
|
9
|
+
import { ReferenceError } from "./reference-errors.js";
|
|
10
|
+
import { fingerprintFromTlsCapture, profileToSource } from "./dump.js";
|
|
11
|
+
import { createPath } from "../node-provider.js";
|
|
12
|
+
const path = createPath();
|
|
13
|
+
/**
|
|
14
|
+
* SECONDARY provider — loads pre-recorded captures from the `captures/` dir.
|
|
15
|
+
*
|
|
16
|
+
* `capture()` resolves a stored capture for the profile; `fingerprint()`
|
|
17
|
+
* derives a {@link Fingerprint} from the captured bytes (only TLS
|
|
18
|
+
* ClientHellos are fingerprinted — other protocols return a stub).
|
|
19
|
+
*/
|
|
20
|
+
export class RealBrowserCaptureProvider {
|
|
21
|
+
kind = { kind: "real-browser" };
|
|
22
|
+
capturesDir;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.capturesDir = options?.capturesDir ?? defaultCapturesDir;
|
|
25
|
+
}
|
|
26
|
+
availableProfiles() {
|
|
27
|
+
// Discovery is driven by the captures manifest (captures/manifest.ts).
|
|
28
|
+
return [
|
|
29
|
+
"chrome-140",
|
|
30
|
+
"firefox-128",
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
async capture(profile, _url) {
|
|
34
|
+
void _url;
|
|
35
|
+
const manifest = await import("../captures/manifest.js");
|
|
36
|
+
const entry = manifest.captures.find((c) => c.meta.profile === profile);
|
|
37
|
+
if (entry === undefined) {
|
|
38
|
+
throw new ReferenceError(`No pre-recorded capture for profile ${String(profile)}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
id: `${profile}/tls/client_hello`,
|
|
42
|
+
source: profileToSource(profile),
|
|
43
|
+
protocol: entry.meta.protocol,
|
|
44
|
+
bytes: entry.bytes,
|
|
45
|
+
description: entry.meta.description,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async fingerprint(profile) {
|
|
49
|
+
const capture = await this.capture(profile, "https://example.com");
|
|
50
|
+
if (capture.protocol !== "tls") {
|
|
51
|
+
throw new ReferenceError(`fingerprint() only supports TLS captures; got ${capture.protocol}`);
|
|
52
|
+
}
|
|
53
|
+
return fingerprintFromTlsCapture(capture);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Default captures dir resolved from this module's location. Computed at module
|
|
57
|
+
// load (not per-instance) since import.meta.dirname is static.
|
|
58
|
+
const here = import.meta.dirname;
|
|
59
|
+
const packageRoot = path.join(here, "..", "..");
|
|
60
|
+
const defaultCapturesDir = path.join(packageRoot, "captures");
|
|
61
|
+
//# sourceMappingURL=browser-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-provider.js","sourceRoot":"","sources":["../../src/reference/browser-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;AAS1B;;;;;;GAMG;AACH,MAAM,OAAO,0BAA0B;IACnB,IAAI,GAA0B,EAAE,IAAI,EAAE,cAAc,EAAW,CAAC;IAChE,WAAW,CAAS;IAEpC,YAAY,OAA4B;QACpC,IAAI,CAAC,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,kBAAkB,CAAC;IAClE,CAAC;IAED,iBAAiB;QACb,uEAAuE;QACvE,OAAO;YACH,YAAyB;YACzB,aAA0B;SAC7B,CAAC;IACN,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAkB,EAAE,IAAY;QAC1C,KAAK,IAAI,CAAC;QACV,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC;QACxE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,cAAc,CACpB,uCAAuC,MAAM,CAAC,OAAO,CAAC,EAAE,CAC3D,CAAC;QACN,CAAC;QACD,OAAO;YACH,EAAE,EAAE,GAAG,OAAO,mBAA0C;YACxD,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC;YAChC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ;YAC7B,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW;SACtC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAkB;QAChC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;QACnE,IAAI,OAAO,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YAC7B,MAAM,IAAI,cAAc,CACpB,iDAAiD,OAAO,CAAC,QAAQ,EAAE,CACtE,CAAC;QACN,CAAC;QACD,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;CACJ;AAED,gFAAgF;AAChF,+DAA+D;AAC/D,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,OAAO,CAAC;AACjC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAChD,MAAM,kBAAkB,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRIMARY reference provider — shells out to curl-impersonate.
|
|
3
|
+
*
|
|
4
|
+
* `capture()` invokes the curl-impersonate binary against `url`, impersonating
|
|
5
|
+
* `profile`, and returns the raw bytes it observed on the wire. The binary
|
|
6
|
+
* must be on PATH (or in `command`); if it is missing, the call throws
|
|
7
|
+
* {@link ReferenceError} so the facade can fall back to the secondary.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `reference.ts` so the facade module stays focused on primary →
|
|
10
|
+
* secondary dispatch while this file owns the curl-impersonate subprocess
|
|
11
|
+
* wiring and dump-format parsing.
|
|
12
|
+
*/
|
|
13
|
+
import type { ProfileId } from "@browsercore/profiles";
|
|
14
|
+
import type { GoldenCapture } from "../types.js";
|
|
15
|
+
import type { Fingerprint, ReferenceProvider, ReferenceProviderKind } from "./reference-types.js";
|
|
16
|
+
/** Options for the curl-impersonate provider. */
|
|
17
|
+
export interface CurlImpersonateOptions {
|
|
18
|
+
/** Path / name of the curl-impersonate binary. Default "curl-impersonate". */
|
|
19
|
+
readonly command: string;
|
|
20
|
+
/** Extra argv passed to the binary. */
|
|
21
|
+
readonly extraArgs?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* PRIMARY provider — shells out to curl-impersonate.
|
|
25
|
+
*
|
|
26
|
+
* `capture()` invokes the curl-impersonate binary against `url`, impersonating
|
|
27
|
+
* `profile`, and returns the raw bytes it observed on the wire. The binary
|
|
28
|
+
* must be on PATH (or in `command`); if it is missing, the call throws
|
|
29
|
+
* {@link ReferenceError} so the facade can fall back to the secondary.
|
|
30
|
+
*/
|
|
31
|
+
export declare class CurlImpersonateProvider implements ReferenceProvider {
|
|
32
|
+
readonly kind: ReferenceProviderKind;
|
|
33
|
+
readonly command: string;
|
|
34
|
+
readonly extraArgs: readonly string[];
|
|
35
|
+
constructor(options?: CurlImpersonateOptions);
|
|
36
|
+
availableProfiles(): ProfileId[];
|
|
37
|
+
capture(profile: ProfileId, url: string): Promise<GoldenCapture>;
|
|
38
|
+
fingerprint(profile: ProfileId): Promise<Fingerprint>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=curl-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curl-provider.d.ts","sourceRoot":"","sources":["../../src/reference/curl-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAIlG,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACnC,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAiCD;;;;;;;GAOG;AACH,qBAAa,uBAAwB,YAAW,iBAAiB;IAC7D,SAAgB,IAAI,EAAE,qBAAqB,CAAyC;IACpF,SAAgB,OAAO,EAAE,MAAM,CAAC;IAChC,SAAgB,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IAE7C,YAAY,OAAO,CAAC,EAAE,sBAAsB,EAG3C;IAED,iBAAiB,IAAI,SAAS,EAAE,CAU/B;IAEK,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA6BrE;IAEK,WAAW,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAG1D;CACJ"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRIMARY reference provider — shells out to curl-impersonate.
|
|
3
|
+
*
|
|
4
|
+
* `capture()` invokes the curl-impersonate binary against `url`, impersonating
|
|
5
|
+
* `profile`, and returns the raw bytes it observed on the wire. The binary
|
|
6
|
+
* must be on PATH (or in `command`); if it is missing, the call throws
|
|
7
|
+
* {@link ReferenceError} so the facade can fall back to the secondary.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `reference.ts` so the facade module stays focused on primary →
|
|
10
|
+
* secondary dispatch while this file owns the curl-impersonate subprocess
|
|
11
|
+
* wiring and dump-format parsing.
|
|
12
|
+
*/
|
|
13
|
+
import { execFile, } from "node:child_process";
|
|
14
|
+
import { ReferenceError } from "./reference-errors.js";
|
|
15
|
+
import { fingerprintFromTlsCapture, parseDumpOutput, profileToSource } from "./dump.js";
|
|
16
|
+
/**
|
|
17
|
+
* Run `command args...` and resolve its captured `{stdout, stderr}`.
|
|
18
|
+
*
|
|
19
|
+
* Wraps node:child_process `execFile` with a `Promise` rather than
|
|
20
|
+
* `util.promisify` so the types resolve without forcing casts — `execFile`'s
|
|
21
|
+
* overloads return `ChildProcess`, which `promisify` cannot infer a useful
|
|
22
|
+
* promise type from. Rejects with an `ExecFileException`-shaped `Error` on
|
|
23
|
+
* non-zero exit, timeout, or spawn failure.
|
|
24
|
+
*/
|
|
25
|
+
function runExecFile(command, args, options) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
execFile(command, args, options, (err, stdout, stderr) => {
|
|
28
|
+
if (err !== null) {
|
|
29
|
+
reject(err);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resolve({ stdout, stderr });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* PRIMARY provider — shells out to curl-impersonate.
|
|
38
|
+
*
|
|
39
|
+
* `capture()` invokes the curl-impersonate binary against `url`, impersonating
|
|
40
|
+
* `profile`, and returns the raw bytes it observed on the wire. The binary
|
|
41
|
+
* must be on PATH (or in `command`); if it is missing, the call throws
|
|
42
|
+
* {@link ReferenceError} so the facade can fall back to the secondary.
|
|
43
|
+
*/
|
|
44
|
+
export class CurlImpersonateProvider {
|
|
45
|
+
kind = { kind: "curl-impersonate" };
|
|
46
|
+
command;
|
|
47
|
+
extraArgs;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.command = options?.command ?? "curl-impersonate";
|
|
50
|
+
this.extraArgs = options?.extraArgs ?? [];
|
|
51
|
+
}
|
|
52
|
+
availableProfiles() {
|
|
53
|
+
// curl-impersonate ships these browser impersonation targets.
|
|
54
|
+
return [
|
|
55
|
+
"chrome-140",
|
|
56
|
+
"chrome-139",
|
|
57
|
+
"firefox-135",
|
|
58
|
+
"firefox-128",
|
|
59
|
+
"safari-18",
|
|
60
|
+
"edge-140",
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
async capture(profile, url) {
|
|
64
|
+
const profileFlag = `--${String(profile)}`;
|
|
65
|
+
const args = [profileFlag, "--dump-traffic", ...this.extraArgs, url];
|
|
66
|
+
let stdout;
|
|
67
|
+
let stderr;
|
|
68
|
+
try {
|
|
69
|
+
const out = await runExecFile(this.command, args, {
|
|
70
|
+
timeout: 30_000,
|
|
71
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
});
|
|
74
|
+
stdout = out.stdout;
|
|
75
|
+
stderr = out.stderr;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
const cause = e instanceof Error ? e : new Error(String(e));
|
|
79
|
+
throw new ReferenceError(`curl-impersonate capture for ${String(profile)} failed: ${cause.message}`, { cause });
|
|
80
|
+
}
|
|
81
|
+
void stderr;
|
|
82
|
+
const bytes = parseDumpOutput(stdout);
|
|
83
|
+
return {
|
|
84
|
+
id: `${profile}/tls/client_hello`,
|
|
85
|
+
source: profileToSource(profile),
|
|
86
|
+
protocol: "tls",
|
|
87
|
+
bytes,
|
|
88
|
+
description: `curl-impersonate capture for ${String(profile)}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async fingerprint(profile) {
|
|
92
|
+
const capture = await this.capture(profile, "https://example.com");
|
|
93
|
+
return fingerprintFromTlsCapture(capture);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=curl-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"curl-provider.js","sourceRoot":"","sources":["../../src/reference/curl-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACH,QAAQ,GAEX,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,yBAAyB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAgBxF;;;;;;;;GAQG;AACH,SAAS,WAAW,CAChB,OAAe,EACf,IAAuB,EACvB,OAA0C;IAE1C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACnC,QAAQ,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,GAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACnE,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBACf,MAAM,CAAC,GAAG,CAAC,CAAC;gBACZ,OAAO;YACX,CAAC;YACD,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;QAChC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,uBAAuB;IAChB,IAAI,GAA0B,EAAE,IAAI,EAAE,kBAAkB,EAAW,CAAC;IACpE,OAAO,CAAS;IAChB,SAAS,CAAoB;IAE7C,YAAY,OAAgC;QACxC,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,kBAAkB,CAAC;QACtD,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;IAC9C,CAAC;IAED,iBAAiB;QACb,8DAA8D;QAC9D,OAAO;YACH,YAAyB;YACzB,YAAyB;YACzB,aAA0B;YAC1B,aAA0B;YAC1B,WAAwB;YACxB,UAAuB;SAC1B,CAAC;IACN,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAkB,EAAE,GAAW;QACzC,MAAM,WAAW,GAAG,KAAK,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,CAAC,WAAW,EAAE,gBAAgB,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACrE,IAAI,MAAc,CAAC;QACnB,IAAI,MAAc,CAAC;QACnB,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE;gBAC9C,OAAO,EAAE,MAAM;gBACf,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;gBAC3B,QAAQ,EAAE,MAAM;aACnB,CAAC,CAAC;YACH,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;YACpB,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,MAAM,KAAK,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5D,MAAM,IAAI,cAAc,CACpB,gCAAgC,MAAM,CAAC,OAAO,CAAC,YAAY,KAAK,CAAC,OAAO,EAAE,EAC1E,EAAE,KAAK,EAAE,CACZ,CAAC;QACN,CAAC;QACD,KAAK,MAAM,CAAC;QACZ,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACtC,OAAO;YACH,EAAE,EAAE,GAAG,OAAO,mBAA0C;YACxD,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC;YAChC,QAAQ,EAAE,KAAK;YACf,KAAK;YACL,WAAW,EAAE,gCAAgC,MAAM,CAAC,OAAO,CAAC,EAAE;SACjE,CAAC;IACN,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAkB;QAChC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,qBAAqB,CAAC,CAAC;QACnE,OAAO,yBAAyB,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;CACJ"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* curl-impersonate dump parsing + TLS fingerprint derivation.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `reference.ts` so the providers and facade stay focused on
|
|
5
|
+
* dispatch while this file owns the curl-impersonate `--dump-traffic` byte
|
|
6
|
+
* format and the JA3/JA4 derivation from a captured ClientHello.
|
|
7
|
+
*/
|
|
8
|
+
import type { ProfileId } from "@browsercore/profiles";
|
|
9
|
+
import type { GoldenCapture } from "../types.js";
|
|
10
|
+
import type { Fingerprint } from "./reference-types.js";
|
|
11
|
+
/** Map a {@link ProfileId} to its {@link CaptureSource} tag. */
|
|
12
|
+
export declare function profileToSource(profile: ProfileId): GoldenCapture["source"];
|
|
13
|
+
/**
|
|
14
|
+
* Parse curl-impersonate `--dump-traffic` output into raw bytes.
|
|
15
|
+
*
|
|
16
|
+
* The dump format is a hex dump with one line per record; we extract the hex
|
|
17
|
+
* payload after the `>>> traffic <<<` marker. Throws {@link DumpParseError}
|
|
18
|
+
* when no hex bytes follow the marker or the hex has odd length.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseDumpOutput(stdout: string): Uint8Array;
|
|
21
|
+
/**
|
|
22
|
+
* Derive a {@link Fingerprint} from a TLS ClientHello capture.
|
|
23
|
+
*
|
|
24
|
+
* Computes JA3 + JA4 from the raw bytes and reads the per-extension metadata
|
|
25
|
+
* (supported groups, signature algorithms, ALPN) from the sidecar
|
|
26
|
+
* `.meta.json` when available.
|
|
27
|
+
*
|
|
28
|
+
* Exported for unit testing the try/catch branches: the happy path reads the
|
|
29
|
+
* sidecar meta, while the fallback path is taken when the sidecar is missing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fingerprintFromTlsCapture(capture: GoldenCapture): Fingerprint;
|
|
32
|
+
/**
|
|
33
|
+
* Extract a human-readable cipher-suite name from a JA4 tag.
|
|
34
|
+
*
|
|
35
|
+
* JA4 doesn't directly encode the negotiated cipher; this is a placeholder
|
|
36
|
+
* that returns the JA4_a segment for inspection. Real cipher-suite resolution
|
|
37
|
+
* requires parsing the ServerHello, which is out of scope for the capture.
|
|
38
|
+
*
|
|
39
|
+
* Exported for unit testing the empty-tag fallback branch.
|
|
40
|
+
*/
|
|
41
|
+
export declare function cipherSuiteName(ja4Tag: string): string;
|
|
42
|
+
//# sourceMappingURL=dump.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dump.d.ts","sourceRoot":"","sources":["../../src/reference/dump.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,aAAa,CAAC;AAG9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGxD,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,GAAG,aAAa,CAAC,QAAQ,CAAC,CAY3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAiB1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,CA6B7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtD"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* curl-impersonate dump parsing + TLS fingerprint derivation.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `reference.ts` so the providers and facade stay focused on
|
|
5
|
+
* dispatch while this file owns the curl-impersonate `--dump-traffic` byte
|
|
6
|
+
* format and the JA3/JA4 derivation from a captured ClientHello.
|
|
7
|
+
*/
|
|
8
|
+
import { computeJa3, computeJa4Fingerprint } from "../fingerprint/index.js";
|
|
9
|
+
import { loadCaptureMeta } from "../golden/golden.js";
|
|
10
|
+
import { DumpParseError } from "./reference-errors.js";
|
|
11
|
+
/** Map a {@link ProfileId} to its {@link CaptureSource} tag. */
|
|
12
|
+
export function profileToSource(profile) {
|
|
13
|
+
const p = String(profile);
|
|
14
|
+
if (p.startsWith("firefox")) {
|
|
15
|
+
return "firefox-135";
|
|
16
|
+
}
|
|
17
|
+
if (p.startsWith("safari")) {
|
|
18
|
+
return "safari-18";
|
|
19
|
+
}
|
|
20
|
+
if (p.startsWith("edge")) {
|
|
21
|
+
return "edge-140";
|
|
22
|
+
}
|
|
23
|
+
return "chrome-140";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse curl-impersonate `--dump-traffic` output into raw bytes.
|
|
27
|
+
*
|
|
28
|
+
* The dump format is a hex dump with one line per record; we extract the hex
|
|
29
|
+
* payload after the `>>> traffic <<<` marker. Throws {@link DumpParseError}
|
|
30
|
+
* when no hex bytes follow the marker or the hex has odd length.
|
|
31
|
+
*/
|
|
32
|
+
export function parseDumpOutput(stdout) {
|
|
33
|
+
// Find the hex body — everything after the ">>> traffic <<<" marker.
|
|
34
|
+
const marker = ">>> traffic <<<";
|
|
35
|
+
const idx = stdout.indexOf(marker);
|
|
36
|
+
const body = idx === -1 ? stdout : stdout.slice(idx + marker.length);
|
|
37
|
+
const hex = body.replaceAll(/[^0-9a-fA-F]/gu, "");
|
|
38
|
+
if (hex.length === 0) {
|
|
39
|
+
throw new DumpParseError("no_hex", 0);
|
|
40
|
+
}
|
|
41
|
+
if (hex.length % 2 !== 0) {
|
|
42
|
+
throw new DumpParseError("odd_length", hex.length);
|
|
43
|
+
}
|
|
44
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
45
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
46
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
47
|
+
}
|
|
48
|
+
return bytes;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Derive a {@link Fingerprint} from a TLS ClientHello capture.
|
|
52
|
+
*
|
|
53
|
+
* Computes JA3 + JA4 from the raw bytes and reads the per-extension metadata
|
|
54
|
+
* (supported groups, signature algorithms, ALPN) from the sidecar
|
|
55
|
+
* `.meta.json` when available.
|
|
56
|
+
*
|
|
57
|
+
* Exported for unit testing the try/catch branches: the happy path reads the
|
|
58
|
+
* sidecar meta, while the fallback path is taken when the sidecar is missing.
|
|
59
|
+
*/
|
|
60
|
+
export function fingerprintFromTlsCapture(capture) {
|
|
61
|
+
const ja3 = computeJa3(capture.bytes);
|
|
62
|
+
const ja4 = computeJa4Fingerprint(capture.bytes);
|
|
63
|
+
// Read the sidecar meta for richer fields (signature algorithms, ALPN,
|
|
64
|
+
// supported curves). Fall back to empty arrays if missing.
|
|
65
|
+
const alpn = [];
|
|
66
|
+
const signatureAlgorithms = [];
|
|
67
|
+
const ellipticCurves = [];
|
|
68
|
+
try {
|
|
69
|
+
const meta = loadCaptureMeta(capture.id);
|
|
70
|
+
if (meta.protocol === "tls") {
|
|
71
|
+
// CaptureMeta doesn't carry ALPN/sigAlgs/curves yet; this is a
|
|
72
|
+
// placeholder for when the sidecar schema is extended.
|
|
73
|
+
void meta;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Sidecar missing or unparseable — leave richer fields empty.
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ja3,
|
|
81
|
+
ja4: ja4.tag,
|
|
82
|
+
alpn,
|
|
83
|
+
cipherSuite: cipherSuiteName(ja4.tag),
|
|
84
|
+
protocolVersion: ja4.tag.slice(5, 7) || "unknown",
|
|
85
|
+
signatureAlgorithms,
|
|
86
|
+
ellipticCurves,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract a human-readable cipher-suite name from a JA4 tag.
|
|
91
|
+
*
|
|
92
|
+
* JA4 doesn't directly encode the negotiated cipher; this is a placeholder
|
|
93
|
+
* that returns the JA4_a segment for inspection. Real cipher-suite resolution
|
|
94
|
+
* requires parsing the ServerHello, which is out of scope for the capture.
|
|
95
|
+
*
|
|
96
|
+
* Exported for unit testing the empty-tag fallback branch.
|
|
97
|
+
*/
|
|
98
|
+
export function cipherSuiteName(ja4Tag) {
|
|
99
|
+
const a = ja4Tag.split("_")[0] ?? "";
|
|
100
|
+
return a.length > 0 ? a : "unknown";
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=dump.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dump.js","sourceRoot":"","sources":["../../src/reference/dump.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Node-backed CryptoProvider double.
|
|
3
|
+
*
|
|
4
|
+
* @browsercore/crypto 0.2.2 exports only types, errors, and utilities — the
|
|
5
|
+
* runtime NodeCryptoProvider implementation lives in
|
|
6
|
+
* `browsersmith/src/platform/crypto/node/`. This repo (Layer 4) cannot depend
|
|
7
|
+
* on browsersmith (Layer 5), so we provide a local Node-backed implementation
|
|
8
|
+
* of the `CryptoProvider` contract for tests and e2e harnesses.
|
|
9
|
+
*
|
|
10
|
+
* This double mirrors the production NodeCryptoProvider behavior:
|
|
11
|
+
* - Hashing via node:crypto (sha256, sha384)
|
|
12
|
+
* - HKDF via node:crypto.hkdfSync
|
|
13
|
+
* - HMAC via node:crypto.createHmac
|
|
14
|
+
* - AEAD via node:crypto.createCipheriv/createDecipheriv (GCM, CCM, ChaCha20)
|
|
15
|
+
* - X25519 via NobleX25519Backend (pure JS, no native binding)
|
|
16
|
+
* - ECDH via node:crypto.ECDH
|
|
17
|
+
* - Signatures via node:crypto.createVerify
|
|
18
|
+
* - Random bytes via node:crypto.randomBytes
|
|
19
|
+
*
|
|
20
|
+
* The production provider uses the same primitives — this double exists only
|
|
21
|
+
* because the package boundary moved the implementation to a higher layer.
|
|
22
|
+
*/
|
|
23
|
+
import type { CryptoProvider } from "@browsercore/crypto";
|
|
24
|
+
/**
|
|
25
|
+
* Node-backed CryptoProvider implementation.
|
|
26
|
+
*
|
|
27
|
+
* Satisfies the full `CryptoProvider` contract using node:crypto + noble-curves.
|
|
28
|
+
* Safe for tests and e2e harnesses — never imported by production protocol code.
|
|
29
|
+
*/
|
|
30
|
+
export declare const nodeCryptoProvider: CryptoProvider;
|
|
31
|
+
//# sourceMappingURL=node-crypto-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-crypto-provider.d.ts","sourceRoot":"","sources":["../../src/reference/node-crypto-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,KAAK,EACR,cAAc,EAKjB,MAAM,qBAAqB,CAAC;AAgC7B;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAsJhC,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Node-backed CryptoProvider double.
|
|
3
|
+
*
|
|
4
|
+
* @browsercore/crypto 0.2.2 exports only types, errors, and utilities — the
|
|
5
|
+
* runtime NodeCryptoProvider implementation lives in
|
|
6
|
+
* `browsersmith/src/platform/crypto/node/`. This repo (Layer 4) cannot depend
|
|
7
|
+
* on browsersmith (Layer 5), so we provide a local Node-backed implementation
|
|
8
|
+
* of the `CryptoProvider` contract for tests and e2e harnesses.
|
|
9
|
+
*
|
|
10
|
+
* This double mirrors the production NodeCryptoProvider behavior:
|
|
11
|
+
* - Hashing via node:crypto (sha256, sha384)
|
|
12
|
+
* - HKDF via node:crypto.hkdfSync
|
|
13
|
+
* - HMAC via node:crypto.createHmac
|
|
14
|
+
* - AEAD via node:crypto.createCipheriv/createDecipheriv (GCM, CCM, ChaCha20)
|
|
15
|
+
* - X25519 via NobleX25519Backend (pure JS, no native binding)
|
|
16
|
+
* - ECDH via node:crypto.ECDH
|
|
17
|
+
* - Signatures via node:crypto.createVerify
|
|
18
|
+
* - Random bytes via node:crypto.randomBytes
|
|
19
|
+
*
|
|
20
|
+
* The production provider uses the same primitives — this double exists only
|
|
21
|
+
* because the package boundary moved the implementation to a higher layer.
|
|
22
|
+
*/
|
|
23
|
+
import { createCipheriv, createDecipheriv, createECDH, createHash, createHmac, createVerify, hkdfSync, randomBytes, } from "node:crypto";
|
|
24
|
+
import { NobleX25519Backend } from "@browsercore/crypto";
|
|
25
|
+
const noble = new NobleX25519Backend();
|
|
26
|
+
/** Curve → node:crypto named curve + expected coordinate length. */
|
|
27
|
+
const ECDH_CURVE_PARAMS = {
|
|
28
|
+
secp256r1: { nodeCurve: "prime256v1", coordLen: 32 },
|
|
29
|
+
secp384r1: { nodeCurve: "secp384r1", coordLen: 48 },
|
|
30
|
+
};
|
|
31
|
+
/** Map our HashId branded union to node:crypto's hash algorithm name. */
|
|
32
|
+
function hashName(hash) {
|
|
33
|
+
return hash === "SHA-256" ? "sha256" : "sha384";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Convert a Uint8Array to a node Buffer for cipher APIs.
|
|
37
|
+
* Newer @types/node BinaryLike excludes bare ArrayBuffer — Buffer is always safe.
|
|
38
|
+
*/
|
|
39
|
+
function toBuf(data) {
|
|
40
|
+
return Buffer.from(data);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Map a 12-byte AEAD nonce to a node:crypto IV (Buffer).
|
|
44
|
+
* Used uniformly by every AEAD path so the conversion happens in one place.
|
|
45
|
+
*/
|
|
46
|
+
function toIv(nonce) {
|
|
47
|
+
return Buffer.from(nonce);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Node-backed CryptoProvider implementation.
|
|
51
|
+
*
|
|
52
|
+
* Satisfies the full `CryptoProvider` contract using node:crypto + noble-curves.
|
|
53
|
+
* Safe for tests and e2e harnesses — never imported by production protocol code.
|
|
54
|
+
*/
|
|
55
|
+
export const nodeCryptoProvider = {
|
|
56
|
+
randomBytes(length) {
|
|
57
|
+
return new Uint8Array(randomBytes(length));
|
|
58
|
+
},
|
|
59
|
+
sha256(data) {
|
|
60
|
+
return new Uint8Array(createHash("sha256").update(toBuf(data)).digest());
|
|
61
|
+
},
|
|
62
|
+
sha384(data) {
|
|
63
|
+
return new Uint8Array(createHash("sha384").update(toBuf(data)).digest());
|
|
64
|
+
},
|
|
65
|
+
hkdf(hash, salt, ikm, info, length) {
|
|
66
|
+
return new Uint8Array(hkdfSync(hashName(hash), toBuf(ikm), toBuf(salt), toBuf(info), length));
|
|
67
|
+
},
|
|
68
|
+
hmac(hash, key, data) {
|
|
69
|
+
return new Uint8Array(createHmac(hashName(hash), toBuf(key)).update(toBuf(data)).digest());
|
|
70
|
+
},
|
|
71
|
+
aes128GcmEncrypt(key, nonce, plaintext, aad) {
|
|
72
|
+
const cipher = createCipheriv("aes-128-gcm", toBuf(key), toIv(nonce));
|
|
73
|
+
cipher.setAAD(toBuf(aad));
|
|
74
|
+
const ciphertext = Buffer.concat([cipher.update(toBuf(plaintext)), cipher.final()]);
|
|
75
|
+
const tag = cipher.getAuthTag();
|
|
76
|
+
return new Uint8Array(Buffer.concat([ciphertext, tag]));
|
|
77
|
+
},
|
|
78
|
+
aes128GcmDecrypt(key, nonce, ciphertext, aad) {
|
|
79
|
+
const tagLen = 16;
|
|
80
|
+
const enc = ciphertext.subarray(0, ciphertext.length - tagLen);
|
|
81
|
+
const tag = ciphertext.subarray(ciphertext.length - tagLen);
|
|
82
|
+
const decipher = createDecipheriv("aes-128-gcm", toBuf(key), toIv(nonce));
|
|
83
|
+
decipher.setAuthTag(toBuf(tag));
|
|
84
|
+
decipher.setAAD(toBuf(aad));
|
|
85
|
+
return new Uint8Array(Buffer.concat([decipher.update(toBuf(enc)), decipher.final()]));
|
|
86
|
+
},
|
|
87
|
+
aes256GcmEncrypt(key, nonce, plaintext, aad) {
|
|
88
|
+
const cipher = createCipheriv("aes-256-gcm", toBuf(key), toIv(nonce));
|
|
89
|
+
cipher.setAAD(toBuf(aad));
|
|
90
|
+
const ciphertext = Buffer.concat([cipher.update(toBuf(plaintext)), cipher.final()]);
|
|
91
|
+
const tag = cipher.getAuthTag();
|
|
92
|
+
return new Uint8Array(Buffer.concat([ciphertext, tag]));
|
|
93
|
+
},
|
|
94
|
+
aes256GcmDecrypt(key, nonce, ciphertext, aad) {
|
|
95
|
+
const tagLen = 16;
|
|
96
|
+
const enc = ciphertext.subarray(0, ciphertext.length - tagLen);
|
|
97
|
+
const tag = ciphertext.subarray(ciphertext.length - tagLen);
|
|
98
|
+
const decipher = createDecipheriv("aes-256-gcm", toBuf(key), toIv(nonce));
|
|
99
|
+
decipher.setAuthTag(toBuf(tag));
|
|
100
|
+
decipher.setAAD(toBuf(aad));
|
|
101
|
+
return new Uint8Array(Buffer.concat([decipher.update(toBuf(enc)), decipher.final()]));
|
|
102
|
+
},
|
|
103
|
+
aes128CcmEncrypt(key, nonce, plaintext, aad) {
|
|
104
|
+
const cipher = createCipheriv("aes-128-ccm", toBuf(key), toIv(nonce), {
|
|
105
|
+
authTagLength: 16,
|
|
106
|
+
});
|
|
107
|
+
cipher.setAAD(toBuf(aad), { plaintextLength: plaintext.length });
|
|
108
|
+
const ciphertext = Buffer.concat([cipher.update(toBuf(plaintext)), cipher.final()]);
|
|
109
|
+
const tag = cipher.getAuthTag();
|
|
110
|
+
return new Uint8Array(Buffer.concat([ciphertext, tag]));
|
|
111
|
+
},
|
|
112
|
+
aes128CcmDecrypt(key, nonce, ciphertext, aad) {
|
|
113
|
+
const tagLen = 16;
|
|
114
|
+
const enc = ciphertext.subarray(0, ciphertext.length - tagLen);
|
|
115
|
+
const tag = ciphertext.subarray(ciphertext.length - tagLen);
|
|
116
|
+
const decipher = createDecipheriv("aes-128-ccm", toBuf(key), toIv(nonce), {
|
|
117
|
+
authTagLength: 16,
|
|
118
|
+
});
|
|
119
|
+
decipher.setAuthTag(toBuf(tag));
|
|
120
|
+
decipher.setAAD(toBuf(aad), { plaintextLength: enc.length });
|
|
121
|
+
return new Uint8Array(Buffer.concat([decipher.update(toBuf(enc)), decipher.final()]));
|
|
122
|
+
},
|
|
123
|
+
chacha20Poly1305Encrypt(key, nonce, plaintext, aad) {
|
|
124
|
+
const cipher = createCipheriv("chacha20-poly1305", toBuf(key), toIv(nonce), {
|
|
125
|
+
authTagLength: 16,
|
|
126
|
+
});
|
|
127
|
+
cipher.setAAD(toBuf(aad));
|
|
128
|
+
const ciphertext = Buffer.concat([cipher.update(toBuf(plaintext)), cipher.final()]);
|
|
129
|
+
const tag = cipher.getAuthTag();
|
|
130
|
+
return new Uint8Array(Buffer.concat([ciphertext, tag]));
|
|
131
|
+
},
|
|
132
|
+
chacha20Poly1305Decrypt(key, nonce, ciphertext, aad) {
|
|
133
|
+
const tagLen = 16;
|
|
134
|
+
const enc = ciphertext.subarray(0, ciphertext.length - tagLen);
|
|
135
|
+
const tag = ciphertext.subarray(ciphertext.length - tagLen);
|
|
136
|
+
const decipher = createDecipheriv("chacha20-poly1305", toBuf(key), toIv(nonce), {
|
|
137
|
+
authTagLength: 16,
|
|
138
|
+
});
|
|
139
|
+
decipher.setAuthTag(toBuf(tag));
|
|
140
|
+
decipher.setAAD(toBuf(aad));
|
|
141
|
+
return new Uint8Array(Buffer.concat([decipher.update(toBuf(enc)), decipher.final()]));
|
|
142
|
+
},
|
|
143
|
+
x25519GenerateKeyPair() {
|
|
144
|
+
const secretKey = this.randomBytes(32);
|
|
145
|
+
const publicKey = noble.publicKey(secretKey);
|
|
146
|
+
return { publicKey, secretKey };
|
|
147
|
+
},
|
|
148
|
+
x25519SharedSecret(secretKey, peerPublicKey) {
|
|
149
|
+
return noble.sharedSecret(secretKey, peerPublicKey);
|
|
150
|
+
},
|
|
151
|
+
ecdhGenerateKeyPair(curve) {
|
|
152
|
+
const params = ECDH_CURVE_PARAMS[curve];
|
|
153
|
+
const ecdh = createECDH(params.nodeCurve);
|
|
154
|
+
ecdh.generateKeys();
|
|
155
|
+
const publicKey = ecdh.getPublicKey(null, "uncompressed");
|
|
156
|
+
const secretKey = ecdh.getPrivateKey();
|
|
157
|
+
return {
|
|
158
|
+
curve,
|
|
159
|
+
publicKey: new Uint8Array(publicKey),
|
|
160
|
+
secretKey: new Uint8Array(secretKey),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
ecdhSharedSecret(curve, secretKey, peerPublicKey) {
|
|
164
|
+
const params = ECDH_CURVE_PARAMS[curve];
|
|
165
|
+
const ecdh = createECDH(params.nodeCurve);
|
|
166
|
+
ecdh.setPrivateKey(toBuf(secretKey));
|
|
167
|
+
const shared = ecdh.computeSecret(toBuf(peerPublicKey));
|
|
168
|
+
return new Uint8Array(shared.subarray(shared.length - params.coordLen));
|
|
169
|
+
},
|
|
170
|
+
verifySignature(scheme, publicKey, signature, data) {
|
|
171
|
+
const verifier = createVerify(verificationHash(scheme));
|
|
172
|
+
verifier.update(toBuf(data));
|
|
173
|
+
return verifier.verify({ key: toBuf(publicKey), format: "der", type: "spki" }, toBuf(signature));
|
|
174
|
+
},
|
|
175
|
+
aesEcbEncrypt(key, block) {
|
|
176
|
+
const cipher = createCipheriv(key.length === 16 ? "aes-128-ecb" : "aes-256-ecb", toBuf(key), null);
|
|
177
|
+
cipher.setAutoPadding(false);
|
|
178
|
+
return new Uint8Array(cipher.update(toBuf(block)));
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Map a TLS signature-scheme name to a node:crypto verification hash.
|
|
183
|
+
* Only the schemes the test suite exercises are covered.
|
|
184
|
+
*/
|
|
185
|
+
function verificationHash(scheme) {
|
|
186
|
+
switch (scheme) {
|
|
187
|
+
case "ecdsa_secp256r1_sha256":
|
|
188
|
+
case "rsa_pss_rsae_sha256":
|
|
189
|
+
case "rsa_pkcs1_sha256":
|
|
190
|
+
return "sha256";
|
|
191
|
+
case "ecdsa_secp384r1_sha384":
|
|
192
|
+
case "rsa_pss_rsae_sha384":
|
|
193
|
+
case "rsa_pkcs1_sha384":
|
|
194
|
+
return "sha384";
|
|
195
|
+
default:
|
|
196
|
+
return "sha256";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=node-crypto-provider.js.map
|