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.
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
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”:
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.
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.