Home
index

Firefox Sync

Recent controversy and a history eroded trust on Mozilla's privacy claims for some, with commenters worrying about Firefox Sync, which can synchronize your browser history etc between devices, relying on Mozilla servers for it. A reasonable concern at first glance. But what data actually gets sent to Mozilla, if any? Let's verify.

Firefox Sync is advertised as private by default. The most important statement is that Mozilla can't read your data:

Cool. Let's inspect what actually goes over the wire. When signing in to our Mozilla account, using the browser's developer tools we see a POST request using HTTP/3 to a “SignIn” GraphQL mutation, with authPW and email as the input. A quick search leads to the protocol documentation. authPW is a PBKDF2 derivation of your password, which is not sent to the server, as promised.

The aforelinked article mentions 1000 rounds of the PBKDF2 key derivation function, but the code suggests 650,000, which is in line with OWASP's recommendation.

Developer tools showing relevant requests

We can use an external tool such as mitmproxy to sniff our HTTPS traffic and verify the information presented by the browser-controlled developer tools. We shall use it henceforth to verify that the actual data being sent to the server (browser history etc) is really encrypted.

For that we run mitmproxy in transparent mode, import its CA into Firefox, and use iptables to redirect the browser's HTTPS traffic to it:

iptables -t nat -A OUTPUT -p tcp --dport 443 -m owner --uid-owner 1001 -j REDIRECT --to-port 8080
mitmproxy confirming the network traffic

To verify that the browser history is encrypted we'll decrypt an in-flight request sending it to Mozilla, using the aforementioned key. First, let's capture said traffic by clicking “Sync now”:

mitmproxy sniffing history sync

The actual data that goes over the wire when you sync a new history item is a BSO with the following structure of interest (docs):

{
  "id" string,
  ...
  "payload": {
    "ciphertext": string,
    "IV": string,
    "hmac": string
  }
}

ciphertext is the actual history item, identified by id. Decrypting it using our key isn't as straightforward as it may sound, and luckily someone already did it in the past (thanks, Mike).

We use Mike's tool to GET and decrypt the history item kKwfjvw3KVN1 sent to Mozilla in the POST above:

sandbox@localhost ~/firefox-sync-client $ ./_out/ffsclient get history 'kKwfjvw3KVN1' --decoded --verbose
[Get Record]

Collection                   := history
RecordID                     := kKwfjvw3KVN1
RawData                      := false
DecodedData                  := true
Sessionfile location is '~/.config/firefox-sync-client.secret'
Load session from '/home/sandbox/.config/firefox-sync-client.secret'
Load existing session from /home/sandbox/.config/firefox-sync-client.secret
SessionToken                 := REDACTED
KeyA                         := REDACTED
KeyB                         := REDACTED
AccessToken                  := REDACTED
RefreshToken                 := REDACTED
UserId                       := REDACTED
HawkAPIEndpoint              := https://sync-1-us-west1-g.sync.services.mozilla.com/1.5/267606241
HawkID :=
ey<REDACTED>=
HawkKey                      := REDACTED
HawkHashAlgorithm            := sha256
Timeout                      := 2025-03-08T10:01:19.067176-03:00
BulkKeys[''].HMACKey         := REDACTED
BulkKeys[''].EncryptionKey   := REDACTED
Saved session is valid (valid until 2025-03-08T10:01:19-03:00)
Calculate HAWK-Auth-Token (normal request)
Authorization :=
Hawk id="ey<REDACTED>=", mac="nkl7NOFZm2xtp2KC6lQu9H07fTAkB9dN4OamGyy9AEg=", ts="1741435802", nonce="Nv4fQjQ="
Do HTTP Request [GET]::https://sync-1-us-west1-g.sync.services.mozilla.com/1.5/267606241/storage/history/kKwfjvw3KVN1
Start HTTP call to https://sync-1-us-west1-g.sync.services.mozilla.com/1.5/267606241/storage/history/kKwfjvw3KVN1 [[ try 1 ]]
HTTP call returned Statuscode 200
Request returned statuscode 200
Request returned Header [X-Weave-Timestamp] := '1741435798.78'
Request returned Header [Access-Control-Allow-Credentials] := 'true'
Request returned Header [Access-Control-Allow-Methods] := 'DELETE, GET, POST, PUT, OPTIONS'
Request returned Header [Server] := 'openresty/1.15.8.2'
Request returned Header [Date] := 'Sat, 08 Mar 2025 12:09:58 GMT'
Request returned Header [Content-Type] := 'application/json'
Request returned Header [Content-Length] := '381'
Request returned Header [Vary] := 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers'
Request returned Header [Access-Control-Max-Age] := '1728000'
Request returned Header [Access-Control-Allow-Headers] := 'DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,X-Conditions-Accepted'
Request returned Header [Via] := '1.1 google'
Request returned Header [X-Last-Modified] := '1741435615.03'
Request returned Header [Alt-Svc] := 'clear'
Request returned body:
{"id":"kKwfjvw3KVN1","modified":1741435615.03,"payload":"{\"ciphertext\":\"ShfvuV5irdReMQSQxeCrem3qhBubo50PWCTo2DimP+Cys+U9zX5jyy5x31nFuPlJ4jjpRFlro0vhq/AxqGi7iD8GXAumQBG3UOiksihh4J/C8AHGW0xA9RXohDPoP9bHGCXS4h3XCxyxL01bwNlZ1tdd/TaTeMtNnKQM0j1eRmo=\",\"IV\":\"NttKJGuRu3mcOkvlZzHUJw==\",\"hmac\":\"5747c528cdcdebd31efd8359ca140e366c8fc8a908f003217ac851810a7a6849\"}","sortindex":50}
Use global bulk-keys
EncryptionKey                := REDACTED
HMACKey                      := REDACTED
Decrypting payload
kKwfjvw3KVN1
2025-03-08T09:06:55.029999971-03:00
{"id":"kKwfjvw3KVN1","histUri":"https://afarah.info/public/","title":"","visits":[{"date":1741435611439952,"type":5}]}

From that output we can verify the following:

Since neither our keys nor password are sent to the server, as verified with mitmproxy, we thus verify that the history items are end-to-end encrypted.

For more details I suggest this post by Val, who wrote a similar CLI tool, or following through the printed statements above in the aforelinked source code.

Thoughts

This exercise made me appreciate Mozilla's Firefox Sync for its privacy considerations, while also giving me hands on experience with mitmproxy and Mozilla's protocols.

A history of poor communication and questionable decisions antagonized Mozilla's decreasing user base. Although I share in some of the criticism, I think us users should also appreciate the great services that are offered to us free of charge and free of espionage, by default and with cryptographic guarantees.

Keen eyed readers may have noted HTTP/2 on the requests when I mentioned HTTP/3. mitmproxy 11 added support for HTTP/3, which should “just work” for transparent proxy, but doesn't for me (v11.0.2). I haven't dug too deep into this and just disabled HTTP/3 in Firefox through network.http.http3.enable in about:config. Stretching this tangent, ChatGPT suggests network.http.http3.enabled (typo on the last character), probably due to the error being present all over the web. It also thinks mitmproxy doesn't support HTTP/3, which was true until Oct 2024, and seems true for my version also. This little rabbit hole stole me some time and I thought it noteworthy. On another tangent, Val's hometown, at the other side of the world for me, is coincidently the only place in Europe I've ever stayed at.

Copyleft. Originally published on 08 March 2025.

Home