diff --git a/lib/modifiedSVGAPI.js b/lib/modifiedSVGAPI.js new file mode 100644 index 0000000..644b139 --- /dev/null +++ b/lib/modifiedSVGAPI.js @@ -0,0 +1,88 @@ +/* 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("./modifiedSVGAPI", {}); + } + + const {checkerWrapper, setProperties, getStatusByFlag} = require("./modifiedAPIFunctions"); + const {byteArrayToString: hash} = require("./hash"); + + + let randomSupply = null; + scope.setRandomSupply = function(supply){ + randomSupply = supply; + }; + + function getValueHash(value){ + return hash(new Float32Array([value])); + } + + const cache = {}; + function getFakeValue(value, window){ + const valueHash = getValueHash(value); + let cachedValue = cache[valueHash]; + if (typeof cachedValue === "number"){ + return cachedValue; + } + else { + const rng = randomSupply.getRng(1, window); + const fakedValue = value + 0.01 * (rng(0) / 0xffffffff - 0.5); + const fakedHash = getValueHash(fakedValue); + cache[valueHash] = fakedValue; + cache[fakedHash] = fakedValue; + return fakedValue; + } + } + scope.getFakeValue = getFakeValue; + + function getFakeValueCallback(args, check){ + const {notify, window, original} = check; + const ret = args.length? original.call(this, ...args): original.call(this); + notify("fakedSVGReadout"); + return getFakeValue(ret, window); + } + + scope.changedFunctions = { + getTotalLength: { + object: ["SVGGeometryElement"], + fakeGenerator: function(checker){ + return function getTotalLength(){ + return checkerWrapper(checker, this, arguments, getFakeValueCallback); + }; + } + }, + getComputedTextLength: { + object: ["SVGTextContentElement"], + fakeGenerator: function(checker){ + return function getComputedTextLength(){ + return checkerWrapper(checker, this, arguments, getFakeValueCallback); + }; + } + }, + getSubStringLength: { + object: ["SVGTextContentElement"], + fakeGenerator: function(checker){ + return function getSubStringLength(charnum, nchars){ + return checkerWrapper(checker, this, arguments, getFakeValueCallback); + }; + } + }, + }; + + + scope.changedGetters = []; + + setProperties(scope.changedFunctions, scope.changedGetters, { + type: "readout", + getStatus: getStatusByFlag("protectSVG"), + api: "svg" + }); +}()); \ No newline at end of file diff --git a/test/domRectSVG.svg b/test/domRectSVG.svg index a3db696..06c60ff 100644 --- a/test/domRectSVG.svg +++ b/test/domRectSVG.svg @@ -16,4 +16,5 @@ Q 90,60 50,90 Q 10,60 10,30 z"/> Text with Unicode 𝞐 + 񳺚顃򳴡ģԹ̔򫳞񊄐񿔺ࠕ \ No newline at end of file diff --git a/test/index.html b/test/index.html index b6cdf24..4183c9c 100644 --- a/test/index.html +++ b/test/index.html @@ -18,6 +18,7 @@
  • Audio Fingerprint test
  • DOMRect Fingerprint test
  • TextMetrics test
  • +
  • SVG test
  • Detection test
  • Performance test
  • Support for webGL
  • diff --git a/test/svgTest.css b/test/svgTest.css new file mode 100644 index 0000000..4d9c621 --- /dev/null +++ b/test/svgTest.css @@ -0,0 +1,43 @@ + +#svg { + position: fixed; + top: -2000%; +} + +#test { + display: inline-block; + margin: 1em; +} +#test .data table { + border-collapse: collapse; +} +#test .data th { + padding: 0.4em; +} +#test .data td{ + border: 1px solid #c7c7c7; + padding: 0.4em; +} +#test .data td.value { + text-align: right; +} +.small { + font-size: 0.8em; + color: gray; +} +.rectHash { + font-size: 4px; +} +.rectHash:hover { + font-size: 100%; +} + +.content-hidable.content-hidden .content, .content-hidable .anti-content { + display: none; +} +.content-hidable .content, .content-hidable.content-hidden .anti-content { + display: initial; +} +.content-hidable .toggle { + cursor: pointer; +} \ No newline at end of file diff --git a/test/svgTest.html b/test/svgTest.html new file mode 100644 index 0000000..3c80383 --- /dev/null +++ b/test/svgTest.html @@ -0,0 +1,36 @@ + + + + TextMetrics test + + + + + + + + +

    SVG test

    +

    Expected result

    +
      +
    1. the hashes are different to the hashes when CanvasBlocker is disabled
    2. +
    3. if "refresh" is clicked nothing must change
    4. +
    5. upon page reload the hash changes (depending on CanvasBlocker settings - e.g. not in the stealth preset)
    6. +
    +

    Tests

    + +
    +

    SVG

    + Hash:
    + Data: +
    + +
    + + + + \ No newline at end of file diff --git a/test/svgTest.js b/test/svgTest.js new file mode 100644 index 0000000..2f238a6 --- /dev/null +++ b/test/svgTest.js @@ -0,0 +1,114 @@ +/* globals testAPI */ +(function(){ + "use strict"; + + function byteArrayToHex(arrayBuffer){ + const chunks = []; + (new Uint32Array(arrayBuffer)).forEach(function(num){ + chunks.push(num.toString(16)); + }); + return chunks.map(function(chunk){ + return "0".repeat(8 - chunk.length) + chunk; + }).join(""); + } + + function formatNumber(number){ + const str = number.toString(); + return "" + str.substring(0, str.length - 2) + "" + + str.substring(str.length - 2); + } + + const svg = document.getElementById("svg"); + const output = document.getElementById("test"); + + function getElements(){ + const doc = svg.contentDocument; + + return Array.from(doc.querySelectorAll(".testRect")); + } + + const tests = []; + function addTest(title, callback){ + tests.push({title, callback}); + } + + async function performTests(){ + const elements = getElements(); + const results = await Promise.all(tests.map(async function(test){ + return { + name: test.title, + data: await Promise.all(elements.map(async function(svgElement){ + return await test.callback(svgElement); + })) + }; + })); + const data = new Float64Array(elements.length * tests.length); + results.forEach(function(svgData, i){ + svgData.data.forEach(function(testData, j){ + if ((typeof testData) === "number"){ + data[i * elements.length + j] = testData; + } + }); + }); + + const hash = await crypto.subtle.digest("SHA-256", data); + output.querySelector(".hash").textContent = byteArrayToHex(hash); + + const dataNode = output.querySelector(".data"); + dataNode.innerHTML = "" + + elements.map(function(svgElement){ + return ""; + }).join("") + + results.map(function(result){ + return "" + result.data.map(function(value){ + if ((typeof value) === "number"){ + return ""; + } + else { + return ""; + } + }).join("") + ""; + }).join("") + + "
    " + svgElement.dataset.name + "
    " + result.name + "" + + formatNumber(value) + + "--
    "; + } + + svg.addEventListener("load", function(){ + addTest("getTotalLength", function(element){ + if (!element.getTotalLength){ + return null; + } + return element.getTotalLength(); + }); + addTest("getComputedTextLength", function(element){ + if (!element.getComputedTextLength){ + return null; + } + return element.getComputedTextLength(); + }); + [{start: 3, end: 7}, {start: 7, end: 11}, {start: 3, end: 11}].forEach(function(substringDefinition){ + addTest( + `getSubStringLength(${substringDefinition.start}, ${substringDefinition.end})`, + function(element){ + if (!element.getSubStringLength){ + return null; + } + return element.getSubStringLength(substringDefinition.start, substringDefinition.end); + } + ); + }); + + test.querySelector(".refresh").addEventListener("click", function(){ + performTests(); + }); + performTests(); + + + document.querySelectorAll(".content-hidable").forEach(function(parentNode){ + parentNode.querySelector(".toggle").addEventListener("click", function(){ + parentNode.classList.toggle("content-hidden"); + }); + }); + }); +}()); \ No newline at end of file