Skip to content
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,47 @@ async with AsyncCamoufox() as browser:

[[Installation & usage](https://camoufox.com/python/)]

### Persistent Fingerprint Seeds

Pass `fingerprint_seed` when you want the same generated identity to be reused
across launches or contexts. The seed controls Camoufox-generated fingerprint
values such as BrowserForge sampling, font and voice subsets, WebGL sampling,
and audio/canvas/font config seeds. Launch-level seeds cover the full generated
`CAMOU_CONFIG`; context-level seeds apply the supported per-context surfaces for
new browser contexts. It does not persist cookies, storage, or browser profile
state.

```python
from camoufox.sync_api import Camoufox, NewContext

with Camoufox(fingerprint_seed="account-123") as browser:
page = browser.new_page()
page.goto("https://example.com")

context = NewContext(browser, fingerprint_seed="account-123:context-2")
page = context.new_page()
page.goto("https://example.com")
```

To persist cookies, local storage, and browser profile state, use a persistent
context with a stable `user_data_dir` alongside the same `fingerprint_seed`:

```python
from pathlib import Path

from camoufox.sync_api import Camoufox

profile_dir = Path("profiles/account-123")

with Camoufox(
persistent_context=True,
user_data_dir=profile_dir,
fingerprint_seed="account-123",
) as context:
page = context.new_page()
page.goto("https://example.com")
```

### Making Full use of Hardware Spoofing

For stable releases, you should always use the main [`camoufox`](https://pypi.org/project/camoufox/) pip package. However, if you want to make use of per-context fingerprints and hardware spoofing, use the [`cloverlabs-camoufox`](https://pypi.org/project/cloverlabs-camoufox/) package. This package is updated with each releases, whereas the official package is released on delay.
Expand Down Expand Up @@ -322,6 +363,7 @@ Below is a list of patches and features implemented in Camoufox.

- Automatically generates & injects unique device characteristics into Camoufox based on their real-world distribution
- WebGL fingerprint injection & rotation
- Optional persistent fingerprint seeds for stable generated identities
- Uses the correct system fonts and subpixel antialiasing & hinting based on your target OS
- Avoid proxy detection by calculating your target geolocation, timezone, & locale from your proxy's target region
- Calculate and spoof the browser's language based on the distribution of language speakers in the proxy's target region
Expand Down
62 changes: 50 additions & 12 deletions docs/per-context-patches.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ bundle/

## Python Library Changes

The Camoufox Python package (`pythonlib/`) generates fingerprints for both `NewBrowser` (global CAMOU_CONFIG) and `NewContext` (per-context init script). **BrowserForge is the default for both paths.** Real fingerprint presets are available as an opt-in alternative.
The Camoufox Python package (`pythonlib/`) generates fingerprints for both `NewBrowser` (global CAMOU_CONFIG) and `NewContext` (per-context init script). **BrowserForge is the default for both paths.** Real fingerprint presets are available as an opt-in alternative. Both paths can also take `fingerprint_seed` to reuse the same Camoufox-generated identity values across runs.

### Fingerprint Source Priority

Expand All @@ -623,46 +623,84 @@ The Camoufox Python package (`pythonlib/`) generates fingerprints for both `NewB
| **NewBrowser** (`launch_options()` in `utils.py`) | BrowserForge synthetic | Pass `fingerprint_preset=True` or a preset dict |
| **NewContext** (`generate_context_fingerprint()` in `fingerprints.py`) | BrowserForge synthetic | Pass `preset=dict` explicitly |

### Persistent Fingerprint Seeds

`fingerprint_seed` accepts `str`, `int`, or `bytes`. It derives independent
sub-seeds for each random fingerprint domain so one stable seed can reproduce
the same generated launch identity while keeping BrowserForge, WebGL, font,
voice, audio, canvas, and screen-offset sampling separate. Context-level seeds
apply the supported per-context surfaces for new browser contexts.

```python
from camoufox.sync_api import Camoufox, NewContext

with Camoufox(fingerprint_seed="account-123") as browser:
context = NewContext(browser, fingerprint_seed="account-123:context-2")
```

The seed only controls generated fingerprint values. It does not persist
cookies, local storage, cache, or Playwright browser profile state.

Persistent browser state should be handled separately with Playwright's
persistent context support. Reuse the same `fingerprint_seed` and the same
`user_data_dir` when cookies/storage should remain tied to the same generated
identity:

```python
from pathlib import Path

from camoufox.sync_api import Camoufox

with Camoufox(
persistent_context=True,
user_data_dir=Path("profiles/account-123"),
fingerprint_seed="account-123",
) as context:
...
```

### What Each Path Sets

| Property | Source | Notes |
|----------|--------|-------|
| UA, platform, HWC, oscpu | BrowserForge or preset | UA version patched to match Camoufox Firefox version |
| Screen dims, colorDepth | BrowserForge or preset | Viewport adjusted by -28px for browser chrome |
| WebGL vendor/renderer | `sample_webgl()` from `webgl_data.db` | OS-weighted probability sampling. BrowserForge does NOT generate WebGL (commented out in `browserforge.yml`). Both paths call `sample_webgl()` when WebGL values are missing. |
| Font list | `_generate_random_font_subset()` | Random 30-78% of OS fonts. Essential + marker fonts always included. NOT from presets — generated fresh per call. |
| Font spacing seed | `randint(1, 2^32-1)` | Excludes 0 (0 = no-op in C++) |
| Audio seed | `randint(1, 2^32-1)` | Excludes 0 |
| Canvas seed | `randint(1, 2^32-1)` | Excludes 0 |
| WebGL vendor/renderer | Preset values or `sample_webgl()` from `webgl_data.db` | BrowserForge does NOT generate WebGL (commented out in `browserforge.yml`). Synthetic paths use OS-weighted probability sampling, deterministic when `fingerprint_seed` is supplied. Preset paths use preset WebGL values when present. |
| Font list | `_generate_random_font_subset()` | Random 30-78% of OS fonts, or deterministic when `fingerprint_seed` is supplied. Essential + marker fonts always included. Normally generated fresh per call unless seeded; preset fonts are only used as a fallback if OS font generation fails. |
| Font spacing seed | Random uint32, or derived from `fingerprint_seed` | Excludes 0 (0 = no-op in C++) |
| Audio seed | Random uint32, or derived from `fingerprint_seed` | Excludes 0 |
| Canvas seed | Random uint32, or derived from `fingerprint_seed` | Excludes 0 |
| Timezone | From preset, or Intl.DateTimeFormat fallback in init script | NewBrowser: from preset or geolocation detection. NewContext: preset or browser default. |
| Speech voices | `_generate_random_voice_subset()` | Random 40-80% of OS voices. Essential voices always included. macOS: 6 essentials + random subset of ~184. Windows: all voices (too few to subset). Linux: empty (no native voices). NOT from presets — generated fresh per call. |
| Speech voices | `_generate_random_voice_subset()` | Random 40-80% of OS voices, or deterministic when `fingerprint_seed` is supplied. Essential voices always included. macOS: 6 essentials + random subset of ~184. Windows: all voices (too few to subset). Linux: empty (no native voices). Normally generated fresh per call unless seeded; preset voices are only used as a fallback if OS voice generation fails. |
| WebRTC IP | Not set by default | User sets via `window.setWebRTCIPv4()`. NewContext init script defaults to empty string `""` |
| Geolocation | User parameter or geoip detection | Via Playwright `context.setGeolocation()` |

### Key Files

**`fingerprints.py`** — Per-context fingerprint generation:
- `generate_context_fingerprint()` — main API. Returns `{init_script, context_options, config, preset}`
- `fingerprint_seed.py` — derives independent deterministic sub-seeds for BrowserForge, WebGL, fonts, voices, and noise seeds
- `from_preset()` — converts real preset to CAMOU_CONFIG format
- `from_browserforge()` — converts BrowserForge Fingerprint to CAMOU_CONFIG using `browserforge.yml` mappings
- `_build_init_script()` — generates JavaScript IIFE calling 15 `window.setXxx()` functions with `typeof` guards (`setWebRTCIPv6` is not included — IPv6 is optional and rarely set)
- `_generate_random_font_subset()` — unique random font subset per call (Fisher-Yates, essential + marker fonts always included)
- `_generate_random_voice_subset()` — unique random voice subset per call (essential voices always included, OS-aware)
- `_build_init_script()` — generates JavaScript IIFE calling supported `window.setXxx()` functions with `typeof` guards (`setWebRTCIPv6` is not included — IPv6 is optional and rarely set)
- `_generate_random_font_subset()` — random by default, deterministic when passed a seeded RNG (essential + marker fonts always included)
- `_generate_random_voice_subset()` — random by default, deterministic when passed a seeded RNG (essential voices always included, OS-aware)

**`utils.py`** — Global browser launch configuration:
- `launch_options()` — builds CAMOU_CONFIG env var, Playwright args, and Firefox prefs
- `fingerprint_seed` — optional stable seed for launch-level BrowserForge, preset, font, voice, WebGL, history, and noise generation
- Font subset generated via same `_generate_random_font_subset()` function
- Voice subset generated via same `_generate_random_voice_subset()` function
- WebGL sampled via same `sample_webgl()` function
- Config validated against `properties.json` before serialization

**`fingerprint-presets.json`** — Bundled real fingerprints organized by OS (macOS, Windows, Linux). Each preset includes navigator properties, screen dimensions, WebGL params, speech voices, and timezone. Font and voice data not used from presets — generated fresh per launch.
**`fingerprint-presets.json`** — Bundled real fingerprints organized by OS (macOS, Windows, Linux). Each preset includes navigator properties, screen dimensions, WebGL params, speech voices, and timezone. Fonts and voices are normally generated from OS lists, deterministic when seeded, and preset values are fallback-only if OS generation fails.

**`fonts.json`** — Complete OS-specific font lists for random font subset generation.

**`voices.json`** — Complete OS-specific speech voice lists for random voice subset generation. macOS: 190 voices, Windows: 53 voices, Linux: empty. Format: `"Name:locale:type"` — names extracted at load time.

**`properties.json`** — Includes `audio:seed` and `canvas:seed` as `CAMOU_CONFIG` properties (uint type). These enable the MaskConfig fallback in the audio and canvas patches when using global config without per-context JavaScript.
**`properties.json`** — Includes `audio:seed` and `canvas:seed` as `CAMOU_CONFIG` properties (uint type). These values are generated deterministically when `fingerprint_seed` is supplied; context-level JavaScript only calls setters that the current browser exposes.

**`camoufox.cfg`** — Sets `fission.autostart=true` and `dom.ipc.processPrelaunch.enabled=false`. No `dom.ipc.processCount` override needed with cross-process storage.

Expand Down
42 changes: 42 additions & 0 deletions pythonlib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,48 @@ In addition, it will also calculate your target geolocation, timezone, and local

---

## Persistent Fingerprint Seeds

Pass `fingerprint_seed` when you want Camoufox to reproduce the same generated
fingerprint values across launches or contexts:

```python
from camoufox.sync_api import Camoufox, NewContext

with Camoufox(fingerprint_seed="account-123") as browser:
page = browser.new_page()
page.goto("https://example.com")

context = NewContext(browser, fingerprint_seed="account-123:context-2")
page = context.new_page()
page.goto("https://example.com")
```

The seed controls generated identity values such as BrowserForge sampling,
font and voice subsets, WebGL sampling, and audio/canvas/font config seeds.
Launch-level seeds cover the full generated `CAMOU_CONFIG`; context-level seeds
apply the supported per-context surfaces for new browser contexts. It does not
persist cookies, storage, cache, or profile state.

To keep browser state tied to the same generated identity, reuse the same
`fingerprint_seed` and the same `user_data_dir` with a persistent context:

```python
from pathlib import Path

from camoufox.sync_api import Camoufox

with Camoufox(
persistent_context=True,
user_data_dir=Path("profiles/account-123"),
fingerprint_seed="account-123",
) as context:
page = context.new_page()
page.goto("https://example.com")
```

---

## Installation

First, install the `camoufox` package:
Expand Down
22 changes: 16 additions & 6 deletions pythonlib/camoufox/async_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from camoufox.virtdisplay import VirtualDisplay

from .fingerprint_seed import FingerprintSeed
from .fingerprints import generate_context_fingerprint
from .utils import async_attach_vd, launch_options

Expand Down Expand Up @@ -138,23 +139,26 @@ async def AsyncNewContext(
os: Optional[str] = None,
ff_version: Optional[str] = None,
webrtc_ip: Optional[str] = None,
fingerprint_seed: Optional[FingerprintSeed] = None,
proxy: Optional[Dict[str, str]] = None,
geolocation: Optional[Dict[str, float]] = None,
**context_kwargs: Any,
) -> BrowserContext:
"""
Creates a new browser context with a unique fingerprint identity.

Each context gets its own real fingerprint preset (navigator, screen, WebGL, fonts, etc.)
with unique seeds for audio, canvas, and font spacing noise. All values are applied
via addInitScript so they self-destruct before page scripts can detect them.
By default, each context gets a BrowserForge synthetic fingerprint. Supported
per-context values are applied via addInitScript so they self-destruct before
page scripts can detect them. Pass a preset dict to use a real bundled
fingerprint instead.

Parameters:
browser: A Browser instance from AsyncNewBrowser or AsyncCamoufox.
preset: A specific fingerprint preset dict to use. If None, picks randomly.
os: Target OS for preset selection ("windows", "macos", "linux").
preset: A specific fingerprint preset dict to use. If None, BrowserForge is used.
os: Target OS for synthetic fingerprint generation ("windows", "macos", "linux").
ff_version: Firefox version string for UA patching.
webrtc_ip: IPv4 address to spoof for WebRTC ICE candidates.
fingerprint_seed: Stable seed for Camoufox-generated fingerprint values.
proxy: Per-context proxy (Playwright format: {"server": "...", "username": "...", "password": "..."}).
geolocation: Per-context geolocation ({"latitude": float, "longitude": float}).
**context_kwargs: Additional Playwright new_context() options.
Expand All @@ -169,7 +173,13 @@ async def AsyncNewContext(

fp = await asyncio.get_event_loop().run_in_executor(
None,
lambda: generate_context_fingerprint(preset=preset, os=os, ff_version=ff_version, webrtc_ip=webrtc_ip),
lambda: generate_context_fingerprint(
preset=preset,
os=os,
ff_version=ff_version,
webrtc_ip=webrtc_ip,
fingerprint_seed=fingerprint_seed,
),
)

# Merge generated context options with user overrides (user wins)
Expand Down
63 changes: 63 additions & 0 deletions pythonlib/camoufox/fingerprint_seed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import hashlib
import random
from typing import Union

FingerprintSeed = Union[str, int, bytes]

_SEED_VERSION = b'camoufox:fingerprint-seed:v1'
_UINT32_MAX = 4_294_967_295


def _seed_bytes(seed: FingerprintSeed) -> bytes:
if isinstance(seed, bool):
raise TypeError('fingerprint seed must be str, int, or bytes')
if isinstance(seed, bytes):
return b'bytes:' + seed
if isinstance(seed, int):
return b'int:' + str(seed).encode('ascii')
if isinstance(seed, str):
return b'str:' + seed.encode('utf-8')
raise TypeError('fingerprint seed must be str, int, or bytes')


def _pack_component(value: bytes) -> bytes:
return len(value).to_bytes(8, 'big') + value


def derive_seed(seed: FingerprintSeed, namespace: str, bits: int = 64) -> int:
"""
Derive a deterministic integer seed for a specific fingerprint namespace.

The input is versioned and length-prefixed so different namespaces and seed
types do not collide when their raw byte representation overlaps.
"""
if not isinstance(namespace, str):
raise TypeError('namespace must be a string')
if not namespace:
raise ValueError('namespace is required')
if isinstance(bits, bool) or not isinstance(bits, int):
raise TypeError('bits must be an integer')
if bits < 1 or bits > 256:
raise ValueError('bits must be between 1 and 256')

material = b''.join((
_pack_component(_SEED_VERSION),
_pack_component(namespace.encode('utf-8')),
_pack_component(_seed_bytes(seed)),
))
value = int.from_bytes(hashlib.sha256(material).digest(), 'big')
return value >> (256 - bits)


def deterministic_rng(seed: FingerprintSeed, namespace: str) -> random.Random:
"""
Return a local random generator derived from a fingerprint seed namespace.
"""
return random.Random(derive_seed(seed, namespace))


def derive_uint32_seed(seed: FingerprintSeed, namespace: str) -> int:
"""
Derive a non-zero uint32 seed for Camoufox fingerprint noise settings.
"""
return derive_seed(seed, namespace) % _UINT32_MAX + 1
Loading