phasmid-profile-gen 0.1.1__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.
- phasmid_profile_gen-0.1.1/.gitignore +9 -0
- phasmid_profile_gen-0.1.1/.pre-commit-config.yaml +8 -0
- phasmid_profile_gen-0.1.1/.python-version +1 -0
- phasmid_profile_gen-0.1.1/.ruff.toml +63 -0
- phasmid_profile_gen-0.1.1/CHANGELOG.md +34 -0
- phasmid_profile_gen-0.1.1/LICENSE +30 -0
- phasmid_profile_gen-0.1.1/PKG-INFO +219 -0
- phasmid_profile_gen-0.1.1/README.md +192 -0
- phasmid_profile_gen-0.1.1/pyproject.toml +62 -0
- phasmid_profile_gen-0.1.1/scripts/build_data.py +489 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/__init__.py +165 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/__main__.py +5 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/_data.py +25 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/_rng.py +17 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/captures.py +78 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/cli.py +168 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/CLDR-LICENSE +41 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/LICENSES.md +16 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/captures.json.xz +0 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/dbip-city-lite.json +11 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/locale.json.xz +0 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/manifest.json +148 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/data/schema.json.xz +0 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/generate.py +205 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/geoip.py +194 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/locales.py +125 -0
- phasmid_profile_gen-0.1.1/src/phasmid_profile_gen/location.py +218 -0
- phasmid_profile_gen-0.1.1/tests/conftest.py +46 -0
- phasmid_profile_gen-0.1.1/tests/fixtures/geoip-rows.json +30 -0
- phasmid_profile_gen-0.1.1/tests/test_build_data.py +176 -0
- phasmid_profile_gen-0.1.1/tests/test_data.py +83 -0
- phasmid_profile_gen-0.1.1/tests/test_generate.py +230 -0
- phasmid_profile_gen-0.1.1/tests/test_location.py +332 -0
- phasmid_profile_gen-0.1.1/uv.lock +720 -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,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Profiles come from the bundled captures only. BrowserForge's network and the
|
|
6
|
+
fpgen GPU-claim extract are gone (with `forge.json.xz`, `fpgen.json.xz` and
|
|
7
|
+
the `build-data` dependency group): every device field is one capture's,
|
|
8
|
+
and `gpu=` selects a capture with that GPU.
|
|
9
|
+
- `ProfileGenerator` protocol for choosing the capture, passed as
|
|
10
|
+
`generate_profile(generator=)`. `RandomCapture` (the default) draws one of
|
|
11
|
+
the OS's captures uniformly from the seed; `from_capture` builds the profile.
|
|
12
|
+
- `list_gpus()` reports `captures` per GPU instead of `weight`.
|
|
13
|
+
- `MissingCaptures` no longer carries a draft; `generate --draft` is removed.
|
|
14
|
+
|
|
15
|
+
First release: Phasmid's `tools/genprofile` as a package, with its data bundled.
|
|
16
|
+
|
|
17
|
+
- Generates the same profiles as `tools/genprofile` from the same captures,
|
|
18
|
+
byte for byte except for the generator's name in `captured.note`.
|
|
19
|
+
- Data built from Phasmid at the commit in `data/manifest.json`, BrowserForge's
|
|
20
|
+
fingerprint network (`apify_fingerprint_datapoints` 0.15.0) and further
|
|
21
|
+
captures: seven in all (one Linux, six Windows).
|
|
22
|
+
- Captures are named by OS family and PCI ids (`windows-0x8086-0x0412`). The
|
|
23
|
+
manifest lists refused dumps by hash only.
|
|
24
|
+
- A Windows capture backs only its own marketing version (10 or 11), so the
|
|
25
|
+
claimed `platformVersion` agrees with the capture's fonts and colours.
|
|
26
|
+
- One capture (`0x5916`) reports a heap limit that no whole-MB RAM gives
|
|
27
|
+
exactly, so its RAM is the nearest whole MB (8053) and its heap limit the one
|
|
28
|
+
the browser computes for it; `manifest.json` lists both as `assumed`.
|
|
29
|
+
- A Windows sample that drew 640 CPU threads (over the schema's 256; about 1 seed
|
|
30
|
+
in 500) fails validation in `tools/genprofile`. It is drawn again here; every
|
|
31
|
+
other seed's profile is the same as there.
|
|
32
|
+
- `PHASMID_GEOIP_MMDB` / `--geoip-db` use a DB-IP City Lite build other than
|
|
33
|
+
the pinned one once DB-IP stops serving it.
|
|
34
|
+
- 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,219 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: phasmid-profile-gen
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Generate Phasmid browser profiles from 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
|
+
Every device field of a profile comes from one capture of a real device: no
|
|
39
|
+
population model is sampled and no GPU identity is derived. The captures, the
|
|
40
|
+
schema and the locale tables are bundled in compressed form, so it runs without
|
|
41
|
+
a Phasmid checkout.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install phasmid-profile-gen # profiles with a given time zone
|
|
47
|
+
pip install 'phasmid-profile-gen[location]' # also place profiles at a proxy exit IP
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Python 3.12 or newer. The `location` extra adds `timezonefinder` (with its
|
|
51
|
+
boundary data, about 33 MB) and `maxminddb`, pinned exactly because they decide
|
|
52
|
+
which zone and position a seed gets.
|
|
53
|
+
|
|
54
|
+
## Use
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from phasmid_profile_gen import dumps, generate_profile
|
|
58
|
+
|
|
59
|
+
# Placed where the proxy comes out: zone, geolocation, WebRTC address, languages.
|
|
60
|
+
profile = generate_profile("windows", exit_ip=proxy_exit_ip)
|
|
61
|
+
|
|
62
|
+
# Or with an explicit locale, reproducibly.
|
|
63
|
+
profile = generate_profile(
|
|
64
|
+
"linux",
|
|
65
|
+
seed="0a1b2c3d4e5f60718293a4b5c6d7e8f9",
|
|
66
|
+
timezone="Europe/Berlin",
|
|
67
|
+
languages=["de-DE", "de", "en-US", "en"],
|
|
68
|
+
)
|
|
69
|
+
open("profile.json", "w").write(dumps(profile))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
phasmid-profile-gen generate --os windows --exit-ip <proxy exit IP> -o profile.json
|
|
74
|
+
phasmid-profile-gen generate --os linux --seed 0a1b2c3d4e5f60718293a4b5c6d7e8f9 \
|
|
75
|
+
--timezone Europe/Berlin --languages de-DE,de,en-US,en -o profile.json
|
|
76
|
+
phasmid-profile-gen gpus # GPUs the bundled captures back (for --gpu)
|
|
77
|
+
phasmid-profile-gen captures # the bundled captures
|
|
78
|
+
phasmid-profile-gen locate <proxy exit IP> --seed <seed>
|
|
79
|
+
phasmid-profile-gen validate profile.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Give exactly one of an exit IP and a time zone. **Generate Windows profiles
|
|
83
|
+
with an exit IP.** Windows Chrome answers geolocation from the system location
|
|
84
|
+
provider, so a Windows profile with no position is a stronger tell than a Linux
|
|
85
|
+
one. The seed is 32 lowercase hex characters. When omitted, a new one is drawn;
|
|
86
|
+
read it back from `profile["seed"]`. The same seed and inputs always give the
|
|
87
|
+
same bytes (`dumps`).
|
|
88
|
+
|
|
89
|
+
### Choosing the capture
|
|
90
|
+
|
|
91
|
+
Which capture backs a profile is a `ProfileGenerator`'s choice: any object
|
|
92
|
+
with `generate(request: Request) -> dict`. The default, `RandomCapture()`,
|
|
93
|
+
draws one of the OS's captures (those with the requested GPU, when `gpu=` is
|
|
94
|
+
given) uniformly from the seed. Pass another strategy as `generator=`;
|
|
95
|
+
`from_capture(capture, request)` builds the profile once a capture is chosen:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from phasmid_profile_gen import Request, catalog, from_capture, generate_profile
|
|
99
|
+
|
|
100
|
+
class Newest:
|
|
101
|
+
def generate(self, request: Request) -> dict:
|
|
102
|
+
captures = [c for c in catalog() if c.family == request.family]
|
|
103
|
+
newest = max(captures, key=lambda c: c.date)
|
|
104
|
+
return from_capture(newest, request, how="the newest capture")
|
|
105
|
+
|
|
106
|
+
profile = generate_profile("windows", timezone="UTC", generator=Newest())
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A generator draws only from `request.seed` (`_rng.decision_rng(seed,
|
|
110
|
+
decision)`), so a seed keeps giving the same profile. `generate_profile`
|
|
111
|
+
validates whatever it returns.
|
|
112
|
+
|
|
113
|
+
Other functions: `locate(ip, seed)`, `list_gpus()`, `list_captures()`,
|
|
114
|
+
`validate(profile)` (against the bundled Phasmid schema) and `new_seed()`.
|
|
115
|
+
Errors are `GenerationError`, `LocationError` and `geoip.GeoipError`.
|
|
116
|
+
|
|
117
|
+
## What a profile is made of
|
|
118
|
+
|
|
119
|
+
| Part | From |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| OS identity and version, CPU threads, memory, touch points, screen and window geometry, GPU with its WebGL and WebGPU surface, fonts, voices, audio, codecs, system colours, pointer, preferences, RAM, disk, media devices, battery, connection | One capture of a real device, taken whole. |
|
|
122
|
+
| 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. |
|
|
123
|
+
|
|
124
|
+
No user-agent or version string is written. Phasmid builds those from its own
|
|
125
|
+
version. Nothing is invented: a field only a capture can supply is never
|
|
126
|
+
filled in by guessing, and an OS without a capture fails with
|
|
127
|
+
`MissingCaptures`. `captured.note` records where every part came from.
|
|
128
|
+
|
|
129
|
+
### Bundled captures
|
|
130
|
+
|
|
131
|
+
Run `phasmid-profile-gen captures` for the current list. It is also in
|
|
132
|
+
[`data/manifest.json`](src/phasmid_profile_gen/data/manifest.json), which lists
|
|
133
|
+
the dumps refused and why.
|
|
134
|
+
|
|
135
|
+
Each is named by its OS family and PCI ids (`windows-0x8086-0x0412`). Where a
|
|
136
|
+
capture has no media device labels (a page visit grants no media permission),
|
|
137
|
+
profiles built from it claim no microphone, speaker or camera.
|
|
138
|
+
|
|
139
|
+
A profile's OS version is its capture's, so a Windows 11 capture gives
|
|
140
|
+
Windows 11 profiles with Windows 11 fonts and colours.
|
|
141
|
+
|
|
142
|
+
## The location database
|
|
143
|
+
|
|
144
|
+
The exit IP is looked up in DB-IP IP to City Lite (CC BY 4.0, "IP Geolocation
|
|
145
|
+
by DB-IP"). It is about 127 MB, so it is not bundled. The build pinned in
|
|
146
|
+
`data/dbip-city-lite.json` is downloaded on first use, checked against the
|
|
147
|
+
pin, and kept in `$XDG_CACHE_HOME/phasmid/geoip/`. That is the cache Phasmid's
|
|
148
|
+
tools use, so an existing download is reused.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
phasmid-profile-gen geoip # download and verify now
|
|
152
|
+
phasmid-profile-gen geoip --archive dbip-city-lite-2026-10.mmdb.gz
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
DB-IP serves only the last two monthly builds. Once the pinned one is gone,
|
|
156
|
+
either install a saved copy with `--archive`, or use any other DB-IP City Lite
|
|
157
|
+
build with `PHASMID_GEOIP_MMDB=/path/to/dbip-city-lite.mmdb`, `--geoip-db` or
|
|
158
|
+
`geoip_db=`. Another build is not checked against the pin; the profile's note
|
|
159
|
+
names its build date and sha256 instead.
|
|
160
|
+
|
|
161
|
+
## Rebuilding the data
|
|
162
|
+
|
|
163
|
+
`src/phasmid_profile_gen/data/` is generated, compressed and committed. It is
|
|
164
|
+
rebuilt from a Phasmid checkout and further harness dumps by
|
|
165
|
+
[`scripts/build_data.py`](scripts/build_data.py):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
uv run python scripts/build_data.py \
|
|
169
|
+
--phasmid ../phasmid \
|
|
170
|
+
--dumps /path/to/dumps
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The build:
|
|
174
|
+
|
|
175
|
+
- maps every dump with the checkout's own `tests/consistency/capture.py` (the
|
|
176
|
+
code `consistency capture` runs), and checks Phasmid's captures against their
|
|
177
|
+
records as `tools/genprofile` does;
|
|
178
|
+
- keeps a dump only if it is stock desktop Google Chrome on Windows
|
|
179
|
+
or Linux, with a hardware GPU, a consistent UA and UA-CH, and a window inside
|
|
180
|
+
its screen, and only if it maps to a valid profile. Each refusal is listed
|
|
181
|
+
with its reason in `manifest.json`;
|
|
182
|
+
- takes the nearest whole-MB RAM when a dump's heap limit sits within 1 MiB of
|
|
183
|
+
one but matches none exactly (a fraction of a MB of RAM), sets the heap limit
|
|
184
|
+
to the one the browser computes for it, and lists both as `assumed` in
|
|
185
|
+
`manifest.json`; a limit further off is refused;
|
|
186
|
+
- stores only what generation reads, under a neutral name (OS family and PCI
|
|
187
|
+
ids). Raw dumps, file names, notes and the device's locale are never written,
|
|
188
|
+
and a refused dump is listed by hash only.
|
|
189
|
+
|
|
190
|
+
The output is deterministic, so a rebuild from the same sources gives the same
|
|
191
|
+
bytes. A Linux dump also needs the operator's OS version (`operator.os_version`),
|
|
192
|
+
which a page cannot read.
|
|
193
|
+
|
|
194
|
+
## Develop
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv sync --all-extras
|
|
198
|
+
uv run pytest
|
|
199
|
+
uv run pre-commit run --all-files
|
|
200
|
+
PHASMID_GEOIP_NETWORK=1 uv run pytest -m network # reads the real DB-IP build
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Releases are published to PyPI by `.github/workflows/publish-pypi.yaml` when a
|
|
204
|
+
GitHub release is published. The version comes from the release tag
|
|
205
|
+
(`uv-dynamic-versioning`, e.g. `v0.1.0`). Publishing uses PyPI trusted
|
|
206
|
+
publishing: register the repository and that workflow as a trusted publisher
|
|
207
|
+
of `phasmid-profile-gen` on PyPI once.
|
|
208
|
+
|
|
209
|
+
## Licence
|
|
210
|
+
|
|
211
|
+
The code is BSD 3-Clause, as Phasmid is ([`LICENSE`](LICENSE)). The bundled
|
|
212
|
+
data keeps its sources' licences:
|
|
213
|
+
|
|
214
|
+
- Unicode CLDR: Unicode License v3.
|
|
215
|
+
- Chromium's locale table: BSD-style.
|
|
216
|
+
|
|
217
|
+
DB-IP (CC BY 4.0) and timezone-boundary-builder (ODbL 1.0) are used at runtime
|
|
218
|
+
but not bundled. See
|
|
219
|
+
[`data/LICENSES.md`](src/phasmid_profile_gen/data/LICENSES.md).
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
Every device field of a profile comes from one capture of a real device: no
|
|
12
|
+
population model is sampled and no GPU identity is derived. The captures, the
|
|
13
|
+
schema and the locale tables are bundled in compressed form, so it runs without
|
|
14
|
+
a Phasmid checkout.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install phasmid-profile-gen # profiles with a given time zone
|
|
20
|
+
pip install 'phasmid-profile-gen[location]' # also place profiles at a proxy exit IP
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Python 3.12 or newer. The `location` extra adds `timezonefinder` (with its
|
|
24
|
+
boundary data, about 33 MB) and `maxminddb`, pinned exactly because they decide
|
|
25
|
+
which zone and position a seed gets.
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from phasmid_profile_gen import dumps, generate_profile
|
|
31
|
+
|
|
32
|
+
# Placed where the proxy comes out: zone, geolocation, WebRTC address, languages.
|
|
33
|
+
profile = generate_profile("windows", exit_ip=proxy_exit_ip)
|
|
34
|
+
|
|
35
|
+
# Or with an explicit locale, reproducibly.
|
|
36
|
+
profile = generate_profile(
|
|
37
|
+
"linux",
|
|
38
|
+
seed="0a1b2c3d4e5f60718293a4b5c6d7e8f9",
|
|
39
|
+
timezone="Europe/Berlin",
|
|
40
|
+
languages=["de-DE", "de", "en-US", "en"],
|
|
41
|
+
)
|
|
42
|
+
open("profile.json", "w").write(dumps(profile))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
phasmid-profile-gen generate --os windows --exit-ip <proxy exit IP> -o profile.json
|
|
47
|
+
phasmid-profile-gen generate --os linux --seed 0a1b2c3d4e5f60718293a4b5c6d7e8f9 \
|
|
48
|
+
--timezone Europe/Berlin --languages de-DE,de,en-US,en -o profile.json
|
|
49
|
+
phasmid-profile-gen gpus # GPUs the bundled captures back (for --gpu)
|
|
50
|
+
phasmid-profile-gen captures # the bundled captures
|
|
51
|
+
phasmid-profile-gen locate <proxy exit IP> --seed <seed>
|
|
52
|
+
phasmid-profile-gen validate profile.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Give exactly one of an exit IP and a time zone. **Generate Windows profiles
|
|
56
|
+
with an exit IP.** Windows Chrome answers geolocation from the system location
|
|
57
|
+
provider, so a Windows profile with no position is a stronger tell than a Linux
|
|
58
|
+
one. The seed is 32 lowercase hex characters. When omitted, a new one is drawn;
|
|
59
|
+
read it back from `profile["seed"]`. The same seed and inputs always give the
|
|
60
|
+
same bytes (`dumps`).
|
|
61
|
+
|
|
62
|
+
### Choosing the capture
|
|
63
|
+
|
|
64
|
+
Which capture backs a profile is a `ProfileGenerator`'s choice: any object
|
|
65
|
+
with `generate(request: Request) -> dict`. The default, `RandomCapture()`,
|
|
66
|
+
draws one of the OS's captures (those with the requested GPU, when `gpu=` is
|
|
67
|
+
given) uniformly from the seed. Pass another strategy as `generator=`;
|
|
68
|
+
`from_capture(capture, request)` builds the profile once a capture is chosen:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from phasmid_profile_gen import Request, catalog, from_capture, generate_profile
|
|
72
|
+
|
|
73
|
+
class Newest:
|
|
74
|
+
def generate(self, request: Request) -> dict:
|
|
75
|
+
captures = [c for c in catalog() if c.family == request.family]
|
|
76
|
+
newest = max(captures, key=lambda c: c.date)
|
|
77
|
+
return from_capture(newest, request, how="the newest capture")
|
|
78
|
+
|
|
79
|
+
profile = generate_profile("windows", timezone="UTC", generator=Newest())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A generator draws only from `request.seed` (`_rng.decision_rng(seed,
|
|
83
|
+
decision)`), so a seed keeps giving the same profile. `generate_profile`
|
|
84
|
+
validates whatever it returns.
|
|
85
|
+
|
|
86
|
+
Other functions: `locate(ip, seed)`, `list_gpus()`, `list_captures()`,
|
|
87
|
+
`validate(profile)` (against the bundled Phasmid schema) and `new_seed()`.
|
|
88
|
+
Errors are `GenerationError`, `LocationError` and `geoip.GeoipError`.
|
|
89
|
+
|
|
90
|
+
## What a profile is made of
|
|
91
|
+
|
|
92
|
+
| Part | From |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| OS identity and version, CPU threads, memory, touch points, screen and window geometry, GPU with its WebGL and WebGPU surface, fonts, voices, audio, codecs, system colours, pointer, preferences, RAM, disk, media devices, battery, connection | One capture of a real device, taken whole. |
|
|
95
|
+
| 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. |
|
|
96
|
+
|
|
97
|
+
No user-agent or version string is written. Phasmid builds those from its own
|
|
98
|
+
version. Nothing is invented: a field only a capture can supply is never
|
|
99
|
+
filled in by guessing, and an OS without a capture fails with
|
|
100
|
+
`MissingCaptures`. `captured.note` records where every part came from.
|
|
101
|
+
|
|
102
|
+
### Bundled captures
|
|
103
|
+
|
|
104
|
+
Run `phasmid-profile-gen captures` for the current list. It is also in
|
|
105
|
+
[`data/manifest.json`](src/phasmid_profile_gen/data/manifest.json), which lists
|
|
106
|
+
the dumps refused and why.
|
|
107
|
+
|
|
108
|
+
Each is named by its OS family and PCI ids (`windows-0x8086-0x0412`). Where a
|
|
109
|
+
capture has no media device labels (a page visit grants no media permission),
|
|
110
|
+
profiles built from it claim no microphone, speaker or camera.
|
|
111
|
+
|
|
112
|
+
A profile's OS version is its capture's, so a Windows 11 capture gives
|
|
113
|
+
Windows 11 profiles with Windows 11 fonts and colours.
|
|
114
|
+
|
|
115
|
+
## The location database
|
|
116
|
+
|
|
117
|
+
The exit IP is looked up in DB-IP IP to City Lite (CC BY 4.0, "IP Geolocation
|
|
118
|
+
by DB-IP"). It is about 127 MB, so it is not bundled. The build pinned in
|
|
119
|
+
`data/dbip-city-lite.json` is downloaded on first use, checked against the
|
|
120
|
+
pin, and kept in `$XDG_CACHE_HOME/phasmid/geoip/`. That is the cache Phasmid's
|
|
121
|
+
tools use, so an existing download is reused.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
phasmid-profile-gen geoip # download and verify now
|
|
125
|
+
phasmid-profile-gen geoip --archive dbip-city-lite-2026-10.mmdb.gz
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
DB-IP serves only the last two monthly builds. Once the pinned one is gone,
|
|
129
|
+
either install a saved copy with `--archive`, or use any other DB-IP City Lite
|
|
130
|
+
build with `PHASMID_GEOIP_MMDB=/path/to/dbip-city-lite.mmdb`, `--geoip-db` or
|
|
131
|
+
`geoip_db=`. Another build is not checked against the pin; the profile's note
|
|
132
|
+
names its build date and sha256 instead.
|
|
133
|
+
|
|
134
|
+
## Rebuilding the data
|
|
135
|
+
|
|
136
|
+
`src/phasmid_profile_gen/data/` is generated, compressed and committed. It is
|
|
137
|
+
rebuilt from a Phasmid checkout and further harness dumps by
|
|
138
|
+
[`scripts/build_data.py`](scripts/build_data.py):
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uv run python scripts/build_data.py \
|
|
142
|
+
--phasmid ../phasmid \
|
|
143
|
+
--dumps /path/to/dumps
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The build:
|
|
147
|
+
|
|
148
|
+
- maps every dump with the checkout's own `tests/consistency/capture.py` (the
|
|
149
|
+
code `consistency capture` runs), and checks Phasmid's captures against their
|
|
150
|
+
records as `tools/genprofile` does;
|
|
151
|
+
- keeps a dump only if it is stock desktop Google Chrome on Windows
|
|
152
|
+
or Linux, with a hardware GPU, a consistent UA and UA-CH, and a window inside
|
|
153
|
+
its screen, and only if it maps to a valid profile. Each refusal is listed
|
|
154
|
+
with its reason in `manifest.json`;
|
|
155
|
+
- takes the nearest whole-MB RAM when a dump's heap limit sits within 1 MiB of
|
|
156
|
+
one but matches none exactly (a fraction of a MB of RAM), sets the heap limit
|
|
157
|
+
to the one the browser computes for it, and lists both as `assumed` in
|
|
158
|
+
`manifest.json`; a limit further off is refused;
|
|
159
|
+
- stores only what generation reads, under a neutral name (OS family and PCI
|
|
160
|
+
ids). Raw dumps, file names, notes and the device's locale are never written,
|
|
161
|
+
and a refused dump is listed by hash only.
|
|
162
|
+
|
|
163
|
+
The output is deterministic, so a rebuild from the same sources gives the same
|
|
164
|
+
bytes. A Linux dump also needs the operator's OS version (`operator.os_version`),
|
|
165
|
+
which a page cannot read.
|
|
166
|
+
|
|
167
|
+
## Develop
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv sync --all-extras
|
|
171
|
+
uv run pytest
|
|
172
|
+
uv run pre-commit run --all-files
|
|
173
|
+
PHASMID_GEOIP_NETWORK=1 uv run pytest -m network # reads the real DB-IP build
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Releases are published to PyPI by `.github/workflows/publish-pypi.yaml` when a
|
|
177
|
+
GitHub release is published. The version comes from the release tag
|
|
178
|
+
(`uv-dynamic-versioning`, e.g. `v0.1.0`). Publishing uses PyPI trusted
|
|
179
|
+
publishing: register the repository and that workflow as a trusted publisher
|
|
180
|
+
of `phasmid-profile-gen` on PyPI once.
|
|
181
|
+
|
|
182
|
+
## Licence
|
|
183
|
+
|
|
184
|
+
The code is BSD 3-Clause, as Phasmid is ([`LICENSE`](LICENSE)). The bundled
|
|
185
|
+
data keeps its sources' licences:
|
|
186
|
+
|
|
187
|
+
- Unicode CLDR: Unicode License v3.
|
|
188
|
+
- Chromium's locale table: BSD-style.
|
|
189
|
+
|
|
190
|
+
DB-IP (CC BY 4.0) and timezone-boundary-builder (ODbL 1.0) are used at runtime
|
|
191
|
+
but not bundled. See
|
|
192
|
+
[`data/LICENSES.md`](src/phasmid_profile_gen/data/LICENSES.md).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "phasmid-profile-gen"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Generate Phasmid browser profiles from 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
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
markers = [
|
|
61
|
+
"network: downloads the pinned DB-IP database (run with PHASMID_GEOIP_NETWORK=1)",
|
|
62
|
+
]
|