This is the payoff post. Part 3 ended with an open question: the fingerprint block is ciphertext, the key material is inside the key block, and the key was not yet recovered. The natural question was:
Inside the browser, the key and the fingerprint block are ultimately used as plaintext. Following the chain from where the browser reads the field, can the plaintext be extracted directly? And since the client holds only public-side material, does that mean you can get to the plaintext without the server’s private key?
The answer is: yes, in principle and in practice — the full chain is recoverable offline from client-side material alone.
A note on detail. This post is deliberately written at the level of result and mechanism. It states what is recoverable and which design choices make it possible, but not the algorithm constants, parameters, or step-by-step recipe. The point is to show the shape of the analysis and the boundary of what is possible — not to hand over a working tool.
1. Result summary
| Question | Status | Conclusion |
|---|---|---|
| Does the client hold enough to recover the plaintext? | confirmed | The client uses only public-side material to unwrap the key block and decrypt the fingerprint block. Replaying the same logic offline needs no private key |
| The fingerprint block’s encryption | solved | a standard block-cipher construction with a symmetric key and IV carried in the unwrapped key message |
| The fingerprint block’s compression | solved | the plaintext is a raw compressed stream (the browser prepends a small header before inflating) |
| The fingerprint block’s serialization | solved | a compact binary object format after decompression, with a top-level map of fingerprint config plus plaintext business fields |
| Full offline recovery | whole sample set | every profile in the sample set was recovered in one pass, with the binary object fully consumed |
| Integrity tag architecture | mostly solved | several short integrity tags inside the key message, at least two of which are hashes of the ciphertext and of the decompressed body, checked before and after decryption respectively |
| Token ordering | solved | the ordering of the key-message fields is not random — it is deterministically derived from the envelope’s plaintext fingerprint fields |
| Repackaging after editing | feasible but blocked | re-encryption works, but the expected integrity tag lives inside the signed key block and cannot be re-signed without the private key -> the runtime check fails |
| Byte-for-byte reproduction | not feasible | recompression bytes differ from the original compressor; functionally equivalent repackaging is unaffected |
| Key rotation | confirmed | older demo profiles use an older public key; the current key fails to unwrap them -> a separate key per major version |
2. The recovery pipeline, at a structural level
The chain from a profile file to plaintext is:
profile file (JSON envelope)
+- outer plaintext fields (parsed in Part 3)
+- key block
| +- unwrap with the client-side public material
| +- strip the wrapping padding -> a message of several fields
| +- one field is the symmetric key
| +- one field is the IV
| +- two fields are length values
| +- several fields are integrity tags
+- fingerprint block
+- symmetric decrypt (key + IV) -> strip padding
+- decompress (raw stream) -> binary object
+- decode -> the fingerprint profile object
In one line: every step of the runtime chain “decrypt then use” depends only on client-side material, so replaying the same chain offline fully recovers the data. The client cannot do more than the client is given — and the client is given everything it needs to decrypt.
3. The derivation step that hides the embedded configuration
The first obstacle: the embedded configuration (which contains the public key) is not stored in the clear. It is decrypted by a custom key-derivation routine whose input is an embedded string.
What I can say about it at a structural level:
- It is a multi-stage routine with a fast pruning stage followed by a full transform and a checksum, and it searches over a bounded iteration space for the value that satisfies the checksum.
- The output is two parts: a configuration blob (which contains the public key) and a seed string.
- A full-binary scan shows the routine is called from thousands of apparent sites — far more than a one-off key-derivation routine would need. It is almost certainly a general-purpose string de-obfuscation function, not a bespoke key derivation. Several other long encoded strings in the binary are likely candidates in the same format.
The configuration blob, once derived, is small and readable, and it contains the public key plus a few operational flags. The specifics of the derivation (the constants, the iteration scheme, the checksum) are the part I am not publishing.
4. The embedded configuration and the public key
The derived configuration is a small JSON object with a handful of fields: a flag enabling the expiry check, a flag that hints at a “read private key” mode, a third-party API key (redacted), a seed, and — the important one — the RSA public key, as a standard PEM block.
Two things this tells us:
- The client-side expiry logic is enabled by default (consistent with Parts 2 and 4).
- The design is explicitly “client holds the public key, server holds the private key”: the server wraps the key message with the private key, and the client unwraps it with the public key. That is the whole basis of “the client can decrypt but cannot forge”.
5. Unwrapping the key block
Unwrapping is a public-key operation on the key block, followed by stripping the wrapping format. The wrapping is a signature-style envelope (not the padding scheme I first guessed — see the errata below), and it unwraps cleanly on the whole sample set.
What the unwrapped message contains, at a structural level:
- a symmetric key
- an IV
- two length values (compressed length and padded ciphertext length)
- several integrity tags
Every field is unique per profile — there is no globally shared key. The field sizes are constant across the sample, but their order varies per profile — which was the key to the last open question.
6. The field ordering is deterministic, not random
An earlier hypothesis was that the field order came from a hash-map iteration order. That was disproved. The ordering is deterministically derived from the envelope’s plaintext fingerprint fields: a fixed set of those fields, in a fixed order, fed through a derivation that produces a shuffle of the fields.
This resolves Part 3’s negative result. The symmetric key is not a direct hash of the message bytes — the fields are simply shuffled, and identifying them by size gets you the key and IV.
The design lesson: deterministically deriving the layout from public envelope data means the server and client agree on the layout without transmitting it, and it ties the layout to the metadata — change the metadata and the layout changes, which is exactly why editing a plaintext metadata field breaks decryption (see the errata below).
7. The runtime verification chain
At a structural level, the runtime verification is:
- Derive the field layout from the envelope fingerprint fields.
- Pre-decryption check: compare a ciphertext hash tag; mismatch means never decrypt.
- Decode and decrypt the fingerprint block.
- Length check.
- Post-decryption check: hash the decompressed body and compare against the expected tag.
- Mismatch -> fatal failure (an early return, not a fallback).
- A post-hoc derivation runs afterward; it does not participate in any tag comparison.
This explains the feasibility boundary: the expected tag lives inside the signed key message. A recomputed hash after editing the plaintext can never be written back there (re-signing needs the private key), so the runtime inevitably fails at the post-decryption check. Therefore:
- Read (offline decryption, verification, diagnostics): completely free.
- Write (repackaging after editing fingerprint fields): the crypto chain works, but requires a third binary patch — forcing the post-decryption comparison to pass. Along with the two guard patches from Part 4, this is the third authorization-test patch point.
- Byte-for-byte reproduction (repack without changing content): the compressor bytes are not reproducible; only functionally equivalent repackaging works.
8. What the plaintext looks like
Without publishing the schema, the recovered object is a top-level map with:
- a format version and a mapping version
- a fingerprint body: a set of sub-dictionaries covering fonts, math constants, media devices, battery stats, audio, Canvas / WebGL, codecs, speech voices, OS version, and more
- a behavior/environment dictionary of switches
- business fields: demo/trial/monthly flags, a charge date and amount, a client name, and the numeric expiry values — including the inner numeric copy of the outer expiry string
So the inner profile is a complete virtual device model: the hardware and software fingerprint the browser will impersonate, plus a thin layer of subscription bookkeeping.
9. A runtime nuance worth flagging
An important correction from the earlier analysis: expired profiles actually load and decrypt normally — the success path prints a success log. But the inner fingerprint was not injected in the runs observed (host CPU count and screen dimensions did not match the inner model). This suggests the expiry gate can short-circuit injection even when decryption succeeds — a nuance worth flagging rather than glossing over.
10. Practical boundaries
- Reuse / move an existing profile: proven feasible. Self-contained, no machine binding, copy directly.
- Extend identity count without a new profile: available now. A noise-seed feature gives the same profile a fresh, restart-stable noise identity; plus per-identity user-data directories and proxy/timezone/language. Device models x any seed x session parameters = an effectively unbounded identity space.
- Generate a brand-new profile (new device model): the offline read path is fully open (this post); the write path works but needs the third patch to bypass the tag comparison, or official authoring.
- Official route: a higher-tier authoring capability is a subscription feature; for anyone already on a higher tier, asking the vendor for authoring rights is the lowest-cost, zero-compliance-risk path.
11. Honest open questions
| # | Question | Status |
|---|---|---|
| 1 | The algorithm behind one of the integrity tags | unidentified; unique per file; not needed for reading |
| 2 | The full mapping from internal numeric keys to field names | built at runtime; not required for decryption or field-level access |
| 3 | An algorithm descriptor reference in the binary | unidentified; no effect on conclusions |
| 4 | Byte-exact recompression vs the original compressor | negative (approximately a standard zlib level, but not the same build) |
12. Corrections made along the way
Good analysis keeps its own errata visible:
- “The outer expiry is display-only” was incomplete. It is also one of the envelope fingerprint fields feeding the layout derivation. Changing it does not affect expiry judgment, but it shifts the field layout, so the pre-decryption check fails and the profile will not load.
- “The field order is a hash-map shuffle” was disproved — it is a deterministic derivation from the envelope metadata.
- “The key block uses an OAEP-style padding” was corrected to a signature-style envelope.
13. Method appendix
At the tooling level, the analysis was done with a small set of pure-standard-library scripts: one to derive the embedded configuration, one to unwrap the key block, one to decrypt-and-decompress a profile, and a set of batch verifiers that assert the round-trip over the whole sample set. Keeping it dependency-free matters: the whole chain can be re-run by anyone with the same inputs.
14. Safety boundary for this stage
- Read-only throughout: the original profile files were only ever opened read-only; all decrypted artifacts landed in a scratch area.
- The public key is public information: the material used for decryption comes from the binary’s embedded configuration. It is not a private-key leak; it is a direct consequence of the “client holds the public key” design.
- Redaction maintained: the embedded third-party API key does not appear in any artifact.
- Repackaging is for authorized testing only: modifications were validated on work copies with a round-trip assertion before any controlled test; no distributable profiles were produced.
Series navigation:
- Part 1: What an Anti-Detection Browser Actually Is
- Part 2: Static Recon Without Running It
- Part 3: The Crypto Layer and the Runtime Gate
- Part 4: Binary Patching as a Research Exercise
- Part 5: Analyzing the Encrypted Profile Offline (this post)
- Part 6: GeoIP, Timezone Mismatch, and Database Forensics