phasmid-profile-gen 0.1.0__tar.gz
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.
Potentially problematic release.
This version of phasmid-profile-gen might be problematic. Click here for more details.
- phasmid_profile_gen-0.1.0/.gitignore +9 -0
- phasmid_profile_gen-0.1.0/.pre-commit-config.yaml +8 -0
- phasmid_profile_gen-0.1.0/.python-version +1 -0
- phasmid_profile_gen-0.1.0/.ruff.toml +63 -0
- phasmid_profile_gen-0.1.0/CHANGELOG.md +16 -0
- phasmid_profile_gen-0.1.0/LICENSE +30 -0
- phasmid_profile_gen-0.1.0/PKG-INFO +203 -0
- phasmid_profile_gen-0.1.0/README.md +176 -0
- phasmid_profile_gen-0.1.0/pyproject.toml +67 -0
- phasmid_profile_gen-0.1.0/scripts/build_data.py +514 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/__init__.py +141 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/__main__.py +5 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/_data.py +25 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/captures.py +81 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/claims.py +188 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/cli.py +177 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/CLDR-LICENSE +41 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/LICENSES.md +18 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/captures.json.xz +0 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/dbip-city-lite.json +11 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/forge.json.xz +0 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/fpgen.json.xz +0 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/locale.json.xz +0 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/manifest.json +88 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/data/schema.json.xz +0 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/forge.py +101 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/generate.py +492 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/geoip.py +194 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/locales.py +125 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/location.py +217 -0
- phasmid_profile_gen-0.1.0/src/phasmid_profile_gen/surface.py +131 -0
- phasmid_profile_gen-0.1.0/tests/conftest.py +46 -0
- phasmid_profile_gen-0.1.0/tests/fixtures/geoip-rows.json +30 -0
- phasmid_profile_gen-0.1.0/tests/fixtures/phasmid-samples/gen-linux-1707x960-be27e761.json +781 -0
- phasmid_profile_gen-0.1.0/tests/fixtures/phasmid-samples/gen-linux-1920x1080-a493e899.json +784 -0
- phasmid_profile_gen-0.1.0/tests/fixtures/phasmid-samples/gen-linux-3072x1728-6e34c862.json +780 -0
- phasmid_profile_gen-0.1.0/tests/test_build_data.py +124 -0
- phasmid_profile_gen-0.1.0/tests/test_data.py +110 -0
- phasmid_profile_gen-0.1.0/tests/test_generate.py +336 -0
- phasmid_profile_gen-0.1.0/tests/test_location.py +331 -0
- phasmid_profile_gen-0.1.0/uv.lock +759 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Exclude a variety of commonly ignored directories.
|
|
2
|
+
exclude = [
|
|
3
|
+
".bzr",
|
|
4
|
+
".direnv",
|
|
5
|
+
".eggs",
|
|
6
|
+
".git",
|
|
7
|
+
".git-rewrite",
|
|
8
|
+
".hg",
|
|
9
|
+
".ipynb_checkpoints",
|
|
10
|
+
".mypy_cache",
|
|
11
|
+
".nox",
|
|
12
|
+
".pants.d",
|
|
13
|
+
".pyenv",
|
|
14
|
+
".pytest_cache",
|
|
15
|
+
".pytype",
|
|
16
|
+
".ruff_cache",
|
|
17
|
+
".svn",
|
|
18
|
+
".tox",
|
|
19
|
+
".venv",
|
|
20
|
+
".vscode",
|
|
21
|
+
"__pypackages__",
|
|
22
|
+
"_build",
|
|
23
|
+
"buck-out",
|
|
24
|
+
"build",
|
|
25
|
+
"dist",
|
|
26
|
+
"node_modules",
|
|
27
|
+
"site-packages",
|
|
28
|
+
"venv",
|
|
29
|
+
"*/migrations/*",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
line-length = 100
|
|
33
|
+
indent-width = 4
|
|
34
|
+
|
|
35
|
+
[lint]
|
|
36
|
+
select = [
|
|
37
|
+
"E", # pycodestyle errors
|
|
38
|
+
"W", # pycodestyle warnings
|
|
39
|
+
"F", # pyflakes
|
|
40
|
+
"I", # isort
|
|
41
|
+
"B", # flake8-bugbear
|
|
42
|
+
"C4", # flake8-comprehensions
|
|
43
|
+
"UP", # pyupgrade
|
|
44
|
+
]
|
|
45
|
+
ignore = [
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Per-file ignores for common patterns
|
|
49
|
+
[lint.per-file-ignores]
|
|
50
|
+
"**/tests/**" = [
|
|
51
|
+
"F841", # Local variable is assigned to but never used
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[format]
|
|
55
|
+
# Like Black, use double quotes for strings.
|
|
56
|
+
quote-style = "double"
|
|
57
|
+
|
|
58
|
+
# Like Black, indent with spaces, rather than tabs.
|
|
59
|
+
indent-style = "space"
|
|
60
|
+
|
|
61
|
+
# Like Black, respect magic trailing commas.
|
|
62
|
+
skip-magic-trailing-comma = false
|
|
63
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-05)
|
|
4
|
+
|
|
5
|
+
First release: Phasmid's `tools/genprofile` as a package, with its data bundled.
|
|
6
|
+
|
|
7
|
+
- Generates the same profiles as `tools/genprofile` from the same captures,
|
|
8
|
+
byte for byte except for the generator's name in `captured.note`.
|
|
9
|
+
- Data built from Phasmid at the commit in `data/manifest.json`, BrowserForge's
|
|
10
|
+
fingerprint network (`apify_fingerprint_datapoints` 0.15.0) and one
|
|
11
|
+
collector-app capture (Windows 10, Intel HD Graphics 4600).
|
|
12
|
+
- A Windows capture backs only its own marketing version (10 or 11), so the
|
|
13
|
+
claimed `platformVersion` agrees with the capture's fonts and colours.
|
|
14
|
+
- `PHASMID_GEOIP_MMDB` / `--geoip-db` use a DB-IP City Lite build other than
|
|
15
|
+
the pinned one once DB-IP stops serving it.
|
|
16
|
+
- CLI subcommands: `generate`, `locate`, `gpus`, `captures`, `geoip`, `validate`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, The Phasmid Authors
|
|
4
|
+
Copyright (c) 2015-2026, The ungoogled-chromium Authors
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: phasmid-profile-gen
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate Phasmid browser profiles from BrowserForge marginals and captured real devices
|
|
5
|
+
Project-URL: Homepage, https://github.com/tintty1/phasmid-profile-gen
|
|
6
|
+
Project-URL: Repository, https://github.com/tintty1/phasmid-profile-gen
|
|
7
|
+
Project-URL: Issues, https://github.com/tintty1/phasmid-profile-gen/issues
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: src/phasmid_profile_gen/data/LICENSES.md
|
|
11
|
+
Keywords: browser fingerprint,browser profile,chromium,phasmid
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: jsonschema>=4.23
|
|
22
|
+
Provides-Extra: location
|
|
23
|
+
Requires-Dist: maxminddb==3.2.0; extra == 'location'
|
|
24
|
+
Requires-Dist: timezonefinder-data==4.2026.5; extra == 'location'
|
|
25
|
+
Requires-Dist: timezonefinder==9.2.0; extra == 'location'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# phasmid-profile-gen
|
|
29
|
+
|
|
30
|
+
[](https://github.com/tintty1/phasmid-profile-gen/actions/workflows/test.yml)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://www.python.org/downloads/)
|
|
33
|
+
|
|
34
|
+
Generates device profiles for [Phasmid](https://github.com/tintty1/phasmid), the
|
|
35
|
+
Chromium fork that reports the device described by one JSON profile instead of
|
|
36
|
+
the machine it runs on.
|
|
37
|
+
|
|
38
|
+
This is Phasmid's `tools/genprofile` as a standalone package. The data it needs
|
|
39
|
+
is bundled in compressed form (about 60 KB), so it runs without a Phasmid
|
|
40
|
+
checkout and without BrowserForge installed. From the same captures it produces
|
|
41
|
+
the same profiles as `tools/genprofile`, byte for byte, except for the
|
|
42
|
+
generator's name in `captured.note`.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install phasmid-profile-gen # profiles with a given time zone
|
|
48
|
+
pip install 'phasmid-profile-gen[location]' # also place profiles at a proxy exit IP
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Python 3.12 or newer. The `location` extra adds `timezonefinder` (with its
|
|
52
|
+
boundary data, about 33 MB) and `maxminddb`, pinned exactly because they decide
|
|
53
|
+
which zone and position a seed gets.
|
|
54
|
+
|
|
55
|
+
## Use
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from phasmid_profile_gen import dumps, generate_profile
|
|
59
|
+
|
|
60
|
+
# Placed where the proxy comes out: zone, geolocation, WebRTC address, languages.
|
|
61
|
+
profile = generate_profile("windows", exit_ip=proxy_exit_ip)
|
|
62
|
+
|
|
63
|
+
# Or with an explicit locale, reproducibly.
|
|
64
|
+
profile = generate_profile(
|
|
65
|
+
"linux",
|
|
66
|
+
seed="0a1b2c3d4e5f60718293a4b5c6d7e8f9",
|
|
67
|
+
timezone="Europe/Berlin",
|
|
68
|
+
languages=["de-DE", "de", "en-US", "en"],
|
|
69
|
+
)
|
|
70
|
+
open("profile.json", "w").write(dumps(profile))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
phasmid-profile-gen generate --os windows --exit-ip <proxy exit IP> -o profile.json
|
|
75
|
+
phasmid-profile-gen generate --os linux --seed 0a1b2c3d4e5f60718293a4b5c6d7e8f9 \
|
|
76
|
+
--timezone Europe/Berlin --languages de-DE,de,en-US,en -o profile.json
|
|
77
|
+
phasmid-profile-gen gpus # GPUs the bundled captures back (for --gpu)
|
|
78
|
+
phasmid-profile-gen captures # the bundled captures
|
|
79
|
+
phasmid-profile-gen locate <proxy exit IP> --seed <seed>
|
|
80
|
+
phasmid-profile-gen validate profile.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Give exactly one of an exit IP and a time zone. **Generate Windows profiles
|
|
84
|
+
with an exit IP.** Windows Chrome answers geolocation from the system location
|
|
85
|
+
provider, so a Windows profile with no position is a stronger tell than a Linux
|
|
86
|
+
one. The seed is 32 lowercase hex characters. When omitted, a new one is drawn;
|
|
87
|
+
read it back from `profile["seed"]`. The same seed and inputs always give the
|
|
88
|
+
same bytes (`dumps`).
|
|
89
|
+
|
|
90
|
+
Other functions: `locate(ip, seed)`, `list_gpus()`, `list_captures()`,
|
|
91
|
+
`validate(profile)` (against the bundled Phasmid schema) and `new_seed()`.
|
|
92
|
+
Errors are `GenerationError`, `LocationError` and `geoip.GeoipError`.
|
|
93
|
+
|
|
94
|
+
## What a profile is made of
|
|
95
|
+
|
|
96
|
+
| Part | From |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| OS identity, CPU threads, memory class, touch points, screen and window geometry, media device presence, battery | A [BrowserForge](https://github.com/daijro/browserforge) sample for desktop Chrome on the OS, drawn from the seed. Each node is drawn for one sampled user agent, filtered to what the OS and the capture allow. |
|
|
99
|
+
| GPU, WebGL and WebGPU surface, fonts, voices, audio, codecs, system colours, pointer, preferences, RAM, disk, media device labels | One capture of a real device. On Linux the GPU's identity may instead be one that the capture's driver class reports with the exact same WebGL surface (Phasmid decision `gpu-claims-stay-within-a-captured-host-class`). |
|
|
100
|
+
| Time zone, geolocation, WebRTC public IP, languages | The exit IP: DB-IP City Lite, timezonefinder, CLDR, and Chrome's default accept-languages for the country's UI locale. Otherwise the time zone and languages you pass. |
|
|
101
|
+
|
|
102
|
+
No user-agent or version string is written. Phasmid builds those from its own
|
|
103
|
+
version. Nothing is invented: a field only a capture can supply is never
|
|
104
|
+
filled in by guessing, and an OS without a capture fails with
|
|
105
|
+
`MissingCaptures`. `captured.note` records where every part came from.
|
|
106
|
+
|
|
107
|
+
### Bundled captures
|
|
108
|
+
|
|
109
|
+
Run `phasmid-profile-gen captures` for the current list. It is also in
|
|
110
|
+
[`data/manifest.json`](src/phasmid_profile_gen/data/manifest.json), which lists
|
|
111
|
+
the dumps refused and why.
|
|
112
|
+
|
|
113
|
+
- Phasmid's own: Linux (Arch, RTX 3060) and Windows 11 (Intel UHD 600).
|
|
114
|
+
- From the [collector app](https://phasmid.ecomeezy.com/): Windows 10 (Intel
|
|
115
|
+
HD Graphics 4600). Visitors grant no media permission, so the dump has no
|
|
116
|
+
device labels. Profiles built from it claim no microphone, speaker or
|
|
117
|
+
camera.
|
|
118
|
+
|
|
119
|
+
A Windows capture backs only its own version: a Windows 11 capture gives
|
|
120
|
+
Windows 11 profiles (`platformVersion` 13 and up), a Windows 10 capture
|
|
121
|
+
Windows 10 ones. Their fonts differ (Windows 11 dropped HoloLens MDL2 Assets
|
|
122
|
+
and added Segoe Fluent Icons). This is the one intended difference from
|
|
123
|
+
`tools/genprofile`, which drew either version for its Windows 11 capture.
|
|
124
|
+
|
|
125
|
+
## The location database
|
|
126
|
+
|
|
127
|
+
The exit IP is looked up in DB-IP IP to City Lite (CC BY 4.0, "IP Geolocation
|
|
128
|
+
by DB-IP"). It is about 127 MB, so it is not bundled. The build pinned in
|
|
129
|
+
`data/dbip-city-lite.json` is downloaded on first use, checked against the
|
|
130
|
+
pin, and kept in `$XDG_CACHE_HOME/phasmid/geoip/`. That is the cache Phasmid's
|
|
131
|
+
tools use, so an existing download is reused.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
phasmid-profile-gen geoip # download and verify now
|
|
135
|
+
phasmid-profile-gen geoip --archive dbip-city-lite-2026-10.mmdb.gz
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
DB-IP serves only the last two monthly builds. Once the pinned one is gone,
|
|
139
|
+
either install a saved copy with `--archive`, or use any other DB-IP City Lite
|
|
140
|
+
build with `PHASMID_GEOIP_MMDB=/path/to/dbip-city-lite.mmdb`, `--geoip-db` or
|
|
141
|
+
`geoip_db=`. Another build is not checked against the pin; the profile's note
|
|
142
|
+
names its build date and sha256 instead.
|
|
143
|
+
|
|
144
|
+
## Rebuilding the data
|
|
145
|
+
|
|
146
|
+
`src/phasmid_profile_gen/data/` is generated, compressed and committed. It is
|
|
147
|
+
rebuilt from a Phasmid checkout and collector-app dumps by
|
|
148
|
+
[`scripts/build_data.py`](scripts/build_data.py):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
uv sync --group build-data
|
|
152
|
+
# in collector-app: npm run pull -- --out captures
|
|
153
|
+
uv run --group build-data python scripts/build_data.py \
|
|
154
|
+
--phasmid ../phasmid \
|
|
155
|
+
--collector ../collector-app/captures/d1-2026-11-05/dumps
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The build:
|
|
159
|
+
|
|
160
|
+
- maps every dump with the checkout's own `tests/consistency/capture.py` (the
|
|
161
|
+
code `consistency capture` runs), and checks Phasmid's captures against their
|
|
162
|
+
records as `tools/genprofile` does;
|
|
163
|
+
- keeps a collector dump only if it is stock desktop Google Chrome on Windows
|
|
164
|
+
or Linux, with a hardware GPU, a consistent UA and UA-CH, and a window inside
|
|
165
|
+
its screen, and only if it maps to a valid profile. Each refusal is listed
|
|
166
|
+
with its reason in `manifest.json`;
|
|
167
|
+
- stores only what generation reads. Raw dumps and visitors' locales are never
|
|
168
|
+
written;
|
|
169
|
+
- restricts BrowserForge's network to desktop Chrome on Windows and Linux and
|
|
170
|
+
the nodes the generator draws. `tests/test_data.py` checks every table
|
|
171
|
+
against browserforge itself.
|
|
172
|
+
|
|
173
|
+
The output is deterministic, so a rebuild from the same sources gives the same
|
|
174
|
+
bytes. A Linux collector dump also needs the operator's OS version (`?manual=1`),
|
|
175
|
+
which a page cannot read.
|
|
176
|
+
|
|
177
|
+
## Develop
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv sync --all-extras --group build-data
|
|
181
|
+
uv run pytest
|
|
182
|
+
uv run pre-commit run --all-files
|
|
183
|
+
PHASMID_GEOIP_NETWORK=1 uv run pytest -m network # reads the real DB-IP build
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Releases are published to PyPI by `.github/workflows/publish-pypi.yaml` when a
|
|
187
|
+
GitHub release is published. The version comes from the release tag
|
|
188
|
+
(`uv-dynamic-versioning`, e.g. `v0.1.0`). Publishing uses PyPI trusted
|
|
189
|
+
publishing: register the repository and that workflow as a trusted publisher
|
|
190
|
+
of `phasmid-profile-gen` on PyPI once.
|
|
191
|
+
|
|
192
|
+
## Licence
|
|
193
|
+
|
|
194
|
+
The code is BSD 3-Clause, as Phasmid is ([`LICENSE`](LICENSE)). The bundled
|
|
195
|
+
data keeps its sources' licences:
|
|
196
|
+
|
|
197
|
+
- BrowserForge's network: Apache 2.0.
|
|
198
|
+
- Unicode CLDR: Unicode License v3.
|
|
199
|
+
- Chromium's locale table: BSD-style.
|
|
200
|
+
|
|
201
|
+
DB-IP (CC BY 4.0) and timezone-boundary-builder (ODbL 1.0) are used at runtime
|
|
202
|
+
but not bundled. See
|
|
203
|
+
[`data/LICENSES.md`](src/phasmid_profile_gen/data/LICENSES.md).
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# phasmid-profile-gen
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tintty1/phasmid-profile-gen/actions/workflows/test.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
|
|
7
|
+
Generates device profiles for [Phasmid](https://github.com/tintty1/phasmid), the
|
|
8
|
+
Chromium fork that reports the device described by one JSON profile instead of
|
|
9
|
+
the machine it runs on.
|
|
10
|
+
|
|
11
|
+
This is Phasmid's `tools/genprofile` as a standalone package. The data it needs
|
|
12
|
+
is bundled in compressed form (about 60 KB), so it runs without a Phasmid
|
|
13
|
+
checkout and without BrowserForge installed. From the same captures it produces
|
|
14
|
+
the same profiles as `tools/genprofile`, byte for byte, except for the
|
|
15
|
+
generator's name in `captured.note`.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install phasmid-profile-gen # profiles with a given time zone
|
|
21
|
+
pip install 'phasmid-profile-gen[location]' # also place profiles at a proxy exit IP
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Python 3.12 or newer. The `location` extra adds `timezonefinder` (with its
|
|
25
|
+
boundary data, about 33 MB) and `maxminddb`, pinned exactly because they decide
|
|
26
|
+
which zone and position a seed gets.
|
|
27
|
+
|
|
28
|
+
## Use
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from phasmid_profile_gen import dumps, generate_profile
|
|
32
|
+
|
|
33
|
+
# Placed where the proxy comes out: zone, geolocation, WebRTC address, languages.
|
|
34
|
+
profile = generate_profile("windows", exit_ip=proxy_exit_ip)
|
|
35
|
+
|
|
36
|
+
# Or with an explicit locale, reproducibly.
|
|
37
|
+
profile = generate_profile(
|
|
38
|
+
"linux",
|
|
39
|
+
seed="0a1b2c3d4e5f60718293a4b5c6d7e8f9",
|
|
40
|
+
timezone="Europe/Berlin",
|
|
41
|
+
languages=["de-DE", "de", "en-US", "en"],
|
|
42
|
+
)
|
|
43
|
+
open("profile.json", "w").write(dumps(profile))
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
phasmid-profile-gen generate --os windows --exit-ip <proxy exit IP> -o profile.json
|
|
48
|
+
phasmid-profile-gen generate --os linux --seed 0a1b2c3d4e5f60718293a4b5c6d7e8f9 \
|
|
49
|
+
--timezone Europe/Berlin --languages de-DE,de,en-US,en -o profile.json
|
|
50
|
+
phasmid-profile-gen gpus # GPUs the bundled captures back (for --gpu)
|
|
51
|
+
phasmid-profile-gen captures # the bundled captures
|
|
52
|
+
phasmid-profile-gen locate <proxy exit IP> --seed <seed>
|
|
53
|
+
phasmid-profile-gen validate profile.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Give exactly one of an exit IP and a time zone. **Generate Windows profiles
|
|
57
|
+
with an exit IP.** Windows Chrome answers geolocation from the system location
|
|
58
|
+
provider, so a Windows profile with no position is a stronger tell than a Linux
|
|
59
|
+
one. The seed is 32 lowercase hex characters. When omitted, a new one is drawn;
|
|
60
|
+
read it back from `profile["seed"]`. The same seed and inputs always give the
|
|
61
|
+
same bytes (`dumps`).
|
|
62
|
+
|
|
63
|
+
Other functions: `locate(ip, seed)`, `list_gpus()`, `list_captures()`,
|
|
64
|
+
`validate(profile)` (against the bundled Phasmid schema) and `new_seed()`.
|
|
65
|
+
Errors are `GenerationError`, `LocationError` and `geoip.GeoipError`.
|
|
66
|
+
|
|
67
|
+
## What a profile is made of
|
|
68
|
+
|
|
69
|
+
| Part | From |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| OS identity, CPU threads, memory class, touch points, screen and window geometry, media device presence, battery | A [BrowserForge](https://github.com/daijro/browserforge) sample for desktop Chrome on the OS, drawn from the seed. Each node is drawn for one sampled user agent, filtered to what the OS and the capture allow. |
|
|
72
|
+
| GPU, WebGL and WebGPU surface, fonts, voices, audio, codecs, system colours, pointer, preferences, RAM, disk, media device labels | One capture of a real device. On Linux the GPU's identity may instead be one that the capture's driver class reports with the exact same WebGL surface (Phasmid decision `gpu-claims-stay-within-a-captured-host-class`). |
|
|
73
|
+
| Time zone, geolocation, WebRTC public IP, languages | The exit IP: DB-IP City Lite, timezonefinder, CLDR, and Chrome's default accept-languages for the country's UI locale. Otherwise the time zone and languages you pass. |
|
|
74
|
+
|
|
75
|
+
No user-agent or version string is written. Phasmid builds those from its own
|
|
76
|
+
version. Nothing is invented: a field only a capture can supply is never
|
|
77
|
+
filled in by guessing, and an OS without a capture fails with
|
|
78
|
+
`MissingCaptures`. `captured.note` records where every part came from.
|
|
79
|
+
|
|
80
|
+
### Bundled captures
|
|
81
|
+
|
|
82
|
+
Run `phasmid-profile-gen captures` for the current list. It is also in
|
|
83
|
+
[`data/manifest.json`](src/phasmid_profile_gen/data/manifest.json), which lists
|
|
84
|
+
the dumps refused and why.
|
|
85
|
+
|
|
86
|
+
- Phasmid's own: Linux (Arch, RTX 3060) and Windows 11 (Intel UHD 600).
|
|
87
|
+
- From the [collector app](https://phasmid.ecomeezy.com/): Windows 10 (Intel
|
|
88
|
+
HD Graphics 4600). Visitors grant no media permission, so the dump has no
|
|
89
|
+
device labels. Profiles built from it claim no microphone, speaker or
|
|
90
|
+
camera.
|
|
91
|
+
|
|
92
|
+
A Windows capture backs only its own version: a Windows 11 capture gives
|
|
93
|
+
Windows 11 profiles (`platformVersion` 13 and up), a Windows 10 capture
|
|
94
|
+
Windows 10 ones. Their fonts differ (Windows 11 dropped HoloLens MDL2 Assets
|
|
95
|
+
and added Segoe Fluent Icons). This is the one intended difference from
|
|
96
|
+
`tools/genprofile`, which drew either version for its Windows 11 capture.
|
|
97
|
+
|
|
98
|
+
## The location database
|
|
99
|
+
|
|
100
|
+
The exit IP is looked up in DB-IP IP to City Lite (CC BY 4.0, "IP Geolocation
|
|
101
|
+
by DB-IP"). It is about 127 MB, so it is not bundled. The build pinned in
|
|
102
|
+
`data/dbip-city-lite.json` is downloaded on first use, checked against the
|
|
103
|
+
pin, and kept in `$XDG_CACHE_HOME/phasmid/geoip/`. That is the cache Phasmid's
|
|
104
|
+
tools use, so an existing download is reused.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
phasmid-profile-gen geoip # download and verify now
|
|
108
|
+
phasmid-profile-gen geoip --archive dbip-city-lite-2026-10.mmdb.gz
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
DB-IP serves only the last two monthly builds. Once the pinned one is gone,
|
|
112
|
+
either install a saved copy with `--archive`, or use any other DB-IP City Lite
|
|
113
|
+
build with `PHASMID_GEOIP_MMDB=/path/to/dbip-city-lite.mmdb`, `--geoip-db` or
|
|
114
|
+
`geoip_db=`. Another build is not checked against the pin; the profile's note
|
|
115
|
+
names its build date and sha256 instead.
|
|
116
|
+
|
|
117
|
+
## Rebuilding the data
|
|
118
|
+
|
|
119
|
+
`src/phasmid_profile_gen/data/` is generated, compressed and committed. It is
|
|
120
|
+
rebuilt from a Phasmid checkout and collector-app dumps by
|
|
121
|
+
[`scripts/build_data.py`](scripts/build_data.py):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
uv sync --group build-data
|
|
125
|
+
# in collector-app: npm run pull -- --out captures
|
|
126
|
+
uv run --group build-data python scripts/build_data.py \
|
|
127
|
+
--phasmid ../phasmid \
|
|
128
|
+
--collector ../collector-app/captures/d1-2026-11-05/dumps
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The build:
|
|
132
|
+
|
|
133
|
+
- maps every dump with the checkout's own `tests/consistency/capture.py` (the
|
|
134
|
+
code `consistency capture` runs), and checks Phasmid's captures against their
|
|
135
|
+
records as `tools/genprofile` does;
|
|
136
|
+
- keeps a collector dump only if it is stock desktop Google Chrome on Windows
|
|
137
|
+
or Linux, with a hardware GPU, a consistent UA and UA-CH, and a window inside
|
|
138
|
+
its screen, and only if it maps to a valid profile. Each refusal is listed
|
|
139
|
+
with its reason in `manifest.json`;
|
|
140
|
+
- stores only what generation reads. Raw dumps and visitors' locales are never
|
|
141
|
+
written;
|
|
142
|
+
- restricts BrowserForge's network to desktop Chrome on Windows and Linux and
|
|
143
|
+
the nodes the generator draws. `tests/test_data.py` checks every table
|
|
144
|
+
against browserforge itself.
|
|
145
|
+
|
|
146
|
+
The output is deterministic, so a rebuild from the same sources gives the same
|
|
147
|
+
bytes. A Linux collector dump also needs the operator's OS version (`?manual=1`),
|
|
148
|
+
which a page cannot read.
|
|
149
|
+
|
|
150
|
+
## Develop
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv sync --all-extras --group build-data
|
|
154
|
+
uv run pytest
|
|
155
|
+
uv run pre-commit run --all-files
|
|
156
|
+
PHASMID_GEOIP_NETWORK=1 uv run pytest -m network # reads the real DB-IP build
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Releases are published to PyPI by `.github/workflows/publish-pypi.yaml` when a
|
|
160
|
+
GitHub release is published. The version comes from the release tag
|
|
161
|
+
(`uv-dynamic-versioning`, e.g. `v0.1.0`). Publishing uses PyPI trusted
|
|
162
|
+
publishing: register the repository and that workflow as a trusted publisher
|
|
163
|
+
of `phasmid-profile-gen` on PyPI once.
|
|
164
|
+
|
|
165
|
+
## Licence
|
|
166
|
+
|
|
167
|
+
The code is BSD 3-Clause, as Phasmid is ([`LICENSE`](LICENSE)). The bundled
|
|
168
|
+
data keeps its sources' licences:
|
|
169
|
+
|
|
170
|
+
- BrowserForge's network: Apache 2.0.
|
|
171
|
+
- Unicode CLDR: Unicode License v3.
|
|
172
|
+
- Chromium's locale table: BSD-style.
|
|
173
|
+
|
|
174
|
+
DB-IP (CC BY 4.0) and timezone-boundary-builder (ODbL 1.0) are used at runtime
|
|
175
|
+
but not bundled. See
|
|
176
|
+
[`data/LICENSES.md`](src/phasmid_profile_gen/data/LICENSES.md).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "phasmid-profile-gen"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Generate Phasmid browser profiles from BrowserForge marginals and captured real devices"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "BSD-3-Clause"
|
|
8
|
+
license-files = ["LICENSE", "src/phasmid_profile_gen/data/LICENSES.md"]
|
|
9
|
+
keywords = ["phasmid", "chromium", "browser fingerprint", "browser profile"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"jsonschema>=4.23",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
# Placing a profile at a proxy exit IP. Pinned exactly: the zone polygons decide
|
|
26
|
+
# which zone and position a seed gives, so another data release changes profiles.
|
|
27
|
+
location = [
|
|
28
|
+
"maxminddb==3.2.0",
|
|
29
|
+
"timezonefinder==9.2.0",
|
|
30
|
+
"timezonefinder-data==4.2026.5",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
phasmid-profile-gen = "phasmid_profile_gen.cli:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/tintty1/phasmid-profile-gen"
|
|
38
|
+
Repository = "https://github.com/tintty1/phasmid-profile-gen"
|
|
39
|
+
Issues = "https://github.com/tintty1/phasmid-profile-gen/issues"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling", "uv-dynamic-versioning"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
source = "uv-dynamic-versioning"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
exclude = [".github", ".claude-scratch"]
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = [
|
|
53
|
+
"pytest>=8",
|
|
54
|
+
"ruff",
|
|
55
|
+
"pre-commit>=4.3.0",
|
|
56
|
+
]
|
|
57
|
+
# Only for scripts/build_data.py, which rebuilds src/phasmid_profile_gen/data.
|
|
58
|
+
build-data = [
|
|
59
|
+
"browserforge==1.2.4",
|
|
60
|
+
"apify-fingerprint-datapoints==0.15.0",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.pytest.ini_options]
|
|
64
|
+
testpaths = ["tests"]
|
|
65
|
+
markers = [
|
|
66
|
+
"network: downloads the pinned DB-IP database (run with PHASMID_GEOIP_NETWORK=1)",
|
|
67
|
+
]
|