@prosopo/fingerprint 2.7.57 → 2.7.59
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/.turbo/turbo-build$colon$cjs.log +2 -5
- package/.turbo/turbo-build$colon$tsc.log +10 -13
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/package.json +58 -58
- package/src/index.ts +0 -105
- package/src/merkle.ts +0 -123
- package/src/proof.test.ts +0 -119
- package/src/proof.ts +0 -182
- package/tsconfig.cjs.json +0 -21
- package/tsconfig.json +0 -28
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
package/src/proof.test.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { UnknownComponents } from "@prosopo/fingerprintjs";
|
|
16
|
-
import { describe, expect, it } from "vitest";
|
|
17
|
-
import {
|
|
18
|
-
type FingerprintProof,
|
|
19
|
-
buildFingerprintProofFromComponents,
|
|
20
|
-
verifyFingerprintProofStructure,
|
|
21
|
-
} from "./proof.js";
|
|
22
|
-
|
|
23
|
-
const sampleComponents = (): UnknownComponents => ({
|
|
24
|
-
colorDepth: { value: 24, duration: 1 },
|
|
25
|
-
cookiesEnabled: { value: true, duration: 1 },
|
|
26
|
-
hardwareConcurrency: { value: 8, duration: 1 },
|
|
27
|
-
timezone: { value: "UTC+1", duration: 2 },
|
|
28
|
-
canvas: {
|
|
29
|
-
value: { winding: true, geometry: "abc", text: "def" },
|
|
30
|
-
duration: 3,
|
|
31
|
-
},
|
|
32
|
-
languages: { value: [["en-GB"], ["en"]], duration: 1 },
|
|
33
|
-
broken: { error: new Error("source failed"), duration: 0 },
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const HEX_HASH = /^0x[0-9a-f]{64}$/;
|
|
37
|
-
|
|
38
|
-
const clone = (proof: FingerprintProof): FingerprintProof =>
|
|
39
|
-
JSON.parse(JSON.stringify(proof)) as FingerprintProof;
|
|
40
|
-
|
|
41
|
-
describe("fingerprint proof", () => {
|
|
42
|
-
it("commits to all components and discloses all by default", () => {
|
|
43
|
-
const proof = buildFingerprintProofFromComponents(sampleComponents());
|
|
44
|
-
expect(proof.root).toMatch(HEX_HASH);
|
|
45
|
-
expect(proof.disclosures).toHaveLength(7);
|
|
46
|
-
const keys = proof.disclosures.map((d) => d.key);
|
|
47
|
-
// disclosures follow the sorted commitment order
|
|
48
|
-
expect(keys).toStrictEqual([...keys].sort());
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("represents an errored source as an error leaf with no value", () => {
|
|
52
|
-
const proof = buildFingerprintProofFromComponents(sampleComponents());
|
|
53
|
-
const broken = proof.disclosures.find((d) => d.key === "broken");
|
|
54
|
-
expect(broken?.error).toBe(true);
|
|
55
|
-
expect(broken && "value" in broken).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("verifies a freshly built proof", () => {
|
|
59
|
-
const proof = buildFingerprintProofFromComponents(sampleComponents());
|
|
60
|
-
expect(verifyFingerprintProofStructure(proof)).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("verifies a selective disclosure of a subset of components", () => {
|
|
64
|
-
const proof = buildFingerprintProofFromComponents(sampleComponents(), [
|
|
65
|
-
"colorDepth",
|
|
66
|
-
"timezone",
|
|
67
|
-
]);
|
|
68
|
-
expect(proof.disclosures).toHaveLength(2);
|
|
69
|
-
expect(verifyFingerprintProofStructure(proof)).toBe(true);
|
|
70
|
-
// the root still commits to every component, not just the disclosed ones
|
|
71
|
-
const full = buildFingerprintProofFromComponents(sampleComponents());
|
|
72
|
-
expect(proof.root).toBe(full.root);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("skips requested keys that the fingerprint does not contain", () => {
|
|
76
|
-
const proof = buildFingerprintProofFromComponents(sampleComponents(), [
|
|
77
|
-
"colorDepth",
|
|
78
|
-
"doesNotExist",
|
|
79
|
-
]);
|
|
80
|
-
expect(proof.disclosures.map((d) => d.key)).toStrictEqual(["colorDepth"]);
|
|
81
|
-
expect(verifyFingerprintProofStructure(proof)).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("rejects a proof whose disclosed value was tampered with", () => {
|
|
85
|
-
const proof = clone(
|
|
86
|
-
buildFingerprintProofFromComponents(sampleComponents()),
|
|
87
|
-
);
|
|
88
|
-
const target = proof.disclosures.find((d) => d.key === "colorDepth");
|
|
89
|
-
if (target === undefined) {
|
|
90
|
-
throw new Error("expected colorDepth disclosure");
|
|
91
|
-
}
|
|
92
|
-
target.value = 32;
|
|
93
|
-
expect(verifyFingerprintProofStructure(proof)).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("rejects a proof whose root was tampered with", () => {
|
|
97
|
-
const proof = clone(
|
|
98
|
-
buildFingerprintProofFromComponents(sampleComponents()),
|
|
99
|
-
);
|
|
100
|
-
proof.root = `0x${"0".repeat(64)}`;
|
|
101
|
-
expect(verifyFingerprintProofStructure(proof)).toBe(false);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("rejects a disclosure carrying a non-sibling leaf's proof", () => {
|
|
105
|
-
const proof = clone(
|
|
106
|
-
buildFingerprintProofFromComponents(sampleComponents()),
|
|
107
|
-
);
|
|
108
|
-
// `broken` and `timezone` are the first and last leaves in sorted order, so
|
|
109
|
-
// they are not Merkle siblings — `broken`'s leaf does not appear anywhere in
|
|
110
|
-
// `timezone`'s proof, so lending it that proof must fail verification.
|
|
111
|
-
const broken = proof.disclosures.find((d) => d.key === "broken");
|
|
112
|
-
const timezone = proof.disclosures.find((d) => d.key === "timezone");
|
|
113
|
-
if (broken === undefined || timezone === undefined) {
|
|
114
|
-
throw new Error("expected broken and timezone disclosures");
|
|
115
|
-
}
|
|
116
|
-
broken.proof = timezone.proof;
|
|
117
|
-
expect(verifyFingerprintProofStructure(proof)).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
});
|
package/src/proof.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { Component, UnknownComponents } from "@prosopo/fingerprintjs";
|
|
16
|
-
import type { MerkleProof } from "@prosopo/types";
|
|
17
|
-
import { hexHash } from "@prosopo/util-crypto";
|
|
18
|
-
import {
|
|
19
|
-
buildMerkleLayers,
|
|
20
|
-
merkleProofFromLayers,
|
|
21
|
-
merkleRootFromLayers,
|
|
22
|
-
verifyMerkleProof,
|
|
23
|
-
} from "./merkle.js";
|
|
24
|
-
|
|
25
|
-
export const FINGERPRINT_PROOF_VERSION = 1;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The fixed set of components the client discloses in a proof of fingerprint.
|
|
29
|
-
* Disclosing every component (the previous default) made the proof enormous —
|
|
30
|
-
* each disclosure carries a full Merkle path and its raw value, and components
|
|
31
|
-
* like webgl/audio/fonts have very large values. Selective disclosure keeps the
|
|
32
|
-
* commitment (`root`) over the whole component set while only these cheap,
|
|
33
|
-
* stable scalar components carry a path + value, shrinking the payload ~10x.
|
|
34
|
-
*
|
|
35
|
-
* The provider-side validator's bounds MUST be a subset of these keys: a bounded
|
|
36
|
-
* key that isn't disclosed would always fail validation as `missing:<key>`.
|
|
37
|
-
*/
|
|
38
|
-
export const FINGERPRINT_DISCLOSURE_KEYS: readonly string[] = [
|
|
39
|
-
"colorDepth",
|
|
40
|
-
"cookiesEnabled",
|
|
41
|
-
"sessionStorage",
|
|
42
|
-
"localStorage",
|
|
43
|
-
"timezone",
|
|
44
|
-
"hardwareConcurrency",
|
|
45
|
-
"languages",
|
|
46
|
-
"architecture",
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* A single disclosed fingerprint component: its raw value (or an error flag)
|
|
51
|
-
* together with the Merkle proof that it belongs to the committed root. The
|
|
52
|
-
* value is left as `unknown` because each FingerprintJS source produces a
|
|
53
|
-
* different shape (string / number / boolean / array / object); the proof
|
|
54
|
-
* validator is responsible for bounding it.
|
|
55
|
-
*/
|
|
56
|
-
export interface FingerprintLeafDisclosure {
|
|
57
|
-
key: string;
|
|
58
|
-
value?: unknown;
|
|
59
|
-
error?: boolean;
|
|
60
|
-
proof: MerkleProof;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* A proof of fingerprint: a commitment (`root`) to the whole, deterministically
|
|
65
|
-
* ordered set of FingerprintJS components, plus a selective disclosure of some
|
|
66
|
-
* of those components. It lets a verifier check that the disclosed values were
|
|
67
|
-
* part of one coherent, genuinely-constructed fingerprint without the client
|
|
68
|
-
* being able to mix-and-match values across requests.
|
|
69
|
-
*/
|
|
70
|
-
export interface FingerprintProof {
|
|
71
|
-
v: number;
|
|
72
|
-
root: string;
|
|
73
|
-
disclosures: FingerprintLeafDisclosure[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Escape a component key exactly as FingerprintJS does in its canonical string,
|
|
77
|
-
// so a leaf can never be forged by shifting a `:`/`|` between key and value.
|
|
78
|
-
const escapeKey = (key: string): string => key.replace(/([:|\\])/g, "\\$1");
|
|
79
|
-
|
|
80
|
-
// Canonical per-component leaf content. Mirrors one segment of FingerprintJS'
|
|
81
|
-
// `componentsToCanonicalString`, so the leaf commits to exactly the value the
|
|
82
|
-
// visitorId hash is derived from.
|
|
83
|
-
const componentLeafContent = (
|
|
84
|
-
key: string,
|
|
85
|
-
component: Component<unknown>,
|
|
86
|
-
): string => {
|
|
87
|
-
const serialized =
|
|
88
|
-
"error" in component ? "error" : JSON.stringify(component.value);
|
|
89
|
-
return `${escapeKey(key)}:${serialized}`;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const disclosureLeafContent = (
|
|
93
|
-
disclosure: FingerprintLeafDisclosure,
|
|
94
|
-
): string => {
|
|
95
|
-
const serialized = disclosure.error
|
|
96
|
-
? "error"
|
|
97
|
-
: JSON.stringify(disclosure.value);
|
|
98
|
-
return `${escapeKey(disclosure.key)}:${serialized}`;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export const leafHashForContent = (content: string): string => hexHash(content);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Build a {@link FingerprintProof} from an already-resolved FingerprintJS
|
|
105
|
-
* components map. The commitment always covers every component (in sorted key
|
|
106
|
-
* order); `discloseKeys` selects which components are revealed (defaulting to
|
|
107
|
-
* all). Keys that are not present in the fingerprint are skipped — a validator
|
|
108
|
-
* should treat an expected-but-absent disclosure as a failure.
|
|
109
|
-
*/
|
|
110
|
-
export const buildFingerprintProofFromComponents = (
|
|
111
|
-
components: UnknownComponents,
|
|
112
|
-
discloseKeys?: readonly string[],
|
|
113
|
-
): FingerprintProof => {
|
|
114
|
-
const keys = Object.keys(components).sort();
|
|
115
|
-
if (keys.length === 0) {
|
|
116
|
-
throw new Error("fingerprint proof: no components to commit to");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const leafHashes = keys.map((key) => {
|
|
120
|
-
const component = components[key];
|
|
121
|
-
if (component === undefined) {
|
|
122
|
-
throw new Error(`fingerprint proof: missing component ${key}`);
|
|
123
|
-
}
|
|
124
|
-
return leafHashForContent(componentLeafContent(key, component));
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const layers = buildMerkleLayers(leafHashes);
|
|
128
|
-
const root = merkleRootFromLayers(layers);
|
|
129
|
-
|
|
130
|
-
const disclose = discloseKeys ?? keys;
|
|
131
|
-
const disclosures: FingerprintLeafDisclosure[] = [];
|
|
132
|
-
for (const key of disclose) {
|
|
133
|
-
const index = keys.indexOf(key);
|
|
134
|
-
if (index === -1) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const component = components[key];
|
|
138
|
-
const leafHash = leafHashes[index];
|
|
139
|
-
if (component === undefined || leafHash === undefined) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const proof = merkleProofFromLayers(layers, leafHash);
|
|
143
|
-
disclosures.push(
|
|
144
|
-
"error" in component
|
|
145
|
-
? { key, error: true, proof }
|
|
146
|
-
: { key, value: component.value, proof },
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { v: FINGERPRINT_PROOF_VERSION, root, disclosures };
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Structural verification of a proof: every disclosed `(key, value)` re-hashes
|
|
155
|
-
* to a leaf that is proven to belong to the single committed `root`. This is
|
|
156
|
-
* the non-secret half of validation — the value-bounding rules live in the
|
|
157
|
-
* provider-side (closed-source) validator and are layered on top of this.
|
|
158
|
-
*/
|
|
159
|
-
export const verifyFingerprintProofStructure = (
|
|
160
|
-
proof: FingerprintProof,
|
|
161
|
-
): boolean => {
|
|
162
|
-
if (proof.v !== FINGERPRINT_PROOF_VERSION) {
|
|
163
|
-
return false;
|
|
164
|
-
}
|
|
165
|
-
if (typeof proof.root !== "string" || proof.disclosures.length === 0) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
for (const disclosure of proof.disclosures) {
|
|
169
|
-
const leafHash = leafHashForContent(disclosureLeafContent(disclosure));
|
|
170
|
-
if (!verifyMerkleProof(leafHash, disclosure.proof)) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
const rootLayer = disclosure.proof[disclosure.proof.length - 1];
|
|
174
|
-
if (rootLayer === undefined || rootLayer[0] !== proof.root) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return true;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
export const encodeFingerprintProof = (proof: FingerprintProof): string =>
|
|
182
|
-
JSON.stringify(proof);
|
package/tsconfig.cjs.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.cjs.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./dist/cjs"
|
|
6
|
-
},
|
|
7
|
-
"include": [
|
|
8
|
-
"./src/**/*.ts",
|
|
9
|
-
"./src/**/*.json",
|
|
10
|
-
"./src/**/*.d.ts",
|
|
11
|
-
"./src/**/*.tsx"
|
|
12
|
-
],
|
|
13
|
-
"references": [
|
|
14
|
-
{
|
|
15
|
-
"path": "../../dev/config/tsconfig.cjs.json"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"path": "../fingerprintjs"
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.esm.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./dist"
|
|
6
|
-
},
|
|
7
|
-
"include": [
|
|
8
|
-
"src",
|
|
9
|
-
"src/**/*.json",
|
|
10
|
-
"src/**/*.ts",
|
|
11
|
-
"src/**/*.tsx",
|
|
12
|
-
"src/**/*.d.ts"
|
|
13
|
-
],
|
|
14
|
-
"references": [
|
|
15
|
-
{
|
|
16
|
-
"path": "../../dev/config/tsconfig.json"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"path": "../fingerprintjs"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"path": "../types"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"path": "../util-crypto"
|
|
26
|
-
}
|
|
27
|
-
]
|
|
28
|
-
}
|