mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-04 16:00:21 +00:00
fix: stop generating a canvas seed, and drop config keys nothing reads
The browser has not noised the canvas since #528, and no patch reads canvas:seed (#721). The launcher still drew one on every launch and sent it through CAMOU_CONFIG, and NewContext called a setCanvasSeed that does not exist. They no longer do. For users this changes nothing on any browser since #528: the value was ignored. A config that still passes canvas:seed gets the usual "Skipping unknown patch" notice instead of silence. On a browser from before #528, the launcher no longer turns canvas noise on, which is the behaviour #528 chose. The same audit found more keys declared in settings/properties.json that no patch or Juggler file reads, so setting them did nothing: - canvas:aaOffset, canvas:aaCapOffset - memorysaver, pdfViewerEnabled, webrtc:localipv4/6 - navigator.onLine, navigator.cookieEnabled, navigator.languages - navigator.appCodeName, appName, product, productSub. Firefox reports these constants itself, so fpgen.yml no longer maps them. - webGl:parameters:blockIfNotDefined and its WebGL2 twin test_config_schema now checks this direction too: every declared key must be read by the browser, unless it is listed with a reason. Three are listed: locale:script and navigator.doNotTrack, which the launcher applies itself, and navigator.buildID (#780). The build-tester grading followed the same wrong premise. It tracked canvas collisions as an unfixed per-context leak. A canvas that is rendered rather than noised follows the fonts and GPU, as it does on real machines, so canvas collisions are now counted with the other device-level values. The tribal rule that recorded it as an open question is now a settled one, canvas-is-not-noised, with an automated check. Closes #721. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6a22248c0b
commit
a3dbe40d1a
@@ -196,7 +196,7 @@ async def AsyncNewContext(
|
||||
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
|
||||
with its own audio noise seed. All values are applied
|
||||
via addInitScript so they self-destruct before page scripts can detect them.
|
||||
|
||||
Parameters:
|
||||
|
||||
@@ -1644,12 +1644,11 @@ def from_preset(preset: Dict, ff_version: Optional[str] = None, salt: Optional[i
|
||||
if webgl.get('unmaskedRenderer'):
|
||||
config['webGl:renderer'] = webgl['unmaskedRenderer']
|
||||
|
||||
# Generate unique random seeds per launch (1 to 2^32-1, excluding 0 which is a no-op in C++)
|
||||
# Generate a unique audio seed per launch (1 to 2^32-1, excluding 0 which is a no-op in C++)
|
||||
# fonts:spacing_seed stays 0 (off): glyph-advance perturbation produces text
|
||||
# widths no real machine emits (see launch_options in utils.py).
|
||||
config['fonts:spacing_seed'] = 0
|
||||
config['audio:seed'] = randint(1, 4_294_967_295) # nosec
|
||||
config['canvas:seed'] = randint(1, 4_294_967_295) # nosec
|
||||
|
||||
if preset.get('timezone'):
|
||||
config['timezone'] = preset['timezone']
|
||||
@@ -1700,7 +1699,6 @@ def _build_init_script(values: Dict[str, Any]) -> str:
|
||||
setters = [
|
||||
('fontSpacingSeed', 'setFontSpacingSeed', '{val}'),
|
||||
('audioFingerprintSeed', 'setAudioFingerprintSeed', '{val}'),
|
||||
('canvasSeed', 'setCanvasSeed', '{val}'),
|
||||
('navigatorPlatform', 'setNavigatorPlatform', '{val}'),
|
||||
('navigatorOscpu', 'setNavigatorOscpu', '{val}'),
|
||||
('navigatorUserAgent', 'setNavigatorUserAgent', '{val}'),
|
||||
@@ -1820,7 +1818,6 @@ def generate_context_fingerprint(
|
||||
# Add seeds (the generator doesn't produce these)
|
||||
config.setdefault('fonts:spacing_seed', 0) # perturbation off; see utils.launch_options
|
||||
config.setdefault('audio:seed', randint(1, 4_294_967_295)) # nosec
|
||||
config.setdefault('canvas:seed', randint(1, 4_294_967_295)) # nosec
|
||||
|
||||
# Determine target OS from platform for font/voice generation
|
||||
plat = config.get('navigator.platform', '')
|
||||
@@ -1917,7 +1914,6 @@ def generate_context_fingerprint(
|
||||
init_values: Dict[str, Any] = {
|
||||
'fontSpacingSeed': config.get('fonts:spacing_seed'),
|
||||
'audioFingerprintSeed': config.get('audio:seed'),
|
||||
'canvasSeed': config.get('canvas:seed'),
|
||||
'navigatorPlatform': nav.get('platform'),
|
||||
'navigatorOscpu': config.get('navigator.oscpu'),
|
||||
'navigatorUserAgent': config.get('navigator.userAgent'),
|
||||
|
||||
@@ -18,13 +18,10 @@ navigator:
|
||||
# fpgen's UAs trail the current release; the version is rewritten to the
|
||||
# Camoufox Firefox version in _cast_to_properties.
|
||||
userAgent: navigator.userAgent
|
||||
appCodeName: navigator.appCodeName
|
||||
appName: navigator.appName
|
||||
appVersion: navigator.appVersion
|
||||
oscpu: navigator.oscpu
|
||||
platform: navigator.platform
|
||||
hardwareConcurrency: navigator.hardwareConcurrency
|
||||
product: navigator.product
|
||||
# Never override productSub (#105)
|
||||
# deviceMemory is not in Firefox, and fpgen reports the string "undefined"
|
||||
# Locale is handled separately (locale:*)
|
||||
|
||||
@@ -174,7 +174,7 @@ def NewContext(
|
||||
Creates a new browser context with a unique fingerprint identity.
|
||||
|
||||
Each context gets its own real fingerprint preset
|
||||
with unique seeds for audio, canvas, and font spacing noise. All values are applied
|
||||
with its own audio noise seed. All values are applied
|
||||
via addInitScript so they self-destruct before page scripts can detect them.
|
||||
|
||||
Parameters:
|
||||
|
||||
@@ -994,7 +994,7 @@ def launch_options(
|
||||
_user_set_dnt = 'navigator.doNotTrack' in config
|
||||
_user_set_gpc = 'navigator.globalPrivacyControl' in config
|
||||
_user_set_accept_encoding = 'headers.Accept-Encoding' in config
|
||||
_user_set_noise_seeds = {k for k in ('audio:seed', 'canvas:seed') if k in config}
|
||||
_user_set_audio_seed = 'audio:seed' in config
|
||||
|
||||
# The salt that makes every seeded draw belong to this identity (see
|
||||
# fingerprints.identity_salt): stable when the caller pinned the identity
|
||||
@@ -1274,16 +1274,14 @@ def launch_options(
|
||||
# on every measureText), which is a fingerprint no stock Firefox emits.
|
||||
# Pass fonts:spacing_seed explicitly to opt back in.
|
||||
set_into(config, 'fonts:spacing_seed', 0)
|
||||
# audio/canvas noise seeds follow the identity: a returning "same device"
|
||||
# must reproduce its audio and canvas hashes (#442/#765). Derived, not
|
||||
# equal, so the two streams differ; never 0 (0 disables the noise).
|
||||
# A preset draws its own random seeds; they are replaced here too so a
|
||||
# pinned preset reproduces them, but a seed the caller set is kept.
|
||||
_ident = identity_seed(config, _identity_salt)
|
||||
if 'audio:seed' not in _user_set_noise_seeds:
|
||||
# The audio noise seed follows the identity: a returning "same device" must
|
||||
# reproduce its audio hash (#442/#765). Never 0 (0 disables the noise). A
|
||||
# preset draws its own random seed; it is replaced here too so a pinned
|
||||
# preset reproduces it, but a seed the caller set is kept. There is no
|
||||
# canvas seed: the browser adds no canvas noise (#528).
|
||||
if not _user_set_audio_seed:
|
||||
_ident = identity_seed(config, _identity_salt)
|
||||
config['audio:seed'] = ((_ident * 2654435761 + 97) & 0xFFFFFFFF) or 1
|
||||
if 'canvas:seed' not in _user_set_noise_seeds:
|
||||
config['canvas:seed'] = ((_ident * 40503 + 12345) & 0xFFFFFFFF) or 1
|
||||
|
||||
# Set geolocation
|
||||
if geoip:
|
||||
|
||||
@@ -38,6 +38,18 @@ MASKCONFIG_READ = re.compile(r'MaskConfig::(?:Get|Has)\w*\(\s*"([^"]+)"')
|
||||
# Add here (with a reason) only when the read genuinely cannot name its key.
|
||||
ALLOWED_UNDECLARED: set = set()
|
||||
|
||||
# Declared keys the browser never reads, each with the reason it is declared
|
||||
# anyway. Everything else in properties.json must be read by a patch or by
|
||||
# Juggler: a key nothing reads does nothing, silently.
|
||||
NOT_READ_BY_THE_BROWSER = {
|
||||
"locale:script": "the launcher joins it with locale:language/region into the UI locale",
|
||||
"navigator.doNotTrack": "the launcher applies it as privacy.donottrackheader.enabled (#760)",
|
||||
"navigator.buildID": "declared ahead of the patch that reads it (#780)",
|
||||
}
|
||||
|
||||
# How Juggler and the patches name a key: a quoted string literal.
|
||||
QUOTED = '"{key}"', "'{key}'"
|
||||
|
||||
|
||||
def _sources():
|
||||
for pattern in ("patches/**/*.patch", "additions/**/*"):
|
||||
@@ -97,6 +109,34 @@ def test_every_key_the_browser_reads_is_declared():
|
||||
pytest.fail("\n".join(lines))
|
||||
|
||||
|
||||
def test_every_declared_key_is_read_by_the_browser():
|
||||
"""The other direction: canvas:seed (#721) was declared, generated by both
|
||||
launchers and sent on every launch for three releases after the patch that
|
||||
read it was removed (#528)."""
|
||||
text = "".join(path.read_text(errors="ignore") for path in _sources())
|
||||
unread = sorted(
|
||||
key
|
||||
for key in _declared_keys()
|
||||
if key not in NOT_READ_BY_THE_BROWSER
|
||||
and not any(form.format(key=key) in text for form in QUOTED)
|
||||
)
|
||||
assert not unread, (
|
||||
"settings/properties.json declares keys that no patch or Juggler file "
|
||||
f"reads, so setting them does nothing: {unread}. Remove them, or add them "
|
||||
"to NOT_READ_BY_THE_BROWSER with the reason they are declared."
|
||||
)
|
||||
|
||||
|
||||
def test_keys_not_read_by_the_browser_are_really_unread():
|
||||
"""An exemption must lapse once the browser starts reading the key."""
|
||||
text = "".join(path.read_text(errors="ignore") for path in _sources())
|
||||
now_read = [
|
||||
key for key in NOT_READ_BY_THE_BROWSER
|
||||
if any(form.format(key=key) in text for form in QUOTED)
|
||||
]
|
||||
assert not now_read, f"remove from NOT_READ_BY_THE_BROWSER, the browser reads them now: {now_read}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", ["media:spoof_codecs"])
|
||||
def test_known_previously_missing_keys_stay_declared(key):
|
||||
"""Pin the specific keys this guard was written for, so a schema edit that
|
||||
|
||||
@@ -42,7 +42,7 @@ def launch(**kwargs):
|
||||
return config_of(utils.launch_options(**kwargs))
|
||||
|
||||
|
||||
DRAWN = ("canvas:seed", "audio:seed", "fonts", "voices", "webGl:renderer")
|
||||
DRAWN = ("audio:seed", "fonts", "voices", "webGl:renderer")
|
||||
|
||||
|
||||
def drawn(config):
|
||||
@@ -51,7 +51,7 @@ def drawn(config):
|
||||
|
||||
class TestUnpinnedLaunchesAreDistinct:
|
||||
def test_noise_seeds_do_not_collide(self):
|
||||
seeds = [launch()["canvas:seed"] for _ in range(40)]
|
||||
seeds = [launch()["audio:seed"] for _ in range(40)]
|
||||
# 40 draws from 2**32: any collision means the seed space collapsed.
|
||||
assert len(set(seeds)) == len(seeds)
|
||||
|
||||
@@ -77,7 +77,7 @@ class TestPinnedIdentityIsStable:
|
||||
pytest.skip("no presets bundled")
|
||||
first = launch(os="windows", fingerprint_preset=preset)
|
||||
second = launch(os="windows", fingerprint_preset=preset)
|
||||
assert (first["canvas:seed"], first["audio:seed"]) == (second["canvas:seed"], second["audio:seed"])
|
||||
assert first["audio:seed"] == second["audio:seed"]
|
||||
assert first["fonts"] == second["fonts"]
|
||||
|
||||
@pytest.mark.parametrize("os_name", ["windows", "macos", "linux"])
|
||||
@@ -101,9 +101,17 @@ class TestPinnedIdentityIsStable:
|
||||
assert config.get("webGl:parameters"), (os_name, i)
|
||||
sample_webgl(key, config["webGl:vendor"], config["webGl:renderer"])
|
||||
|
||||
def test_caller_seeds_are_kept(self):
|
||||
config = launch(config={"canvas:seed": 7, "audio:seed": 9})
|
||||
assert (config["canvas:seed"], config["audio:seed"]) == (7, 9)
|
||||
def test_caller_seed_is_kept(self):
|
||||
assert launch(config={"audio:seed": 9})["audio:seed"] == 9
|
||||
|
||||
|
||||
def test_no_canvas_seed_is_generated():
|
||||
"""The browser adds no canvas noise (#528), and no patch reads canvas:seed
|
||||
(#721). Generating one only sent the browser a value it ignored."""
|
||||
assert "canvas:seed" not in launch()
|
||||
context = fp.generate_context_fingerprint(os="linux")
|
||||
assert "canvas:seed" not in context["config"]
|
||||
assert "setCanvasSeed" not in context["init_script"]
|
||||
|
||||
def test_salt_of_equal_objects_is_equal(self):
|
||||
a = fp.generate_fingerprint(os="windows")
|
||||
|
||||
Reference in New Issue
Block a user