diff --git a/.documentation/addon description/de/description.txt b/.documentation/addon description/de/description.txt index dda4c08..b35b41d 100644 --- a/.documentation/addon description/de/description.txt +++ b/.documentation/addon description/de/description.txt @@ -13,6 +13,7 @@ Beschützte "Fingerprinting"-APIs:
  • history
  • window (standardmäßig deaktiviert)
  • DOMRect
  • +
  • TextMetrics
  • navigator (standardmäßig deaktiviert)
  • screen
  • diff --git a/.documentation/addon description/en/description.txt b/.documentation/addon description/en/description.txt index df8dc77..908b277 100644 --- a/.documentation/addon description/en/description.txt +++ b/.documentation/addon description/en/description.txt @@ -13,6 +13,7 @@ Protected "fingerprinting" APIs:
  • history
  • window (disabled by default)
  • DOMRect
  • +
  • TextMetrics
  • navigator (disabled by default)
  • screen
  • diff --git a/.vscode/settings.json b/.vscode/settings.json index 86052b6..6155c43 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,6 +42,7 @@ "mediump", "micrococo", "monero", + "monospace", "nocanvas", "onedrive", "onloaded", diff --git a/README.md b/README.md index 8888d95..3a7e641 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Protected "fingerprinting" APIs: * history * window (disabled by default) * DOMRect + * TextMetrics * navigator (disabled by default) * screen diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 45d2be3..b12ebf2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -154,6 +154,10 @@ "message": "DOMRect API", "description": "" }, + "section_TextMetrics-api": { + "message": "TextMetrics API", + "description": "" + }, "section_Navigator-api": { "message": "Navigator API", "description": "" @@ -279,6 +283,18 @@ "message": "Do you want to allow DOMRect API readout?", "description": "" }, + "askForTextMetricsPermission": { + "message": "Do you want to allow the TextMetrics API?", + "description": "" + }, + "askForTextMetricsInputPermission": { + "message": "Do you want to allow TextMetrics API input?", + "description": "" + }, + "askForTextMetricsReadoutPermission": { + "message": "Do you want to allow TextMetrics API readout?", + "description": "" + }, "askForNavigatorPermission": { "message": "Do you want to allow the navigator API?", "description": "" @@ -705,6 +721,10 @@ "message": "Faked DOMRect readout on {url}", "description": "" }, + "fakedTextMetricsReadout": { + "message": "Faked TextMetrics readout on {url}", + "description": "" + }, "fakedNavigatorReadout": { "message": "Faked navigator readout on {url}", "description": "" @@ -1154,6 +1174,19 @@ "description": "" }, + "protectTextMetrics_title": { + "message": "Protect TextMetrics API", + "description": "" + }, + "protectTextMetrics_description": { + "message": "This protects against the \"measureText()\" fingerprinting which can be used to cross validate DOMRect values.", + "description": "" + }, + "protectTextMetrics_urlSpecific": { + "message": "To exclude specific websites from this protection, click on the black arrow to open the menu, add the domain or URL by clicking on \"+\" and remove its checkmark.", + "description": "" + }, + "protectNavigator_title": { "message": "Protect navigator API", "description": "" diff --git a/lib/modifiedAPI.js b/lib/modifiedAPI.js index aa4a126..9d38160 100644 --- a/lib/modifiedAPI.js +++ b/lib/modifiedAPI.js @@ -39,6 +39,7 @@ appendModified(require("./modifiedHistoryAPI")); appendModified(require("./modifiedWindowAPI")); appendModified(require("./modifiedDOMRectAPI")); + appendModified(require("./modifiedTextMetricsAPI")); appendModified(require("./modifiedNavigatorAPI")); appendModified(require("./modifiedScreenAPI")); }()); \ No newline at end of file diff --git a/lib/modifiedDOMRectAPI.js b/lib/modifiedDOMRectAPI.js index 44601a4..9a70378 100644 --- a/lib/modifiedDOMRectAPI.js +++ b/lib/modifiedDOMRectAPI.js @@ -50,7 +50,15 @@ } const cache = {}; - const valueCache = [{}, {}, {}, {}]; + const valueCache = [{}, {}, {}, {}, {}]; + scope.cache = { + valueCache, + X: 0, + Y: 1, + WIDTH: 2, + HEIGHT: 3, + OTHER: 4 + }; function getFakeDomRect(window, domRect, prefs, notify){ const hash = getHash(domRect); let cached = cache[hash]; diff --git a/lib/modifiedTextMetricsAPI.js b/lib/modifiedTextMetricsAPI.js new file mode 100644 index 0000000..21e9acc --- /dev/null +++ b/lib/modifiedTextMetricsAPI.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +(function(){ + "use strict"; + + let scope; + if ((typeof exports) !== "undefined"){ + scope = exports; + } + else { + scope = require.register("./modifiedTextMetricsAPI", {}); + } + + const {checkerWrapper, setProperties, getStatusByFlag} = require("./modifiedAPIFunctions"); + const {byteArrayToString: hash} = require("./hash"); + const {cache} = require("./modifiedDOMRectAPI"); + const valueCache = cache.valueCache; + + function getValueHash(value){ + return hash(new Float32Array([value])); + } + + let randomSupply = null; + scope.setRandomSupply = function(supply){ + randomSupply = supply; + }; + + function getFakeValue(window, value, i, prefs){ + const valueHash = getValueHash(value); + const cache = valueCache[i]; + let cachedValue = cache[valueHash]; + if (typeof cachedValue === "number"){ + return cachedValue; + } + if ((value * prefs("domRectIntegerFactor", window.location)) % 1 === 0){ + cache[valueHash] = value; + return value; + } + else { + const rng = randomSupply.getRng(5, window); + const fakedValue = value + 0.01 * (rng(i) / 0xffffffff - 0.5); + const fakedHash = getValueHash(fakedValue); + cache[valueHash] = fakedValue; + cache[fakedHash] = fakedValue; + return fakedValue; + } + } + + function generateChangedTextMetricsPropertyGetter(property, cacheIndex){ + const changedGetter = { + objectGetters: [ + function(window){return window.TextMetrics && window.TextMetrics.prototype;} + ], + name: property, + getterGenerator: function(checker){ + const temp = { + get [property](){ + return checkerWrapper(checker, this, arguments, function(args, check){ + const {prefs, notify, window, original} = check; + const originalValue = original.call(this, ...args); + const returnValue = getFakeValue(window, originalValue, cacheIndex, prefs); + if (originalValue !== returnValue){ + notify("fakedTextMetricsReadout"); + } + return returnValue; + }); + } + }; + return Object.getOwnPropertyDescriptor(temp, property).get; + } + }; + return changedGetter; + } + + scope.changedGetters = [ + generateChangedTextMetricsPropertyGetter("width", cache.WIDTH), + generateChangedTextMetricsPropertyGetter("actualBoundingBoxAscent", cache.OTHER), + generateChangedTextMetricsPropertyGetter("actualBoundingBoxDescent", cache.OTHER), + generateChangedTextMetricsPropertyGetter("actualBoundingBoxLeft", cache.OTHER), + generateChangedTextMetricsPropertyGetter("actualBoundingBoxRight", cache.OTHER), + generateChangedTextMetricsPropertyGetter("alphabeticBaseline", cache.OTHER), + generateChangedTextMetricsPropertyGetter("emHeightAscent", cache.OTHER), + generateChangedTextMetricsPropertyGetter("emHeightDescent", cache.OTHER), + generateChangedTextMetricsPropertyGetter("fontBoundingBoxAscent", cache.OTHER), + generateChangedTextMetricsPropertyGetter("fontBoundingBoxDescent", cache.OTHER), + generateChangedTextMetricsPropertyGetter("hangingBaseline", cache.OTHER), + generateChangedTextMetricsPropertyGetter("ideographicBaseline", cache.OTHER), + ]; + + setProperties({}, scope.changedGetters, { + type: "readout", + getStatus: getStatusByFlag("protectTextMetrics"), + api: "textMetrics" + }); +}()); \ No newline at end of file diff --git a/lib/settingDefinitions.js b/lib/settingDefinitions.js index 15a6b46..dc8e621 100644 --- a/lib/settingDefinitions.js +++ b/lib/settingDefinitions.js @@ -117,7 +117,20 @@ "getExtentOfChar @ domRect", "intersectionRect @ domRect", "boundingClientRect @ domRect", - "rootBounds", + "rootBounds @ domRect", + {name: "TextMetrics-API", level: 1}, + "width @ textMetrics", + "actualBoundingBoxAscent @ textMetrics", + "actualBoundingBoxDescent @ textMetrics", + "actualBoundingBoxLeft @ textMetrics", + "actualBoundingBoxRight @ textMetrics", + "alphabeticBaseline @ textMetrics", + "emHeightAscent @ textMetrics", + "emHeightDescent @ textMetrics", + "fontBoundingBoxAscent @ textMetrics", + "fontBoundingBoxDescent @ textMetrics", + "hangingBaseline @ textMetrics", + "ideographicBaseline @ textMetrics", {name: "Navigator-API", level: 1}, "appCodeName @ navigator", "appName @ navigator", @@ -335,6 +348,11 @@ name: "domRectIntegerFactor", defaultValue: 4 }, + { + name: "protectTextMetrics", + defaultValue: true, + urlSpecific: true + }, { name: "blockDataURLs", defaultValue: true, diff --git a/manifest.json b/manifest.json index e935a9f..b301baa 100644 --- a/manifest.json +++ b/manifest.json @@ -50,6 +50,7 @@ "lib/modifiedHistoryAPI.js", "lib/modifiedWindowAPI.js", "lib/modifiedDOMRectAPI.js", + "lib/modifiedTextMetricsAPI.js", "lib/navigator.js", "lib/modifiedNavigatorAPI.js", "lib/modifiedScreenAPI.js", diff --git a/options/sanitationRules.js b/options/sanitationRules.js index 08e839d..69eec3d 100644 --- a/options/sanitationRules.js +++ b/options/sanitationRules.js @@ -83,6 +83,7 @@ {mainFlag: "protectAudio", section: "Audio-API"}, {mainFlag: "protectWindow", section: "Window-API"}, {mainFlag: "protectDOMRect", section: "DOMRect-API"}, + {mainFlag: "protectTextMetrics", section: "TextMetrics-API"}, {mainFlag: "protectNavigator", section: "Navigator-API"}, {mainFlag: "protectScreen", section: "Screen-API"}, ].forEach(function(api){ diff --git a/options/settingsDisplay.js b/options/settingsDisplay.js index 01c0082..4ebe6f1 100644 --- a/options/settingsDisplay.js +++ b/options/settingsDisplay.js @@ -619,6 +619,25 @@ }, ] }, + { + name: "TextMetrics-API", + settings: [ + { + "name": "protectTextMetrics" + }, + { + "name": "protectedAPIFeatures", + "replaceKeyPattern": / @ .+$/, + "displayedSection": "TextMetrics-API", + "displayDependencies": [ + { + "protectTextMetrics": [true], + "displayAdvancedSettings": [true] + } + ] + }, + ] + }, { name: "Navigator-API", settings: [ diff --git a/releaseNotes.txt b/releaseNotes.txt index 9d887bd..ab06ee7 100644 --- a/releaseNotes.txt +++ b/releaseNotes.txt @@ -5,6 +5,7 @@ Version 1.2: new features: - added warning if some features of a API are disabled + - added TextMetrics protection fixes: - diff --git a/test/index.html b/test/index.html index ade9edb..e76622d 100644 --- a/test/index.html +++ b/test/index.html @@ -16,6 +16,7 @@
  • Data-URL test
  • Audio Fingerprint test
  • DOMRect Fingerprint test
  • +
  • TextMetrics test
  • Detection test
  • Performance test
  • Support for webGL
  • diff --git a/test/testAPI.js b/test/testAPI.js index e655c32..e09b036 100644 --- a/test/testAPI.js +++ b/test/testAPI.js @@ -1,6 +1,10 @@ const testAPI = function(){ "use strict"; + const digest = crypto.subtle? crypto.subtle.digest.bind(crypto.subtle, "SHA-256"): function(buffer){ + return new Uint32Array(buffer.buffer); + }; + function bufferToString(hash){ const chunks = []; (new Uint32Array(hash)).forEach(function(num){ @@ -18,7 +22,7 @@ const testAPI = function(){ const buffer = ((typeof input) === "string")? new TextEncoder("utf-8").encode(input): input; - const hash = await crypto.subtle.digest("SHA-256", buffer); + const hash = await digest(buffer); return bufferToString(hash); } }; diff --git a/test/textMetricsTest.html b/test/textMetricsTest.html new file mode 100644 index 0000000..ae7f160 --- /dev/null +++ b/test/textMetricsTest.html @@ -0,0 +1,40 @@ + + + + TextMetrics test + + + + + + +

    TextMetrics test

    +

    Expected result

    + +

    Tests

    +
    +
    +

    measureText

    + Hashes: + + + + +
    all:

    + Number of differences:
    + +
    +
    + + + + \ No newline at end of file diff --git a/test/textMetricsTest.js b/test/textMetricsTest.js new file mode 100644 index 0000000..d7fbdea --- /dev/null +++ b/test/textMetricsTest.js @@ -0,0 +1,94 @@ +/* globals testAPI */ +(function(){ + "use strict"; + + const fonts = ["none", "sans-serif", "serif", "monospace", "cursive", "fantasy"]; + const charCodePoints = [ + 0x20B9, 0x2581, 0x20BA, 0xA73D, 0xFFFD, 0x20B8, 0x05C6, + 0x1E9E, 0x097F, 0xF003, 0x1CDA, 0x17DD, 0x23AE, 0x0D02, 0x0B82, 0x115A, + 0x2425, 0x302E, 0xA830, 0x2B06, 0x21E4, 0x20BD, 0x2C7B, 0x20B0, 0xFBEE, + 0xF810, 0xFFFF, 0x007F, 0x10A0, 0x1D790, 0x0700, 0x1950, 0x3095, 0x532D, + 0x061C, 0x20E3, 0xFFF9, 0x0218, 0x058F, 0x08E4, 0x09B3, 0x1C50, 0x2619 + ]; + const textMetricsProperties = [ + "width", + "actualBoundingBoxAscent", + "actualBoundingBoxDescent", + "actualBoundingBoxLeft", + "actualBoundingBoxRight", + "alphabeticBaseline", + "emHeightAscent", + "emHeightDescent", + "fontBoundingBoxAscent", + "fontBoundingBoxDescent", + "hangingBaseline", + "ideographicBaseline", + ].filter(function(property){ + return TextMetrics.prototype.hasOwnProperty(property); + }); + + const hashTable = document.querySelector("#measureText .hashes table"); + textMetricsProperties.forEach(function(property){ + const row = document.createElement("tr"); + hashTable.appendChild(row); + + const name = document.createElement("td"); + name.textContent = property + ": "; + row.appendChild(name); + + const hash = document.createElement("td"); + hash.className = "hash " + property; + row.appendChild(hash); + }); + + async function testMeasureText(){ + const canvas = document.createElement("canvas"); + const node = document.createElement("span"); + document.body.appendChild(node); + const context = canvas.getContext("2d"); + + const data = new Float64Array(fonts.length * charCodePoints.length * textMetricsProperties.length); + let dataIndex = 0; + const propertyData = {}; + textMetricsProperties.forEach(function(property){ + propertyData[property] = new Float64Array(fonts.length * charCodePoints.length); + }); + let propertyDataIndex = 0; + + let differences = 0; + + fonts.forEach(function(font){ + context.font = node.style.font = "22000px " + font; + + charCodePoints.forEach(function(charCodePoint){ + const char = String.fromCodePoint(charCodePoint); + node.textContent = char; + + const textMetric = context.measureText(char); + const domRect = node.getBoundingClientRect(); + textMetricsProperties.forEach(function(property){ + data[dataIndex] = textMetric[property]; + propertyData[property][propertyDataIndex] = textMetric[property]; + dataIndex += 1; + }); + propertyDataIndex += 1; + if (textMetric.width !== domRect.width){ + differences += 1; + } + }); + }); + document.body.removeChild(node); + + document.querySelector("#measureText .differences").textContent = + differences + " of " + fonts.length * charCodePoints.length; + textMetricsProperties.forEach(async function(property){ + document.querySelector("#measureText .hash." + property).textContent = + await testAPI.hash(propertyData[property]); + }); + document.querySelector("#measureText .hash.all").textContent = await testAPI.hash(data); + + } + + testMeasureText(); + document.querySelector("#measureText .refresh").addEventListener("click", testMeasureText); +}()); \ No newline at end of file diff --git a/versions/updates.json b/versions/updates.json index 679d35e..002e984 100644 --- a/versions/updates.json +++ b/versions/updates.json @@ -129,6 +129,10 @@ { "version": "1.2Alpha20200224", "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-1.2Alpha20200224-an+fx.xpi" + }, + { + "version": "1.2Alpha20200314", + "update_link": "https://canvasblocker.kkapsner.de/versions/canvasblocker_beta-1.2Alpha20200314-an+fx.xpi" } ] }