Technical Analysis of DuckDuckGo Privacy Essentials (part 1)
A technical audit of the DuckDuckGo Privacy Essentials addon.
The DuckDuckGo Privacy Essentials addon is a browser addon available for the Firefox and Chrome browsers. The addon lists several features including blocking trackers, private search, encryption, privacy grades, and support for Global Privacy Control. It is the 7th most popular addon for Firefox1 with approximately 1.5 million users.
While the addon’s name clearly implies privacy-related functionality, its technical measures remain undocumented. The objective of this post is to analyze the anti-fingerprinting functionality of the addon for ease of comparison against alternative privacy solutions.
Scope
At the time of writing, DuckDuckGo Privacy Essentials (DPE) has 25 released versions on addons.mozilla.org, with the first version released August 21, 2019.
This post will only discuss version 2021.6.2
of the addon. The addon’s XPI file was downloaded from the Mozilla addons site, and was found to have a SHA256 hash of 510c4e7b7e720ccb5ee0eec78c498dee5c012a5939b8b0706b7a195377876fa2
. Based on the tags from the addon’s Git repository, the commit hash corresponding to this version is bcef1583085ecbfdb5a3a52c12538ea949e8a65e
.
All analysis and testing in this report is focused on Firefox for Desktop. Analysis of other browsers - including the Firefox mobile browsers - is out of scope for this post. For “part one” of this series, we limit our analysis to the anti-fingerprinting functionality found in ./shared/js/content-scope/.
Anti-fingerprinting
Audio API
DPE intercepts calls to several methods in the audio API. Specifically:
AudioBuffer.getChannelData
AnalyserNode.getByteTimeDomainData
AnalyserNode.getByteTimeDomainData
AnalyserNode.getFloatTimeDomainData
AnalyserNode.getByteFrequencyData
AnalyserNode.getFloatFrequencyData
The addon modifies the return values of these methods by adding deterministic noise to the channelData
buffer based on the value of audioKey
:
iterateDataKey(audioKey, (item, byte) => {
const itemAudioIndex = item % channelData.length
let factor = byte * 0.0000001
if (byte ^ 0x1) {
factor = 0 - factor
}
channelData[itemAudioIndex] = channelData[itemAudioIndex] + factor
})
In an attempt to limit tracking across sites, audioKey
is a hash including the domain of the site. This should imply that a user’s fingerprint will not be shared across domains and therefore cannot be used for cross-site tracking. Further analysis is required to determine if this noise is sufficient to mask the underlying fingerprint. The key is generated as follows:
export function getDataKeySync (sessionKey, domainKey, inputData) {
const hmac = new sjcl.misc.hmac(sjcl.codec.utf8String.toBits(sessionKey + domainKey), sjcl.hash.sha256)
return sjcl.codec.hex.fromBits(hmac.encrypt(inputData))
}
Canvas API
DPE intercepts calls to several methods in the canvas API:
CanvasRenderingContext2D.getImageData
HTMLCanvasElement.toDataURL
HTMLCanvasElement.toBlob
Similar to the audio API protection, canvas data is also modified based on a domain-specific key to limit the ability to track users across sites:
iterateDataKey(canvasKey, (item, byte) => {
const channel = byte % 3
const lookupId = item % length
const pixelCanvasIndex = arr[lookupId] + channel
imageData.data[pixelCanvasIndex] = imageData.data[pixelCanvasIndex] ^ (byte & 0x1)
})
Do Not Track API
JS Variable | Default Value | Addon Value |
---|---|---|
navigator.doNotTrack 2 |
"1" if DNT enabled, "unspecified" otherwise. |
"unspecified" |
DPE unsets navigator.doNotTrack
, but does not also unset the DNT
header. If a fingerprint includes both the header and DOM values, this behavior may make a user of the addon more unique compared to the global population.
NOTE: In response to this post, a future release is expected to stop removing navigator.doNotTrack
.3
Global Privacy Control
The DuckDuckGo Privacy Essentials addon has a feature titled Global Privacy Control (GPC). On a technical level, GPC is a draft specification for communicating privacy preferences via HTTP headers and JavaScript properties.
Header Name | Default Value | Addon Value |
---|---|---|
Sec-GPC 4 |
not present | 1 when GPC is enabled, not present otherwise |
JS Variable | Default Value | Addon Value |
---|---|---|
navigator.globalPrivacyControl 5 |
undefined |
true when GPC is enabled, false otherwise |
It is interesting that DPE disables navigator.doNotTrack
but specifically adds the navigator.globalPrivacyControl
field and Sec-GPC
header. Do Not Track and Global Privacy Control are competing specifications for communicating tracking preferences; DuckDuckGo was involved in creating the draft specification for GPC, which may indicate the reason for this preference. Still, it seems like this behavior should be documented as it may interfere with the user’s expectations when enabling Do Not Track.
DPE allows the presence of the GPC header to be toggled by the user via the addon’s settings. However, navigator.globalPrivacyControl
is always present and indicates the user’s configuration of the addon. Beyond allowing fingerprinting of the addon’s presence, this allows fingerprinting based on the addon’s configuration.
The value of navigator.globalPrivacyControl
is computed as follows:
// If GPC on, set DOM property to true if not already true
if (args.globalPrivacyControlValue) {
if (navigator.globalPrivacyControl) return
defineProperty(navigator, 'globalPrivacyControl', {
value: true,
enumerable: true
})
} else {
// If GPC off, set DOM property prototype to false so it may be overwritten
// with a true value by user agent or other extensions
if (typeof navigator.globalPrivacyControl !== 'undefined') return
defineProperty(Object.getPrototypeOf(navigator), 'globalPrivacyControl', {
value: false,
enumerable: true
})
}
Referer
JS Variable | Default Value | Addon Value |
---|---|---|
document.referrer (external link) |
https://example.com/index.html depending on the Referrer-Policy |
https://example.com/ |
document.referrer (internal link) |
https://example.com/index.html depending on the Referrer-Policy |
https://example.com/index.html depending on the Referrer-Policy |
In the absence of a Referrer-Policy
header, Firefox defaults to stripping path information from the Referer
header for external links. However, certain Referrer-Policy
configurations, such as unsafe-url
, will cause the full path to be sent.
DPE specifically avoids modification of document.referrer
on internal links (i.e., links between pages on the same host) using the following condition:
new URL(document.URL).hostname !== new URL(document.referrer).hostname
Hardware APIs
JS Variable | Default Value | Addon Value |
---|---|---|
navigator.keyboard |
undefined |
undefined |
navigator.hardwareConcurrency |
2 if privacy.resistFingerprinting is enabled, or the accurate number of cores up to dom.maxHardwareConcurrency otherwise.6 |
8 |
navigator.deviceMemory |
undefined |
undefined |
The choice of 8
for navigator.hardwareConcurrency
may make a user more unique. According to the Firefox Public Data Report for June 14th, 2021, 52% of Firefox users have two physical cores, followed by 34% with four cores. Only 3% reported having eight physical cores. Firefox’s own “resist fingerprinting” functionality spoofs navigator.hardwareConcurrency
to 2
to make users appear less unique compared to the global population.
It is possible that the addon creators are attempting to mitigate potential performance impacts. If a webpage uses workers for compute-intensive tasks, the webpage may throttle performance by letting CPU capacity go unused. However, spoofing additional cores may also cause a webpage to spawn more than there are available cores, which may also have deleterious performance impact.
NOTE: In response to this post, a future release of DPE is expected to set navigator.hardwareConcurrency
to 2
.7
Screen Size
JS Variable | Default Behavior | Addon Behavior |
---|---|---|
screen.availWidth , screen.availHeight |
Displays the amount of available space on the screen, minus any OS UI elements. | Displays the total height and width of the screen. |
screen.availTop , screen.availLeft |
Displays the amount of space dedicated to OS UI elements in pixels. | Always 0 . |
screen.colorDepth , screen.pixelDepth |
Displays the actual color/pixel depth. | Always 24 . |
window.screenX , window.screenLeft , window.screenY , window.screenTop |
Displays the position of the window across all monitors. Values update when the window is moved. | Maps the window position onto a single monitor to prevent calculation of monitor count and positioning. The values are updated when the window is resized, but not when the window is moved. |
Overall, DPE prevents observation of window movements and total monitor count. The position of the window on the screen remains static whereas the default behavior is to update those parameters in realtime. Additionally, since certain OS-level UI elements are only present on some monitors, movement of the window across monitors cannot be detected in this way. DPE does not hide monitor resolution, which may be somewhat unique, especially on mobile browsers and monitors with special form factors (ultra-wide, HDPI, etc.). The Firefox Public Data Report indicates that the two most popular display resolutions are 1920x1080
(38%) and 1366x768
(26%).
Non-Firefox APIs
Browsers often offer experimental or non-standard APIs. Additionally, not all browsers conform with all released standards. The addon has anti-fingerprinting protection for the following APIs which are not supported in Firefox and are therefore not analyzed here:
- Floc API (
Document.prototype.interestCohort
) - Battery API (
navigator.getBattery()
) - Temporary Storage API (
navigator.webkitTemporaryStorage
)
Future Work
- Evaluate data shared with DuckDuckGo servers during/after the installation of the addon.
- Evaluate the efficacy of the above anti-fingerprinting techniques. For example, whether it is possible to amplify the signal of the audio API fingerprinting techniques to overcome the injected noise.
- Analyze how the DPE Privacy Grade is computed and whether external servers are accessed in computing the grade.
- Assess the reproducibility of the published XPI file from the addon’s source code.
- Analyze the tracker blocking functionality, including cookie handling for known tracker domains.
Updates
- 2020-07-07:
- Added notes indicating that a future release of DPE will stop removing
navigator.doNotTrack
and will setnavigator.hardwareConcurrency
to2
. Thanks to the DuckDuckGo team for their communication and quick response! - Corrected a spelling error.
- Added notes indicating that a future release of DPE will stop removing
Footnotes
-
https://github.com/duckduckgo/duckduckgo-privacy-extension/pull/720 ↩
-
Sec-GPC
references: header specification, source ↩ -
RuntimeService::ClampedHardwareConcurrency
,gMaxHardwareConcurrency
, Bugzilla: spoof navigator.hardwareConcurrency ↩ -
https://github.com/duckduckgo/duckduckgo-privacy-extension/pull/722 ↩