# IPHey inconsistent browser fingerprint (whale), explained

> IPHey's inconsistent browser fingerprint (whale) looked like a version problem. We followed it through branding and JavaScript to a Widevine certificate.

Source: https://mosaicbrowser.com/blog/iphey-whale-signal

## IPHey's "(whale)" signal, and what it was really about

IPHey's inconsistent browser fingerprint (whale) looked like a version problem. We followed it through branding and JavaScript to a Widevine certificate.

![Tesseract versus Chrome, each name in its own tile: Following “(whale)” to the end](https://mosaicbrowser.com/covers/iphey-whale-signal.webp)

IPHey's "inconsistent browser fingerprint (whale)" is, as far as we can tell, its matcher's nearest guess for a Chromium build it cannot place. Naver Whale is a Chromium browser, so an unknown Chromium lands nearest to it. We followed that label through the Chrome version, through branding and through the whole JavaScript surface. It ended at the Widevine DRM module sending a device certificate in the clear, which any build Google has not signed does.

Every desktop profile we made got the signal. It cost us a long time, and most of it went on theories that were true and beside the point.

## First theory: the version is too new

The BROWSER card had been red for a long time, and the excuse in our detector runner said Google's own build of the same Chrome major scored the same. We re-measured that properly, with the stock browser relaying through the profile's own proxy, so both browsers left from the same US residential exit in the same minute.

```
card        Tesseract Chrome 153        Chrome for Testing 153
headline    Unreliable                  Unreliable
BROWSER     inconsistent (whale)        inconsistent (whale)
LOCATION    green, United States        red, masked or inconsistent
MX SCORE    90                          80

```

The `(whale)` string was byte-identical, from Google's own unmodified binary. And the Chrome 152 installed on the same Mac scored Trustworthy 100. That read like a database: IPHey had not catalogued Chromium 153 yet, so it reached for the nearest thing it had. We pinned the excuse to the version so it would expire by itself when the engine moved on.

That control did prove something real. Chrome for Testing carries none of our patches, so the label was not caused by any of them. What it did not prove was that the version was the cause.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## Second theory: branding

Chrome for Testing is unbranded, like every build that is not Google Chrome itself. So the next question was what a branded Chrome has that an unbranded Chromium does not. We found two things, and both were real.

The first was the speech voice list. `speechSynthesis.getVoices()` returns 199 voices in Google Chrome on our Mac and 180 in any unbranded build, because nineteen Google network voices come from a component that ships only in branded Chrome. A page reads that in one call. Patch `0145` restores them, and the story is in [199 voices](https://mosaicbrowser.com/blog/chromium-speech-voices).

The second was field trials. An unbranded build compiles in a testing config that gives it twenty API-surface entries Chrome does not have and takes away ones it does. `--disable-field-trial-config` moves all twenty, written up in [twenty APIs Chrome doesn't have](https://mosaicbrowser.com/blog/twenty-apis-chrome-lacks).

Fixing both made our engine closer to Chrome. Neither moved IPHey.

## Third theory: something in JavaScript

So we compared everything a page script can see, and then the order it sees it in. Sorted property lists cannot show enumeration order, and enumeration order is readable. Unsorted, with the field-trial switch on, our engine was byte-identical to branded Chrome across `window`, `Navigator`, `Screen`, `Document`, `Element`, `Performance`, the canvas and WebGL contexts, `MediaDevices`, `window.chrome` and more.

Below JavaScript it matched too: all fourteen request headers in order and case, zero differing fields in the TLS ClientHello, zero in the HTTP/2 settings. About fifty surfaces in all. And IPHey still scored 90 where branded Chrome scored 100.

That ruled a lot out. The unpatched Chrome for Testing had already ruled out our patches, and now the surfaces they touch were ruled out as well, so a patch bisect would have hunted for something that was not there.

## What IPHey's inconsistent browser fingerprint (whale) was: Widevine

What found it was reading what IPHey collects rather than what we assumed it collects. Its detector is a wasm-bindgen module, and wasm-bindgen names every import after the thing it reads. We wrapped the module's import table and logged every call, which gave us the whole collection in plain text. One field differed: a media key session message of 1704 bytes from our engine, against 2 bytes from Chrome.

Chrome's two bytes ask for a service certificate so the Widevine module can encrypt its client ID before sending anything. That privacy mode is only for a browser that passes Verified Media Path, which needs a Google signature over the browser binary. Our engine cannot carry one, so its first message was a licence request with the per-install device certificate inside it, readable.

```
ours, unmodified          Unreliable    MX  90
ours, EME made to reject  Trustworthy   MX 100
branded Chrome 153        Trustworthy   MX 100

```

That certificate is the same for every profile on the machine and survives every value a fingerprint generator redraws. The full measurement, and the three fixes we tried that changed nothing, are in [one key for every profile](https://mosaicbrowser.com/blog/widevine-device-certificate).

## What we changed, and what we kept

Widevine is off in Mosaic unless a profile asks for it, under Privacy › Leak protection › Allow Widevine DRM playback. A page asking for it is told it is not supported, which is readable too, but it is not a key that joins your profiles together.

We kept the voice and field-trial fixes, because a Chromium that says it is Chrome should not fail a one-line check. And we kept the habit the score taught us, written up in [IPHey's score moved with the address](https://mosaicbrowser.com/blog/iphey-score-moves-with-address): a control on the same exit in the same minute, and several rounds, before believing any verdict.

## Questions

### What does IPHey's "(whale)" mean?

Our reading is that it is the nearest match IPHey's matcher finds for a Chromium it cannot place, and Naver Whale is a Chromium browser. It does not mean you are running Whale. Google's own unbranded Chrome for Testing got the identical label.

### What caused "Detected an inconsistent browser fingerprint (whale)" for us?

The Widevine DRM module. In a browser Google has not signed, its first licence message carried a per-install device certificate in the clear, 1704 bytes where Google Chrome sent 2. With Widevine refused, the same profile scored Trustworthy 100.

### Can I fix the whale signal by changing my user agent?

Not in our case. Every JavaScript surface we compared, including property enumeration order, already matched branded Chrome, and the TLS handshake and HTTP/2 settings matched too. The difference was below all of that.

### Does Mosaic turn Widevine off?

Yes, by default. A profile that needs Netflix or Spotify can turn on Allow Widevine DRM playback under Privacy, Leak protection, knowing that the certificate is the same for every profile on the computer.

## Related

[Widevine device certificate privacy: one key for every profile](https://mosaicbrowser.com/blog/widevine-device-certificate): Widevine device certificate privacy needs a Google-signed browser. Without it, the first licence message is 1704 bytes where Chrome sends 2.

[199 voices: how a speech list tells Chromium from Chrome](https://mosaicbrowser.com/blog/chromium-speech-voices): speechSynthesis.getVoices() lists 199 voices in Google Chrome on our Mac and 180 in any unbranded Chromium. A page reads the difference in one call.

[Twenty APIs Chrome doesn't have: Chromium's field trials](https://mosaicbrowser.com/blog/twenty-apis-chrome-lacks): Unbranded Chromium runs its field-trial testing config at every launch. Against Chrome 153 that was 20 API differences, and one switch moved them all.

[IPHey's score moved with the address, not the browser](https://mosaicbrowser.com/blog/iphey-score-moves-with-address): IPHey score changes even on an untouched Chrome: 100, 100, 70 and 0 in one afternoon. The score follows the exit and the moment; the browser card does not.

[A detector that never answered because its tab was in the background](https://mosaicbrowser.com/blog/requestanimationframe-background-tab): Background tab throttling stops requestAnimationFrame. Our test tab was in the background, two detectors never answered, and Page.bringToFront fixed both.

[When the FingerprintJS demo can't name your browser](https://mosaicbrowser.com/blog/fingerprintjs-cannot-name-browser): FingerprintJS read 'Chromium-Based Browser Not Available' for our engine. We blamed the version; the real variable was Google's branding.

[Pixelscan's masking verdict, bisected to one surface](https://mosaicbrowser.com/blog/pixelscan-masking-bisected): We bisected Pixelscan's masking detected verdict on one profile. Canvas, WebGL, audio and client rects were fine. Hiding about 2% of font families caused it.

[The canvas method nobody noised: what a worker test found](https://mosaicbrowser.com/blog/the-canvas-method-nobody-noised): Our canvas noise reached toDataURL and missed toBlob, so a page calling toBlob got a PNG byte-identical to stock Chrome's. A worker test caught it.

## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
