Technical Analysis of DuckDuckGo Privacy Essentials (part 1)

7 minute read

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.doNotTrack2 "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 set navigator.hardwareConcurrency to 2. Thanks to the DuckDuckGo team for their communication and quick response!
    • Corrected a spelling error.

Footnotes