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
+
+ - the hashes are different to the hashes when CanvasBlocker is disabled
+ - the number of differences stays the same when CanvasBlocker is disabled
+ - if "refresh" is clicked nothing must change
+ - upon page reload the hashes change (depending on CanvasBlocker settings - e.g. not in the stealth preset)
+
+Tests
+
+
+
measureText
+ Hashes:
+ 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"
}
]
}