bautaDocs

Persona File Format

The JSON format of a persona file: its fields, its label rules, and the launch settings it may also set.

A persona file is JSON that states a device's facts: its OS, hardware, screen, GPU and the noise its readbacks get. bauta compiles it into everything a site can read, so the surfaces never contradict each other. Changing a persona needs no rebuild: edit the file and relaunch.

Example

{
  "bauta": 1,
  "name": "win-intel",
  "os": "windows", "os_version": "15.0.0", "architecture": "x86", "bitness": "64",
  "hardware": {"cores": 8, "memory_gb": 8},
  "screen": {"width": 1536, "height": 864, "avail_width": 1536, "avail_height": 824, "dpr": 1.25},
  "gpu": {"vendor": "Google Inc. (Intel)", "renderer": "ANGLE (Intel, ... Direct3D11 vs_5_0 ps_5_0, D3D11)"},
  "noise": ["canvas", "audio", "webgl"]
}

personas/win-intel.json, in the bauta-browser repository, is a complete example: a Windows 11 laptop with Intel UHD 630 graphics. Pair it with the Windows font environment, personas/fonts/win.conf.

Make a persona from a file with bauta persona new <name> --from <file>. When you launch the browser yourself, BAUTA_PLUGIN_CONFIG names the file by its absolute path, and the browser prints [bauta] persona: <file> on stderr.

Fields

fieldmeaning
bauta1: this format.
nameThe persona's seed. All noise is derived from it and your license's secret, so one name reads the same on every launch, and two names, or two licenses, read differently.
oswindows, macos or linux. See os for what it fixes.
os_version, architecture, bitnessOptional. The high-entropy Client Hints: platformVersion; x86 or arm; 64 or 32.
brandOptional: "chrome", the default and the only brand of this release. Another Chromium-family brand ("edge", or {"name", "version", "token"}) is refused.
hardwarecores (hardwareConcurrency) and memory_gb (deviceMemory and the Device-Memory client hint: a bucket desktop Chrome reports, 2, 4, 8, 16 or 32).
screenThe monitor, in CSS pixels. See screen for its keys.
gpuWebGL's unmasked vendor and renderer. The limits that differ by graphics backend follow from it.
webglOptional: the device's whole WebGL set, {"webgl1", "webgl2"}, each with its params, extensions and max_anisotropy, as a capture of the device reads them. A page then reads that device's parameters and extension list, not those the backend implies.
voicesOptional: the speech voices the device's OS has, [{"name", "lang"}], in place of what os implies (Windows' English (United States) voices).
windowOptional: {"frame": [w, h]}, what the device's browser window adds around its pages: outerWidth - innerWidth and outerHeight - innerHeight as a page there reads them (a capture of the device gives them). bauta's own window, on a virtual display, has no frame; a page reads this one.
noiseWhich readbacks get deterministic noise: canvas, audio, webgl. See noise.
rulesOptional: label rules, applied before what the facts compile to.

Other top-level keys, such as _doc, are ignored. The Chrome version is never a field: bauta reports its own.

os

os fixes, as that OS's Chrome reports or draws them:

  • the User-Agent's platform part, navigator.platform and userAgentData.platform;
  • the set of APIs a page finds: Web Share and Web Bluetooth on Windows and macOS, the Barcode Detection API on macOS;
  • the speech voices: Windows' own on a Windows persona;
  • the CSS system colors and selection colors.

This release refuses Android personas.

screen

keymeaning
width, heightThe monitor, in CSS pixels.
avail_width, avail_heightThe area left by the taskbar or menu bar.
dprThe device pixel ratio: 1.25 for a 1920x1080 panel at Windows' 125 % scaling. With a dpr, bauta draws pages at that scale, so devicePixelRatio, the screen and the resolution media queries agree.
avail_topOptional: where the available area starts, as below a Mac's menu bar. By default all the height the area leaves on macOS, 0 elsewhere.
color_depthOptional: 24 or 30.
color_gamutOptional: "srgb", "p3" or "rec2020".
hdrOptional.
refresh_rateIn Hz, 60 by default. bauta's frames come no faster, whatever this machine's display does.

By default, a Mac's screen has 30 bits in P3, and others 24 in sRGB, with no HDR.

noise

valuereadbacks
canvasgetImageData, toDataURL, toBlob
audiogetChannelData, copyFromChannel, which read the same samples alike
webglreadPixels

Noise lands only where a renderer shades, such as antialiased text and edges, and gradients. What Chrome draws exactly (a fill, a clear, a transparent pixel) reads exactly, and a pixel reads alike however it is read: the whole canvas, a part of it, or its export.

Refused personas

A persona that contradicts itself is refused, and bauta says why on stderr ([bauta] persona refused, <file>: <reason>). The page then sees the real machine. Among the contradictions:

  • a GPU renderer that names another OS's graphics backend (Direct3D11 on Windows, Metal on macOS);
  • a deviceMemory that desktop Chrome does not report;
  • an available screen area larger than the screen;
  • a User-Agent rule that names another Chrome version;
  • Edge at another major version;
  • an Android persona.

Label rules

Label rules answer one WebAPI call each, at the exit of the call, instead of the machine. They extend a persona (its rules field) where its facts do not reach. A file of only rules, {"name": ..., "rules": [...]}, is a persona too, answered rule by rule without the checks above.

{"label": "WebGLRenderingContext.getParameter", "arg0": 3379, "return": 16384}
fieldmeaning
labelThe call to answer, named as the binding layer names it (see labels).
arg0Optional. The rule matches only when the call's first argument is this number, such as a getParameter enum, or this string, such as a getExtension name.
returnThe answer. Any JSON value.
perturbUse instead of return to add deterministic noise to the call's own result: canvas, audio or readpixels.
keepUse instead of return: true lets the call's own result stand, so a rule with arg0 can exempt a call from the label's rule without arg0.

Labels

calllabel
reading an attribute, such as navigator.platformNavigator.get platform
writing an attributeInterface.set name
calling a method, such as gl.getParameter(…)WebGLRenderingContext.getParameter
a constructor, such as new AudioContext()AudioContext.AudioContext

The interface is the one whose WebIDL declares the member. A mixin's members carry the name of the interface that includes the mixin: navigator.hardwareConcurrency is Navigator.get hardwareConcurrency, and a 2D canvas's getImageData is CanvasRenderingContext2D.getImageData. WebGL 1 and WebGL 2 are separate interfaces (WebGLRenderingContext, WebGL2RenderingContext), so give each one its rules.

A page's workers answer as the page does. A Navigator.* rule also answers WorkerNavigator.* in a dedicated, shared or service worker, and a CanvasRenderingContext2D.* rule also answers a worker's OffscreenCanvasRenderingContext2D.*. A rule for the worker's own label wins over the page's.

How a rule answers

  • Matching. A rule with arg0 wins when the call's first argument equals it. Otherwise the first rule without arg0 for that label applies. So {"label": "WebGLRenderingContext.getExtension", "return": null} with {"label": "WebGLRenderingContext.getExtension", "arg0": "OES_texture_float", "keep": true} offers that one extension alone.
  • The User-Agent family is answered by the browser. The rules for Navigator.get userAgent, NavigatorUAData.get platform and NavigatorUAData.getHighEntropyValues set the User-Agent and the Client Hints for every request, page and worker alike, and appVersion follows the User-Agent. Only the OS part of a userAgent is used: bauta writes the Chrome version itself, so a persona never claims another version.
  • The screen is answered where it is drawn. Screen.get width, height, availWidth and availHeight rules set the screen itself, so screen.*, matchMedia and stylesheets read the same one.
  • return. Scalars, arrays and objects become JavaScript values.
    • If the real call returns a Promise (userAgentData.getHighEntropyValues, navigator.getBattery, …), the answer is handed back as a resolved Promise.
    • If the real call returns an Int32Array or Float32Array and the rule gives a list, the numbers are written into that typed array, which stays a typed array. Examples are MAX_VIEWPORT_DIMS and ALIASED_LINE_WIDTH_RANGE.
    • An attribute's array or object is the same object on every read, and an array is frozen, as Chrome's navigator.languages is.
  • perturb. Adds deterministic noise to the result, seeded by name and small enough that an image or a sound does not visibly change.
    • canvas: the pixels a call returns (CanvasRenderingContext2D.getImageData), and the same noise on the canvas's exports (toDataURL, toBlob).
    • audio: the samples a call returns (AudioBuffer.getChannelData), and the same samples copied out (AudioBuffer.copyFromChannel).
    • readpixels: the destination buffer of WebGL readPixels (RGBA / UNSIGNED_BYTE reads).

Launch settings

Under the gateway, a persona file may also set how its browser runs. These keys sit beside the facts:

keywhat it does
timezone, localeSuch as "America/New_York" and "en-US". With a proxy and neither stated, they follow where the proxy exits (below).
proxySuch as "http://user:password@host:port" or "socks5://user:password@host:port", with any :, @ or / in the user or password %-escaped. If the proxy turns the credentials away, requests fail at once, and bauta serve says so.
humanizetrue makes clicks and typing move as a hand does.
third_party_cookies"block" blocks third-party cookies, as for a person who chose "Block third-party cookies" in Chrome's settings. Otherwise they are allowed, Chrome's default.
ephemeraltrue for a persona that keeps nothing between sessions, such as a crawler's.
requestsSpends fewer of a proxy's bytes: a preset and your own rules (below).
{
  "timezone": "America/New_York",
  "locale": "en-US",
  "proxy": "socks5://user:password@host:port",
  "humanize": true,
  "third_party_cookies": "block",
  "requests": {"preset": "balanced"}
}

Timezone and locale from the proxy

With a proxy, and neither timezone nor locale stated, they follow where the proxy exits. bauta serve asks an IP echo through the proxy (BAUTA_IP_ECHO, https://api.ipify.org by default) and places the address with the database bauta geo update keeps. If no place can be found, and none was found before, the persona's browser does not start, rather than show this machine's timezone behind a foreign address.

Timezone and locale come from V8 and ICU, outside the binding layer, so they are launch settings. When you launch the browser yourself, set TZ, and LANGUAGE and LANG (such as en_US and en_US.UTF-8).

IP geolocation by DB-IP, under CC BY 4.0.

requests

requests takes a preset, "safe", "balanced" or "aggressive", and rules. The browser blocks in its own network layer, with its HTTP cache on.

presetblocks
safeaudio, video and ad networks' requests
balancedalso third parties' images and fonts
aggressiveevery image and font

rules adds your own, in Chrome's declarativeNetRequest format (block, allow, and modifyHeaders of request headers). Whatever a preset or a rule says, a bot-protection vendor's challenges and CAPTCHAs load in full, as a page must to pass them. bauta traffic <name> shows what the persona's pages fetched, by kind, what was blocked and what the cache answered.

Writing a coherent persona

A fingerprinter compares surfaces against each other. The facts above keep most of them in step; what is left to you:

  • The values are ones real devices of that class report: a GPU that OS's machines ship with, the cores and memory that go with it, a common screen for it.
  • The screen is at least as large as the window you launch (--window-size).
  • Pair the persona with the matching font environment (personas/fonts/).

Next

On this page