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
| field | meaning |
|---|---|
bauta | 1: this format. |
name | The 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. |
os | windows, macos or linux. See os for what it fixes. |
os_version, architecture, bitness | Optional. The high-entropy Client Hints: platformVersion; x86 or arm; 64 or 32. |
brand | Optional: "chrome", the default and the only brand of this release. Another Chromium-family brand ("edge", or {"name", "version", "token"}) is refused. |
hardware | cores (hardwareConcurrency) and memory_gb (deviceMemory and the Device-Memory client hint: a bucket desktop Chrome reports, 2, 4, 8, 16 or 32). |
screen | The monitor, in CSS pixels. See screen for its keys. |
gpu | WebGL's unmasked vendor and renderer. The limits that differ by graphics backend follow from it. |
webgl | Optional: 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. |
voices | Optional: the speech voices the device's OS has, [{"name", "lang"}], in place of what os implies (Windows' English (United States) voices). |
window | Optional: {"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. |
noise | Which readbacks get deterministic noise: canvas, audio, webgl. See noise. |
rules | Optional: 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.platformanduserAgentData.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
| key | meaning |
|---|---|
width, height | The monitor, in CSS pixels. |
avail_width, avail_height | The area left by the taskbar or menu bar. |
dpr | The 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_top | Optional: 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_depth | Optional: 24 or 30. |
color_gamut | Optional: "srgb", "p3" or "rec2020". |
hdr | Optional. |
refresh_rate | In 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
| value | readbacks |
|---|---|
canvas | getImageData, toDataURL, toBlob |
audio | getChannelData, copyFromChannel, which read the same samples alike |
webgl | readPixels |
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
deviceMemorythat 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}| field | meaning |
|---|---|
label | The call to answer, named as the binding layer names it (see labels). |
arg0 | Optional. 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. |
return | The answer. Any JSON value. |
perturb | Use instead of return to add deterministic noise to the call's own result: canvas, audio or readpixels. |
keep | Use 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
| call | label |
|---|---|
reading an attribute, such as navigator.platform | Navigator.get platform |
| writing an attribute | Interface.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
arg0wins when the call's first argument equals it. Otherwise the first rule withoutarg0for 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 platformandNavigatorUAData.getHighEntropyValuesset the User-Agent and the Client Hints for every request, page and worker alike, andappVersionfollows the User-Agent. Only the OS part of auserAgentis 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,availWidthandavailHeightrules set the screen itself, soscreen.*,matchMediaand 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
Int32ArrayorFloat32Arrayand the rule gives a list, the numbers are written into that typed array, which stays a typed array. Examples areMAX_VIEWPORT_DIMSandALIASED_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.languagesis.
- If the real call returns a Promise (
perturb. Adds deterministic noise to the result, seeded bynameand 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 WebGLreadPixels(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:
| key | what it does |
|---|---|
timezone, locale | Such as "America/New_York" and "en-US". With a proxy and neither stated, they follow where the proxy exits (below). |
proxy | Such 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. |
humanize | true 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. |
ephemeral | true for a persona that keeps nothing between sessions, such as a crawler's. |
requests | Spends 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.
| preset | blocks |
|---|---|
safe | audio, video and ad networks' requests |
balanced | also third parties' images and fonts |
aggressive | every 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
- How personas work: what a persona is, and where it lives.
- Proxies: a persona behind a proxy.
- Bandwidth: spending fewer of a proxy's bytes.