From f09f85de2782d0256252f58ba11420995a33910b Mon Sep 17 00:00:00 2001 From: Ricardo Lafuente Date: Oct 01 2021 10:49:34 +0000 Subject: Merge branch 'master' into website --- diff --git a/.gitignore b/.gitignore index b5c3f96..e4bc0d6 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,6 @@ tests/system_tests/data/ tests/system_tests/get_data/top_sites/*.csv tests/system_tests/get_data/selenium/*.jar tests/unit_tests/node_modules/* +tests/unit_tests/tmp/* website/output diff --git a/Makefile b/Makefile index 582cb7f..e973859 100644 --- a/Makefile +++ b/Makefile @@ -25,8 +25,11 @@ get_csv: wget -q -N https://www.iana.org/assignments/locally-served-dns-zones/ipv6.csv cp ipv6.csv common/ipv6.dat +submodules: + git submodule init + git submodule update -%_JSR.zip: $(COMMON_FILES) get_csv +%_JSR.zip: $(COMMON_FILES) get_csv submodules @rm -rf $*_JSR/ $@ @cp -r common/ $*_JSR/ @cp -r $*/* $*_JSR/ @@ -34,6 +37,7 @@ get_csv: @./fix_manifest.sh $*_JSR/manifest.json @nscl/include.sh $*_JSR @rm -f $*_JSR/.*.sw[pno] + @find $*_JSR/ -name '*.license' -delete @cd $*_JSR/ && zip -q -r ../$@ ./* --exclude \*.sw[pno] clean: diff --git a/chrome/manifest.json b/chrome/manifest.json index c605863..77a9d79 100644 --- a/chrome/manifest.json +++ b/chrome/manifest.json @@ -43,10 +43,11 @@ "match_origin_as_fallback": true, "js": [ "nscl/lib/browser-polyfill.js", + "nscl/common/uuid.js", "nscl/content/patchWindow.js", + "nscl/lib/sha256.js", "alea.js", "helpers.js", - "inject.js", "document_start.js" ], "run_at": "document_start" @@ -77,5 +78,5 @@ "notifications" ], "short_name": "JSR", - "version": "0.4.6" + "version": "0.5" } diff --git a/common/code_builders.js b/common/code_builders.js index a181b2f..43e3ab1 100644 --- a/common/code_builders.js +++ b/common/code_builders.js @@ -2,6 +2,7 @@ * \brief Functions that build code that modifies JS evironment provided to page scripts * * \author Copyright (C) 2019 Libor Polcak + * \author Copyright (C) 2021 Giorgio Maone * * \license SPDX-License-Identifier: GPL-3.0-or-later */ @@ -23,7 +24,7 @@ * Create IIFE to wrap the code in closure */ function enclose_wrapping(code, ...args) { - return `try{(function(...args) {${code}})(${args});} catch {}`; + return `try{(function(...args) {${code}})(${args});} catch (e) {console.error(e)}`; } /** @@ -43,18 +44,37 @@ function enclose_wrapping2(code, name, params, call_with_window) { * a function in the page context. */ function define_page_context_function(wrapper) { - var originalF = wrapper["original_function"] || `${wrapper.parent_object}.${wrapper.parent_object_property}`; - return enclose_wrapping2(`var originalF = ${originalF}; - var replacementF = function(${wrapper.wrapping_function_args}) { - // This comment is needed to correctly differentiate wrappers with the same body - // by the toString() wrapper - // ${wrapper.parent_object}.${wrapper.parent_object_property} - ${wrapper.original_function} - // Prevent fingerprintability of the extension by toString behaviour - // ${gen_random32()} + let {parent_object, parent_object_property, original_function, replace_original_function} = wrapper; + if (replace_original_function) { + let lastDot = original_function.lastIndexOf("."); + parent_object = original_function.substring(0, lastDot); + parent_object_property = original_function.substring(lastDot + 1); + } + let originalF = original_function || `${parent_object}.${parent_object_property}`; + return enclose_wrapping2(`let originalF = ${originalF}; + let replacementF = function(${wrapper.wrapping_function_args}) { ${wrapper.wrapping_function_body} }; - ${wrapper.replace_original_function ? wrapper.original_function : `${wrapper.parent_object}.${wrapper.parent_object_property}`} = replacementF; - original_functions[replacementF.toString()] = originalF.toString(); + if (WrapHelper.XRAY) { + let innerF = replacementF; + replacementF = function(...args) { + + // prepare callbacks + args = args.map(a => typeof a === "function" ? WrapHelper.pageAPI(a) : a); + + let ret = WrapHelper.forPage(innerF.call(this, ...args)); + if (ret) { + if (ret instanceof xrayWindow.Promise || ret instanceof WrapHelper.unX(xrayWindow).Promise) { + ret = Promise.resolve(ret); + } + try { + ret = WrapHelper.unX(ret); + } catch (e) {} + } + return ret; + } + } + exportFunction(replacementF, ${parent_object}, {defineAs: '${parent_object_property}'}); ${wrapper.post_replacement_code || ''} `, wrapper.wrapping_code_function_name, wrapper.wrapping_code_function_params, wrapper.wrapping_code_function_call_window); } @@ -63,12 +83,14 @@ function define_page_context_function(wrapper) { * This function creates code that assigns an already defined function to given property. */ function generate_assign_function_code(code_spec_obj) { - return `${code_spec_obj.parent_object}.${code_spec_obj.parent_object_property} = ${code_spec_obj.export_function_name}; - ` + return `exportFunction(${code_spec_obj.export_function_name}, + ${code_spec_obj.parent_object}, + {defineAs: '${code_spec_obj.parent_object_property}'}); + `; } /** - * This function wraps object properties using Object.defineProperties. + * This function wraps object properties using WrapHelper.defineProperties(). */ function generate_object_properties(code_spec_obj) { var code = ` @@ -78,33 +100,39 @@ function generate_object_properties(code_spec_obj) { return; } `; - for (assign of code_spec_obj.wrapped_objects) { - code += `var ${assign.wrapped_name} = ${assign.original_name};`; + for (let assign of code_spec_obj.wrapped_objects || []) { + code += `var ${assign.wrapped_name} = window.${assign.original_name};`; } - code += `descriptor = Object.getOwnPropertyDescriptor( - ${code_spec_obj.parent_object}, "${code_spec_obj.parent_object_property}"); - if (descriptor === undefined) { - descriptor = { // Originally not a descriptor - get: ${code_spec_obj.parent_object}.${code_spec_obj.parent_object_property}, - set: undefined, - configurable: false, + code += ` + { + let obj = ${code_spec_obj.parent_object}; + let prop = "${code_spec_obj.parent_object_property}"; + let descriptor = Object.getOwnPropertyDescriptor(obj, prop); + if (!descriptor) { + // let's traverse the prototype chain in search of this property + for (let proto = Object.getPrototypeOf(obj); proto; proto = Object.getPrototypeOf(obj)) { + if (descriptor = Object.getOwnPropertyDescriptor(proto, prop)) { + obj = WrapHelper.unX(obj); + break; + } + } + if (!descriptor) descriptor = { + // Originally not a descriptor, fallback enumerable: true, + configurable: true, }; } ` - for (wrap_spec of code_spec_obj.wrapped_properties) { + for (let wrap_spec of code_spec_obj.wrapped_properties) { code += ` originalPDF = descriptor["${wrap_spec.property_name}"]; replacementPD = ${wrap_spec.property_value}; descriptor["${wrap_spec.property_name}"] = replacementPD; - if (replacementPD instanceof Function) { - original_functions[replacementPD.toString()] = originalPDF.toString(); - } `; } - code += `Object.defineProperty(${code_spec_obj.parent_object}, + code += `WrapHelper.defineProperty(${code_spec_obj.parent_object}, "${code_spec_obj.parent_object_property}", descriptor); - `; + }`; return code; } @@ -119,7 +147,7 @@ function generate_delete_properties(code_spec_obj) { if ("${prop}" in ${code_spec_obj.parent_object}) { // Delete only properties that are available. // The if should be safe to be deleted but it can possibly reduce fingerprintability - Object.defineProperty( + WrapHelper.defineProperty( ${code_spec_obj.parent_object}, "${prop}", {get: undefined, set: undefined, configurable: false, enumerable: false} ); @@ -140,25 +168,49 @@ function generate_assignement(code_spec_obj) { * This function builds the wrapping code. */ var build_code = function(wrapper, ...args) { - var post_wrapping_functions = { + let post_wrapping_functions = { function_define: define_page_context_function, function_export: generate_assign_function_code, object_properties: generate_object_properties, delete_properties: generate_delete_properties, assign: generate_assignement, }; - var code = `try {if (${wrapper.parent_object} === undefined) {return;}} catch (e) {return; /* It seems that the parent object does not exist */ }`; - for (wrapped of wrapper.wrapped_objects) { - code += ` - var ${wrapped.wrapped_name} = ${wrapped.original_name}; - if (${wrapped.wrapped_name} === undefined) { - // Do not wrap an object that is not defined, e.g. because it is experimental feature. - // This should reduce fingerprintability. - return; - } + + let target = `${wrapper.parent_object}.${wrapper.parent_object_property}`; + let code = ""; + if (wrapper.apply_if !== undefined) { + code += `if (!(${wrapper.apply_if})) {return}` + } + { + // Do not wrap an object that is not defined, e.g. because it is experimental feature. + // This should reduce fingerprintability. + let objPath = [], undefChecks = []; + for (leaf of target.split('.')) { + undefChecks.push( + objPath.length ? `!("${leaf}" in ${objPath.join('.')})` // avoids e.g. Event.prototype.timeStamp from throwing "Illegal invocation" + : `typeof ${leaf} === "undefined"` + ); + objPath.push(leaf); + } + + code += ` + try { + if (${undefChecks.join(" || ")}) return; + } catch (e) { + return; + }`; + } + + for (let {original_name = target, wrapped_name, callable_name} of wrapper.wrapped_objects || []) { + if (original_name !== target) code += ` + if (typeof ${original_name} === undefined) return; `; + if (wrapped_name) code += `var ${wrapped_name} = window.${original_name};`; + if (callable_name) code += `var ${callable_name} = WrapHelper.pageAPI(window.${original_name});`; } - code += `${wrapper.helping_code || ''}`; + code += ` + ${wrapper.helping_code || ''}`; + if (wrapper.wrapping_function_body){ code += `${define_page_context_function(wrapper)}`; } @@ -174,12 +226,13 @@ var build_code = function(wrapper, ...args) { } } if (wrapper["wrapper_prototype"] !== undefined) { - code += `Object.setPrototypeOf(${wrapper.parent_object}.${wrapper.parent_object_property}, - ${wrapper.wrapper_prototype}); - ` + let source = wrapper.wrapper_prototype; + code += `if (${target.prototype} !== ${source.prototype}) { // prevent cyclic __proto__ errors on Proxy + Object.setPrototypeOf(${target}, ${source}); + }`; } code += ` - if (!${wrapper.nofreeze}) { + if (${wrapper.freeze}) { Object.freeze(${wrapper.parent_object}.${wrapper.parent_object_property}); } `; @@ -195,32 +248,286 @@ function wrap_code(wrappers) { if (wrappers.length === 0) { return; // Nothing to wrap } - var code = `(function() { - var original_functions = {}; - `; - for (tobewrapped of wrappers) { + + let build = wrapper => { try { - code += build_code(build_wrapping_code[tobewrapped[0]], tobewrapped.slice(1)); + return build_code(build_wrapping_code[wrapper[0]], wrapper.slice(1)); + } catch (e) { + console.error(e); + return ""; } - catch (e) { - console.log(e); - } - } - code += ` - var originalToStringF = Function.prototype.toString; - var originalToStringStr = Function.prototype.toString(); - Function.prototype.toString = function() { - var currentString = originalToStringF.call(this); - var originalStr = original_functions[currentString]; - if (originalStr !== undefined) { - return originalStr; + }; + + let code = (w => { + + // cross-wrapper globals + let xrayWindow = window; // the "privileged" xray window wrapper in Firefox + let WrapHelper; // xray boundary helper + { + const XRAY = (xrayWindow.top !== unwrappedWindow.top && typeof XPCNativeWrapper !== "undefined"); + let privilegedToPage = new WeakMap(); + let pageReady = new WeakSet(); + + let promise = obj => obj.then(r => forPage(r)); + + forPage = obj => { + if (typeof obj !== "object" && typeof obj !== "function" || obj === null + || pageReady.has(obj)) return obj; + if (privilegedToPage.has(obj)) return privilegedToPage.get(obj); // keep clone identity + let ret = obj; // fallback + if (XRAY) { + if (obj instanceof xrayWindow.Promise) { + return promise(obj); + } + if (obj instanceof unX(xrayWindow).Promise) { + return new xrayWindow.Promise((resolve, reject) => { + unX(xrayWindow).Promise.prototype.then.call(obj, + forPage(r => { + if (r.wrappedJSObject && r.wrappedJSObject === unX(r)) { + r = unX(r) + } else r = forPage(r); + resolve(r); + } + ), forPage(e => reject(e))) + }); + } + try { + if (obj.wrappedJSObject && obj.wrappedJSObject === unX(obj)) { + return obj; + } + } catch (e) {} + try { + ret = cloneInto(obj, unX(xrayWindow), {cloneFunctions: true, wrapReflectors: true}); + } catch (e) { + // can't be cloned: must be a Proxy + } + } else { + // Chromium: just use patchWindow's exportFunction() to make our wrappers look like native functions + if (typeof obj === "function") { + ret = exportFunction(obj, unX(xrayWindow)); + } } - else { - return currentString; + pageReady.add(ret); + privilegedToPage.set(obj, ret); + return ret; + } + + let fixProp = (d, prop, obj) => { + for (let accessor of ["set", "get"]) { + if (typeof d[accessor] === "function") { + let f = d[accessor]; + d[accessor] = exportFunction(d[accessor], obj, {}); + } } + if (typeof d.value === "object") d.value = forPage(d.value); + return d; }; - original_functions[Function.prototype.toString.toString()] = originalToStringStr; - })();`; - return code; + let OriginalProxy = unwrappedWindow.Proxy; + let Proxy = OriginalProxy; + let pageAPI, unX; + if (XRAY) { + + unX = o => XPCNativeWrapper.unwrap(o); + + // automatically export Proxy constructor parameters + let proxyConstructorHandler = forPage({ + construct(targetConstructor, args) { + let [target, handler] = unX(args); + let selfProxy = !!(target === WrapHelper.Proxy && handler.construct); + if (selfProxy) { + let {construct} = handler; + handler.construct = (target, args) => { + let proxy = construct(target, unX(args)); + pageReady.add(proxy); + return proxy; + } + } + + target = forPage(target); + handler = forPage(handler); + let proxy = new targetConstructor(target, handler); + pageReady.add(proxy); + return proxy; + }, + }); + Proxy = new OriginalProxy(OriginalProxy, proxyConstructorHandler); + let then; + let apiHandler = { + apply(target, thisArg, args) { + let pa = unX(args); + for (let j = pa.length; j-- > 0;) { + let a = pa[j]; + if (a && unX(a) === a) { + pa[j] = forPage(a); + } else if (typeof a === "function") { + pa[j] = new Proxy(a, apiHandler); + } + } + console.debug("apiHandler call", target, thisArg, pa); + let ret = target.apply(thisArg, pa); + if (ret) { + console.debug("apiHandler ret", ret, ret instanceof Promise, ret instanceof unX(xrayWindow).Promise, ret instanceof xrayWindow.Promise, ret.then); + if (ret instanceof xrayWindow.Promise) { + then = then || (then = new Proxy(xrayWindow.Promise.prototype.then, apiHandler)); + if (ret.wrappedJSObject) { + let p = unX(ret); + if (p === ret.wrappedJSObject) { + p.then = then + ret = p; + } + } + } else { + ret = forPage(ret); + } + } + return ret; + } + }; + + + + pageAPI = f => { + if (typeof f !== "function") return f; + return new Proxy(f, apiHandler); + } + } else { + pageAPI = unX = f => f; + } + + let overlay; + { + let overlayProtos = new WeakMap(); + let overlayObjects = new WeakMap(); + overlay = (obj, data) => { + obj = unX(obj); + let proto = obj.__proto__; + let proxiedProps = overlayProtos.get(proto); + if (!proxiedProps) overlayProtos.set(proto, proxiedProps = {}); + let props = Object.getOwnPropertyDescriptors(data); + for (let p in props) { + if (p in proxiedProps) continue; + for (let rootProto = proto; ;) { + let protoProps = Object.getOwnPropertyDescriptors(rootProto); + let protoProp = protoProps[p]; + if (!protoProp) { + rootProto = rootProto.__proto__; + if (rootProto) continue; + } + if (protoProp) { + let original; + if (protoProp.get) { + let getterHandler = forPage({ + apply(target, thisArg, args) { + let obj = unX(thisArg); + if (overlayObjects.has(obj)) { + let data = overlayObjects.get(obj); + return forPage(data[p]); + } + return target.apply(thisArg, args); + } + }); + let original = protoProp.get; + protoProp.get = new Proxy(protoProp.get, getterHandler); + } else if (typeof protoProp.value === "function") { + original = protoProp.value; + let methodHandler = forPage({ + apply(target, thisArg, args) { + let obj = unX(thisArg); + if (overlayObjects.has(obj)) { + let data = overlayObjects.get(obj); + return forPage(data[p].apply(thisArg, args)); + } + return target.apply(thisArg, args); + } + }); + protoProp.value = new Proxy(protoProp.value, methodHandler); + } else { + protoProp = null; + } + if (protoProp) { + Reflect.defineProperty(rootProto, p, protoProp); + proxiedProps[p] = {rootProto, original, protoProp}; + break; + } + } + Reflect.defineProperty(obj, p, forPage(props[p])); + break; + } + } + overlayObjects.set(obj, data); + return obj; + } + } + + WrapHelper = { + XRAY, // boolean, are we in a xray environment (i.e. on Firefox)? + shared: {}, // shared storage object for in inter-wrapper coordination + + // WrapHelper.forPage() can be used by "complex" proxies to explicitly + // prepare an object/function created in Firefox's sandboxed content script environment + // to be consumed/called from the page context, and to make replacements for native + // objects and functions provided by the wrappers look as much native as possible. + // in most cases, however, this gets automated by the code builders replacing + // Object methods found in the wrapper sources with their WrapHelper counterparts + // and by proxying "callable_name" functions through WrapHelper.pageAPI(). + forPage, + _forPage: x => x, // dummy for easily testing out the preparation + isForPage: obj => pageReady.has(obj), + unX, // safely waives xray wrappers + // xray-aware Object creation helpers, mostly used transparently by the code builders + defineProperty(obj, prop, descriptor, ...args) { + obj = unX(obj); + return Object.defineProperty(obj, prop, fixProp(descriptor, prop, obj), ...args); + }, + defineProperties(obj, descriptors, ...args) { + obj = unX(obj); + for (let [prop, d] of Object.entries(descriptors)) { + descriptors[prop] = fixProp(d, prop, obj); + } + return Object.defineProperties(obj, descriptors, ...args); + }, + create(proto, descriptors) { + let obj = forPage(Object.create(unX(proto))); + return descriptors ? this.defineProperties(obj, descriptors) && obj : obj; + }, + + // WrapHelper.overlay(obj, data) + // Proxies the prototype of the obj object in order to return the properties of the data object + // as if they were native properties (e.g. as if they were returned by getters on the prototype chain, + // rather than defined on the instance). + // This allows spoofing some native objects data in a less detectable / fingerprintable way than using + // Object.defineProperty(). See wrappingS-MCS.js for an example. + overlay, + // WrapHelper.pageAPI(f) + // Proxies the function/method f so that arguments and return values, and especially callbacks and + // Promise objects, are recursively managed in order to transparently marshal objects back + // and forth Firefox's sandbox for extensions and the page scripts. + pageAPI, + // the original Proxy constructor + OriginalProxy, + // our xray-aware proxied Proxy constructor + Proxy, + }; + Object.freeze(WrapHelper); + } + + with(unwrappedWindow) { + let window = unwrappedWindow; + let {Proxy} = WrapHelper; + let {Promise, Object, Array, JSON} = xrayWindow; + try { + // WRAPPERS // + + } finally { + // cleanup environment if necessary + } + + } + }).toString().replace('// WRAPPERS //', + wrappers.map(build) + .join("\n") + .replace(/\bObject\.(create|definePropert)/g, "WrapHelper.$1")); + + return `(${code})();`; } diff --git a/common/document_start.js b/common/document_start.js index 7c7d207..3f8dddf 100644 --- a/common/document_start.js +++ b/common/document_start.js @@ -22,19 +22,33 @@ // along with this program. If not, see . // -function configureInjection({code, wrappers, ffbug1267027, domainHash, sessionHash}) { - console.debug("configureInjection", new Error().stack, document.readyState); +var wrappersPort; + +function configureInjection({code, wrappers, domainHash, sessionHash}) { configureInjection = () => false; // one shot + if(browser.extension.inIncognitoContext){ + // Redefine the domainHash for incognito context: + // Compute the SHA256 hash of the original hash so that the incognito hash is: + // * significantly different to the original domainHash, + // * computationally difficult to revert, + // * the same for all incognito windows (for the same domain). + var hash = sha256.create(); + hash.update(JSON.stringify(domainHash)); + domainHash = hash.hex(); + } var aleaCode = `(() => { - var domainHash = ${JSON.stringify(domainHash)}; - var sessionHash = ${JSON.stringify(sessionHash)}; + var domainHash = ${JSON.stringify(domainHash)}; ${alea} var prng = new alea(domainHash); ${code} })()`; - - injectScript(aleaCode, wrappers, ffbug1267027); - return true; + try { + wrappersPort = patchWindow(aleaCode); + return true; + } catch (e) { + console.error(e, `Trying to run\n${aleaCode}`) + } + return false; } if ("configuration" in window) { console.debug("Early configuration found!", configuration); diff --git a/common/ffbug1267027.js b/common/ffbug1267027.js deleted file mode 100644 index 3079dce..0000000 --- a/common/ffbug1267027.js +++ /dev/null @@ -1,187 +0,0 @@ -/** \file - * \brief Code for handling Firefox bug 1267027 - * - * \author Copyright (C) 2019-2021 Libor Polcak - * - * \license SPDX-License-Identifier: GPL-3.0-or-later - */ -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without ev1267027en the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -/// The original script injecting function -var injectScriptElement = injectScript; - -/** - * This function create code (as string) that creates code that can be used to inject (or overwrite) - * a function in the page context. Supporting code for dealing with bug - * https://bugzilla.mozilla.org/show_bug.cgi?id=1267027. - */ -function define_page_context_function_ffbug(wrapper) { - return `(function() { - function tobeexported(${wrapper.wrapping_function_args}) { - ${wrapper.wrapping_function_body} - } - exportFunction(tobeexported, ${wrapper.parent_object}, {defineAs: '${wrapper.parent_object_property}'}); - ${wrapper.post_replacement_code || ''} - })(); - ` -} - -/** - * This function creates code that assigns an already defined function to given property. Supporting - * code for dealing with bug https://bugzilla.mozilla.org/show_bug.cgi?id=1267027. - */ -function generate_assign_function_code_ffbug(code_spec_obj) { - return `exportFunction(${code_spec_obj.export_function_name}, ${code_spec_obj.parent_object}, - {defineAs: '${code_spec_obj.parent_object_property}'}); - ` -} - -/** - * This function wraps object properties using Object.defineProperties. Supporting - * code for dealing with bug https://bugzilla.mozilla.org/show_bug.cgi?id=1267027. - */ -function generate_object_properties_ffbug(code_spec_obj) { - var code = ""; - for (assign of code_spec_obj.wrapped_objects) { - code += `var ${assign.wrapped_name} = window.wrappedJSObject.${assign.original_name};`; - } - code += ` - window.wrappedJSObject.Object.defineProperties( - window.wrappedJSObject.${code_spec_obj.parent_object}, - { - ${code_spec_obj.parent_object_property}: {` - for (wrap_spec of code_spec_obj.wrapped_properties) { - code += `${wrap_spec.property_name}: ${wrap_spec.property_value}`; - } - code += ` - } - } - );`; - return code; -} - -/** - * This function removes a property. Supporting - * code for dealing with bug https://bugzilla.mozilla.org/show_bug.cgi?id=1267027. - - */ -function generate_delete_properties_ffbug(code_spec_obj) { - var code = ` - `; - for (prop of code_spec_obj.delete_properties) { - code += ` - if ("${prop}" in window.wrappedJSObject.${code_spec_obj.parent_object}) { - // Delete only properties that are available. - // The if should be safe to be deleted but it can possibly reduce fingerprintability - Object.defineProperty( - window.wrappedJSObject.${code_spec_obj.parent_object}, - "${prop}", {get: undefined, set: undefined, configurable: false, enumerable: false} - ); - } - ` - } - return code; -} - -/** - * This function generates code that makes an assignment. Supporting - * code for dealing with bug https://bugzilla.mozilla.org/show_bug.cgi?id=1267027. - */ -function generate_assignement_ffbug(code_spec_obj) { - return `window.wrappedJSObject.${code_spec_obj.parent_object}.${code_spec_obj.parent_object_property} = ${code_spec_obj.value};` -} - -/** - * Alternative definition of the build_code function. - * - * FIXME:this code needs improvements, see bug #25 - */ -function build_code_ffbug(wrapper, ...args) { - var post_wrapping_functions = { - function_define: define_page_context_function_ffbug, - function_export: generate_assign_function_code_ffbug, - object_properties: generate_object_properties_ffbug, - delete_properties: generate_delete_properties_ffbug, - assign: generate_assignement_ffbug, - }; - var code = ""; - for (wrapped of wrapper.wrapped_objects) { - code += `var ${wrapped.wrapped_name} = window.wrappedJSObject.${wrapped.original_name};`; - } - code += `${wrapper.helping_code || ''}`; - if (wrapper.wrapping_function_body) { - code += `function tobeexported(${wrapper.wrapping_function_args}) { - ${wrapper.wrapping_function_body} - } - `; - } - if (wrapper["wrapper_prototype"] !== undefined) { - code += `Object.setPrototypeOf(tobeexported, ${wrapper.wrapper_prototype}); - `; - } - if (wrapper.wrapping_function_body) { - code += `exportFunction(tobeexported, ${wrapper.parent_object}, {defineAs: '${wrapper.parent_object_property}'}); - ` - } - if (wrapper["post_wrapping_code"] !== undefined) { - for (code_spec of wrapper["post_wrapping_code"]) { - code += post_wrapping_functions[code_spec.code_type](code_spec); - } - } - return enclose_wrapping(code, ...args); -}; - -/** - * Determine if we are running in the context where FF blocks script inserting due to bug - * https://bugzilla.mozilla.org/show_bug.cgi?id=1267027 - */ -function is_firefox_blocking_scripts() { - var random_str = "JSR" + gen_random32(); - if (window.wrappedJSObject[random_str] !== undefined) { - // Unlikely, but we hit an existing property - return is_firefox_blocking_scripts(); // rerun and generate a new number - } - injectScriptElement(enclose_wrapping(`window.${random_str} = 1;`)); - if (window.wrappedJSObject[random_str] === undefined) { - return true; - } - else { - injectScriptElement(enclose_wrapping(`delete window.${random_str};`)); - return false; - } -} - -/** - * This function deals with the Firefox bug https://bugzilla.mozilla.org/show_bug.cgi?id=1267027 - * See also https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts - */ -window.injectScript = function prevent_ffbug(code, wrappers, ffbug_present) { - if (ffbug_present === undefined) { - ffbug_present = is_firefox_blocking_scripts(); - browser.runtime.sendMessage({ - message: "ffbug1267027", - url: window.location.href, - present: ffbug_present - }); - } - if (ffbug_present === true) { - build_code = build_code_ffbug; - eval(wrap_code(wrappers)); - } - else { - injectScriptElement(code); - } -} diff --git a/common/helpers.js b/common/helpers.js index c817b6b..d1cdcc2 100644 --- a/common/helpers.js +++ b/common/helpers.js @@ -105,3 +105,16 @@ function shuffleArray(array) { [array[i], array[j]] = [array[j], array[i]]; } } +/** + * \brief makes number from substring of given string - should work as reinterpret_cast + * \param str String + * \param length Number specifying substring length + */ +function strToUint(str, length){ + var sub = str.substring(0,length); + var ret = ""; + for (var i = sub.length-1; i >= 0; i--) { + ret += ((sub[i].charCodeAt(0)).toString(2).padStart(8, "0")); + } + return "0b"+ret; +}; diff --git a/common/inject.js b/common/inject.js deleted file mode 100644 index fc1bb3c..0000000 --- a/common/inject.js +++ /dev/null @@ -1,51 +0,0 @@ -/** \file - * \brief Inject code to page scripts - * - * \author Copyright (C) 2019 Libor Polcak - * - * \license SPDX-License-Identifier: GPL-3.0-or-later - */ -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without ev1267027en the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - - -/** - * Execute given script in the page's JavaScript context. - * - * This function is a modified version of the similar function from - * Privacy Badger - * https://github.com/EFForg/privacybadger/blob/master/src/js/utils.js - * \copyright Copyright (C) 2014 Electronic Frontier Foundation - * - * Derived from Adblock Plus - * \copyright Copyright (C) 2006-2013 Eyeo GmbH - * - * Privacy Badger is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * @param {String} text The content of the script to insert. - */ -window.injectScript = function (text) { - var parent = document.documentElement; - var script = document.createElement('script'); - - script.text = text; - script.async = false; - - parent.insertBefore(script, parent.firstChild); - parent.removeChild(script); -}; - diff --git a/common/level_cache.js b/common/level_cache.js index 833e5e4..832e8fc 100644 --- a/common/level_cache.js +++ b/common/level_cache.js @@ -21,15 +21,10 @@ // /** - * Keep list of domains that are known (not) to be affected by the - * Firefox bug 1267027. - */ -var domains_bug1267027 = {}; - -/** * Returns the a Promise which resolves to the configuration * for the current level to be used by the content script for injection - * @param {url} string + * @param {url} string + * @param isPrivate bool specifying incognito mode */ @@ -37,13 +32,11 @@ function getContentConfiguration(url) { return new Promise(resolve => { function resolve_promise() { var page_level = getCurrentLevelJSON(url); - let {sessionHash, domainHash} = Hashes.getFor(url); + let {domainHash} = Hashes.getFor(url); resolve({ code: page_level[1], wrappers: page_level[0].wrappers, - ffbug1267027: domains_bug1267027[url], - sessionHash, - domainHash, + domainHash }); } if (levels_initialised === true) { @@ -66,9 +59,6 @@ function contentScriptLevelSetter(message) { switch (message.message) { case "get wrapping for URL": return getContentConfiguration(message.url) - case "ffbug1267027": - domains_bug1267027[message.url] = message.present; - break; } } browser.runtime.onMessage.addListener(contentScriptLevelSetter); @@ -76,7 +66,7 @@ browser.runtime.onMessage.addListener(contentScriptLevelSetter); /** * Register a dynamic content script to be ran for early configuration and - * injection of the wrapper, hopefully before of the asynchronous + * injection of the wrapper, hopefully before of the asynchronous * message listener above * \see Depends on /nscl/service/DocStartInjection.js */ diff --git a/common/levels.js b/common/levels.js index bc1f7ec..866d9c5 100644 --- a/common/levels.js +++ b/common/levels.js @@ -51,7 +51,7 @@ var wrapping_groups = { groups: [ { name: "time_precision", - description: "Manipulate the time precision provided by Date and performance", + description: "Limit the precision of high resolution time stamps (Date, Performance, events, Gamepad API, Web VR API)", description2: ["If you enable Geolocation API wrapping below, timestamps provided by the Geolocation API will be wrapped as well"], options: [ { @@ -90,6 +90,12 @@ var wrapping_groups = { "PerformanceEntry.prototype", // ECMA "window.Date", + // DOM + "Event.prototype.timeStamp", + // GP + "Gamepad.prototype.timestamp", + // VR + "VRFrameData.prototype.timestamp", ], }, { @@ -165,7 +171,7 @@ var wrapping_groups = { }, { name: "webgl", - description: "Protect against wegbl fingerprinting", + description: "Protect against WEBGL fingerprinting", description2: [ "Function WebGLRenderingContext.getParameter() returns modified/bottom values for certain parameters", "WebGLRenderingContext functions .getFramebufferAttachmentParameter(), .getActiveAttrib(), .getActiveUniform(), .getAttribLocation(), .getBufferParameter(), .getProgramParameter(), .getRenderbufferParameter(), .getShaderParameter(), .getShaderPrecisionFormat(), .getTexParameter(), .getUniformLocation(), .getVertexAttribOffset(), .getSupportedExtensions() and .getExtension() return modified values", @@ -189,7 +195,7 @@ var wrapping_groups = { ], }], wrappers: [ - // WEGBL + // WEBGL "WebGLRenderingContext.prototype.getParameter", "WebGL2RenderingContext.prototype.getParameter", "WebGLRenderingContext.prototype.getFramebufferAttachmentParameter", @@ -251,7 +257,7 @@ var wrapping_groups = { }], wrappers: [ // NP - "navigator.plugins", + "navigator.plugins", // also modifies "navigator.mimeTypes", ], }, { @@ -345,7 +351,8 @@ var wrapping_groups = { ], wrappers: [ // AJAX - "window.XMLHttpRequest", + "XMLHttpRequest.prototype.open", + "XMLHttpRequest.prototype.send", ], }, { @@ -481,6 +488,30 @@ var wrapping_groups = { ], }, { + name: "gamepads", + description: "Prevent websites from learning information on local gamepads", + description2: [], + default: true, + options: [], + wrappers: [ + // GAMEPAD + "navigator.getGamepads", + ], + }, + { + name: "vr", + description: "Prevent websites from learning information on local Virtual Reality displays", + description2: [], + default: true, + options: [], + wrappers: [ + // VR + "navigator.activeVRDisplays", + // XR + "navigator.xr", + ], + }, + { name: "analytics", description: "Prevent sending analytics through Beacon API", description2: [], @@ -500,6 +531,7 @@ var wrapping_groups = { wrappers: [ // BATTERY "navigator.getBattery", + "window.BatteryManager", ], }, { @@ -598,6 +630,7 @@ var level_1 = { "time_precision_precision": 2, "time_precision_randomize": false, "hardware": true, + "hardware_method": 0, "battery": true, "geolocation": true, "geolocation_locationObfuscationType": 2, @@ -613,20 +646,22 @@ var level_2 = { "time_precision_precision": 1, "time_precision_randomize": false, "hardware": true, - "hardware_method": 2, + "hardware_method": 0, "battery": true, "htmlcanvaselement": true, - "htmlcanvaselement_method": 1, + "htmlcanvaselement_method": 0, "audiobuffer": true, "audiobuffer_method": 0, "webgl": true, - "webgl_method": 1, + "webgl_method": 0, "plugins": true, - "plugins_method": 1, + "plugins_method": 0, "enumerateDevices": true, "enumerateDevices_method": 1, "geolocation": true, "geolocation_locationObfuscationType": 3, + "gamepads": true, + "vr": true, "analytics": true, "windowname": true, }; @@ -644,11 +679,11 @@ var level_3 = { "htmlcanvaselement": true, "htmlcanvaselement_method": 1, "audiobuffer": true, - "audiobuffer_method": 0, + "audiobuffer_method": 1, "webgl": true, "webgl_method": 1, "plugins": true, - "plugins_method": 1, + "plugins_method": 2, "enumerateDevices": true, "enumerateDevices_method": 2, "xhr": true, @@ -664,6 +699,8 @@ var level_3 = { "webworker_approach_slow": false, "geolocation": true, "geolocation_locationObfuscationType": 0, + "gamepads": true, + "vr": true, "analytics": true, "windowname": true, }; diff --git a/common/options.html b/common/options.html index d37b286..0af13d4 100644 --- a/common/options.html +++ b/common/options.html @@ -26,8 +26,6 @@ SPDX-License-Identifier: GPL-3.0-or-later

External links

  • Test page
  • -
  • Test page - for CSP bug in Firefox
  • Levels
  • Permissions
  • Source code
  • @@ -63,10 +61,12 @@ SPDX-License-Identifier: GPL-3.0-or-later

    Network boundary shield prevents web pages to use the browser as a proxy between local network - and the public Internet. See the + and the public Internet. See our + blog post and Force - Point report for an example of the attack. + Point report for examples of attacks handled by the Network Boundary Shield. The protection encapsulates the WebRequest API, so it captures all outgoing requests.

    diff --git a/common/options_domains.html b/common/options_domains.html index 53b1388..dc214d7 100644 --- a/common/options_domains.html +++ b/common/options_domains.html @@ -25,8 +25,6 @@ SPDX-License-Identifier: GPL-3.0-or-later

    External links

    • Test page
    • -
    • Test page - for CSP bug in Firefox
    • Levels
    • Permissions
    • Source code
    • diff --git a/common/session_hash.js b/common/session_hash.js index b3e0337..8c5a3e5 100644 --- a/common/session_hash.js +++ b/common/session_hash.js @@ -21,19 +21,27 @@ // along with this program. If not, see . // +/** + * Object for generating and caching domain/session hashes + * getFor method used to get domain hashes from given url + * + * \note cached visited domains with related keys are only deleted after end of the session + */ var Hashes = { - sessionHash: gen_random64().toString(), - visitedDomains: {}, - getFor(url) { + sessionHash : gen_random64().toString(), + visitedDomains : {}, + getFor(url){ if (!url.origin) url = new URL(url); let {origin} = url; - let domainHash = this.visitedDomains[origin]; + let domainHash = this.visitedDomains[origin]; if (!domainHash) { - domainHash = this.visitedDomains[origin] = generateId(); + let hmac = sha256.hmac.create(this.sessionHash); + hmac.update(url.origin); + domainHash = hmac.hex(); + this.visitedDomains[origin] = domainHash; } return { - sessionHash: this.sessionHash, - domainHash, + domainHash }; } }; diff --git a/common/update.js b/common/update.js index 5ca3550..ed95140 100644 --- a/common/update.js +++ b/common/update.js @@ -41,9 +41,6 @@ function installUpdate() { * whitelistedHosts: {} // associative array of hosts that are removed from http protection control (hostname => boolean) * requestShieldOn: {} // Boolean, if it's TRUE or undefined, the http request protection is turned on, if it's FALSE, the protection si turned off * - *------local - * sessionHash: {}, // 64bit session hash - * visitedDomains: {} // associative array of domain hashes (domain name => 32 byte hash) * */ browser.storage.sync.get(null).then(function (item) { @@ -154,6 +151,39 @@ function installUpdate() { } item.version = 2.7; } + if (item.version < 2.8) { + for (level in item["custom_levels"]) { + let l = item["custom_levels"][level]; + if (l.htmlcanvaselement) { + l.plugins = true; + if (l.htmlcanvaselement_method == 0) { + l.plugins_method = 0; + } + else { + l.plugins_method = 2; + } + } + } + item.version = 2.8; + } + if (item.version < 2.9) { + for (level in item["custom_levels"]) { + let l = item["custom_levels"][level]; + if (l.analytics) { + l.gamepads = true; + } + } + item.version = 2.9; + } + if (item.version < 2.10) { + for (level in item["custom_levels"]) { + let l = item["custom_levels"][level]; + if (l.gamepads) { + l.vr = true; + } + } + item.version = 2.10; + } browser.storage.sync.set(item); }); } diff --git a/common/wrappingS-AJAX.js b/common/wrappingS-AJAX.js index 30deed7..c2728d5 100644 --- a/common/wrappingS-AJAX.js +++ b/common/wrappingS-AJAX.js @@ -4,6 +4,7 @@ * \see https://xhr.spec.whatwg.org/ * * \author Copyright (C) 2019 Libor Polcak + * \author Copyright (C) 2021 Giorgio Maone * * \license SPDX-License-Identifier: GPL-3.0-or-later */ @@ -27,29 +28,40 @@ (function() { var wrappers = [ { - parent_object: "window", - parent_object_property: "XMLHttpRequest", + parent_object: "XMLHttpRequest.prototype", + parent_object_property: "open", wrapped_objects: [ { - original_name: "XMLHttpRequest", - wrapped_name: "originalXMLHttpRequest", + original_name: "XMLHttpRequest.prototype.open", + wrapped_name: "originalOpen", }, ], helping_code: "var blockEveryXMLHttpRequest = args[0]; var confirmEveryXMLHttpRequest = args[1];", - wrapping_function_args: "", + wrapping_function_args: "...args", wrapping_function_body: ` - var currentXMLHttpRequestObject = new originalXMLHttpRequest(); - var originalXMLHttpRequestOpenFunction = currentXMLHttpRequestObject.open; - currentXMLHttpRequestObject.open = function(...args) { - if (blockEveryXMLHttpRequest || (confirmEveryXMLHttpRequest && !confirm('There is a XMLHttpRequest on URL ' + args[1] + '. Do you want to continue?'))) { - currentXMLHttpRequestObject.send = function () {}; // Prevents throwing an exception - return undefined; - } - else { - return originalXMLHttpRequestOpenFunction.call(this, ...args); - } - }; - return currentXMLHttpRequestObject; + let {XHR_blocked} = WrapHelper.shared; + if (blockEveryXMLHttpRequest || (confirmEveryXMLHttpRequest && !confirm('There is a XMLHttpRequest on URL ' + args[1] + '. Do you want to continue?'))) { + XHR_blocked.add(this); + return []; + } + XHR_blocked.delete(this); + return originalOpen.call(this, ...args); + `, + }, + { + parent_object: "XMLHttpRequest.prototype", + parent_object_property: "send", + wrapped_objects: [ + { + original_name: "XMLHttpRequest.prototype.send", + wrapped_name: "originalSend", + }, + ], + + helping_code: "WrapHelper.shared.XHR_blocked = new WeakSet();", + wrapping_function_args: "...args", + wrapping_function_body: ` + if (!WrapHelper.shared.XHR_blocked.has(this)) return originalSend.call(this, ...args); `, }, ] diff --git a/common/wrappingS-BATTERY-CR.js b/common/wrappingS-BATTERY-CR.js index 51643a3..72ccf2f 100644 --- a/common/wrappingS-BATTERY-CR.js +++ b/common/wrappingS-BATTERY-CR.js @@ -3,7 +3,7 @@ * * \see https://www.w3.org/TR/battery-status/ * - * \author Copyright (C) 2020 Peter Hornak + * \author Copyright (C) 2021 Libor Polčák * * \license SPDX-License-Identifier: GPL-3.0-or-later */ @@ -22,6 +22,24 @@ // along with this program. If not, see . // +/** \file + * \ingroup wrappers + * + * The `navigator.getBattery()` reports the state of the battery and can be + * misused to fingerprint users for a short term. The API was removed from + * Firefox. + * + * \see https://lukaszolejnik.com/battery + * + * The API is still supported in browsers derived from Chromium. The wrapper + * mimics Firefox behaviour. + * + * \bug Because we mimic Firefox behaviour, a Chromium derived browser + * becomes more easily fingerprintable. This can be fixed by properly + * wrapping `BatteryManager.prototype` getters and setters. + */ + + /* * Create private namespace */ @@ -31,18 +49,25 @@ parent_object: "navigator", parent_object_property: "getBattery", wrapped_objects: [], - helping_code: ` - if (navigator.getBattery === undefined) { - return; + post_wrapping_code: [ + { + code_type: "delete_properties", + parent_object: "navigator", + delete_properties: ["getBattery"], + } + ], + }, + { + parent_object: "window", + parent_object_property: "BatteryManager", + wrapped_objects: [], + post_wrapping_code: [ + { + code_type: "delete_properties", + parent_object: "window", + delete_properties: ["BatteryManager"], } - `, - original_function: "navigator.getBattery", - wrapping_function_body: ` - return undefined; - `, - post_replacement_code: ` - delete BatteryManager; - ` + ], }, ]; add_wrappers(wrappers); diff --git a/common/wrappingS-DOM.js b/common/wrappingS-DOM.js new file mode 100644 index 0000000..5f726af --- /dev/null +++ b/common/wrappingS-DOM.js @@ -0,0 +1,84 @@ +/** \file + * \brief This file contains wrappers for the DOM API + * + * \see https://dom.spec.whatwg.org/ + * + * \author Copyright (C) 2021 Libor Polcak + * + * \license SPDX-License-Identifier: GPL-3.0-or-later + */ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +/** \file + * \ingroup wrappers + * + * The events carry timestamp of their creation. As we allow wrapping of + * several ways to obtain timestamps, we need to provide the same precision + * for the Event API. + */ + +(function() { + var remember_past_values = `var precision = args[0]; + var doNoise = args[1]; + var pastValues = {}; + ${rounding_function} + ${noise_function} + var mitigationF = rounding_function; + if (doNoise === true){ + mitigationF = function(value, precision) { + let params = [value, precision]; + if (params in pastValues) { + return pastValues[params]; + } + let result = noise_function(...params); + pastValues[params] = result; + return result; + } + } + `; + var wrappers = [ + { + /** + * \see https://dom.spec.whatwg.org/#ref-for-dom-event-timestamp%E2%91%A0 + */ + parent_object: "Event.prototype", + parent_object_property: "timeStamp", + wrapped_objects: [], + helping_code: remember_past_values + `let origGet = Object.getOwnPropertyDescriptor(Event.prototype, "timeStamp").get`, + post_wrapping_code: [ + { + code_type: "object_properties", + parent_object: "Event.prototype", + parent_object_property: "timeStamp", + wrapped_objects: [], + /** \brief replaces Event.prototype.timeStamp getter to create + * a timestamp with the desired precision. + */ + wrapped_properties: [ + { + property_name: "get", + property_value: ` + function() { + return mitigationF(origGet.call(this), precision); + }`, + }, + ], + } + ], + }, + ] + add_wrappers(wrappers); +})() diff --git a/common/wrappingS-ECMA-ARRAY.js b/common/wrappingS-ECMA-ARRAY.js index 6a29547..992fe18 100644 --- a/common/wrappingS-ECMA-ARRAY.js +++ b/common/wrappingS-ECMA-ARRAY.js @@ -248,15 +248,17 @@ var proxyHandler = `{ var random_idx = Math.floor(Math.random() * target['length']); // Load random index from array var rand_val = target[random_idx]; + /* let proto_keys = ['buffer', 'byteLength', 'byteOffset', 'length']; if (proto_keys.indexOf(key) >= 0) { return target[key]; } + */ // offsetF argument needs to be in array range if (typeof key !== 'symbol' && Number(key) >= 0 && Number(key) < target.length) { key = offsetF(key) } - let value = Reflect.get(...arguments); + let value = target[key] return typeof value == 'function' ? value.bind(target) : value; }, set(target, key, value) { @@ -267,7 +269,7 @@ var proxyHandler = `{ if (typeof key !== 'symbol' && Number(key) >= 0 && Number(key) < target.length) { key = offsetF(key) } - return Reflect.set(...arguments); + return target[key] = value; } }`; @@ -551,7 +553,6 @@ function redefineDataViewFunctions(target, offsetF, doMapping) { _data[offsetF(i)] = arr[i]; } } - let _target = target; var proxy = new newProxy(_data, ${proxyHandler}); // Proxy has to support all methods, original object supports. ${offsetDecorator}; @@ -650,8 +651,8 @@ function redefineDataViewFunctions(target, offsetF, doMapping) { has (target, key) { return (is_proxy === key) || (key in target); } - }; - let newProxy = new Proxy(Proxy, { + }; + let newProxy = new originalProxy(originalProxy, { construct(target, args) { return new originalProxy(new target(...args), proxyHandler); } diff --git a/common/wrappingS-ECMA-DATE.js b/common/wrappingS-ECMA-DATE.js index fffb5d6..6d1cf6a 100644 --- a/common/wrappingS-ECMA-DATE.js +++ b/common/wrappingS-ECMA-DATE.js @@ -68,7 +68,7 @@ parent_object: "window.Date", parent_object_property: "now", wrapping_function_args: "", - wrapping_function_body: "return func(originalDateConstructor.now.call(Date), precision);", + wrapping_function_body: "return func(originalF.call(Date), precision);", }, { code_type: "function_export", diff --git a/common/wrappingS-GEO.js b/common/wrappingS-GEO.js index 3e17b7d..0d8e943 100644 --- a/common/wrappingS-GEO.js +++ b/common/wrappingS-GEO.js @@ -6,6 +6,7 @@ * \author Copyright (C) 2019 Martin Timko * \author Copyright (C) 2020 Libor Polcak * \author Copyright (C) 2020 Peter Marko + * \author Copyright (C) 2021 Giorgio Maone * * \license SPDX-License-Identifier: GPL-3.0-or-later */ @@ -54,10 +55,12 @@ (function() { var processOriginalGPSDataObject_globals = gen_random32 + ` /** - * Make sure that repeated calls shows the same position to reduce - * fingerprintablity. + * Make sure that repeated calls shows the same position (BUT different objects, via cloning) + * to reduce fingerprintablity. */ - var previouslyReturnedCoords = undefined; + let previouslyReturnedCoords; + let clone = obj => Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)); + /** * \brief Store the limit for the returned timestamps. * @@ -96,7 +99,22 @@ * * As there are not many people near poles, we do not believe this wrapping is useful near poles * so we do not consider this bug as important. */ - var processOriginalGPSDataObject = ` + + function spoofCall(fakeData, originalPositionObject, successCallback) { + // proxying the original object lessens the fingerprintable weirdness + // (e.g. accessors on the instance rather than on the prototype) + fakeData = clone(fakeData); + let pos = new Proxy(originalPositionObject, { + get(target, key) { + return (key in fakeData) ? fakeData[key] : target[key]; + }, + getPrototypeOf(target) { + return Object.getPrototypeOf(target); + } + }); + successCallback(pos); + } + function processOriginalGPSDataObject(expectedMaxAge, originalPositionObject) { if (expectedMaxAge === undefined) { expectedMaxAge = 0; // default value @@ -104,21 +122,18 @@ // Set reasonable expectedMaxAge of 1 hour for later computation expectedMaxAge = Math.min(3600000, expectedMaxAge); geoTimestamp = Math.max(geoTimestamp, Date.now() - Math.random()*expectedMaxAge); + + let spoofPos = coords => { + let pos = { timestamp: geoTimestamp }; + if (coords) pos.coords = coords; + spoofCall(pos, originalPositionObject, successCallback); + }; + if (provideAccurateGeolocationData) { - var pos = { - coords: originalPositionObject.coords, - timestamp: geoTimestamp // Limit the timestamp accuracy - }; - successCallback(pos); - return; + return spoofPos(); } - if (previouslyReturnedCoords !== undefined) { - var pos = { - coords: previouslyReturnedCoords, - timestamp: geoTimestamp - }; - successCallback(pos); - return; + if (previouslyReturnedCoords) { + return spoofPos(clone(previouslyReturnedCoords)); } const EQUATOR_LEN = 40074; @@ -164,25 +179,18 @@ var newAccuracy = DESIRED_ACCURACY_KM * 1000 * 2.5; // in meters - const editedPositionObject = { - coords: { - latitude: newLatitude, - longitude: newLongitude, - altitude: null, - accuracy: newAccuracy, - altitudeAccuracy: null, - heading: null, - speed: null, - __proto__: originalPositionObject.coords.__proto__ - }, - timestamp: geoTimestamp, - __proto__: originalPositionObject.__proto__ + previouslyReturnedCoords = { + latitude: newLatitude, + longitude: newLongitude, + altitude: null, + accuracy: newAccuracy, + altitudeAccuracy: null, + heading: null, + speed: null, + __proto__: originalPositionObject.coords.__proto__ }; - Object.freeze(editedPositionObject.coords); - previouslyReturnedCoords = editedPositionObject.coords; - successCallback(editedPositionObject); - } - `; + spoofPos(previouslyReturnedCoords); + }; /** * \brief process the parameters of the wrapping function * @@ -223,7 +231,6 @@ delete_properties: ["geolocation"], } ], - nofreeze: true, }, { parent_object: "navigator.geolocation", @@ -232,7 +239,7 @@ wrapped_objects: [ { original_name: "navigator.geolocation.getCurrentPosition", - wrapped_name: "originalGetCurrentPosition", + callable_name: "originalGetCurrentPosition", }, ], helping_code: setArgs + processOriginalGPSDataObject_globals, @@ -240,11 +247,13 @@ /** \fn fake navigator.geolocation.getCurrentPosition * \brief Provide a fake geolocation position */ - wrapping_function_body: processOriginalGPSDataObject + ` + wrapping_function_body: ` + ${spoofCall} + ${processOriginalGPSDataObject} var options = { enableHighAccuracy: false, }; - try { + if (origOptions) try { if ("timeout" in origOptions) { options.timeout = origOptions.timeout; } @@ -277,11 +286,7 @@ if (provideAccurateGeolocationData) { function wrappedSuccessCallback(originalPositionObject) { geoTimestamp = Date.now(); // Limit the timestamp accuracy by calling possibly wrapped function - var pos = { - coords: originalPositionObject.coords, - timestamp: geoTimestamp - }; - successCallback(pos); + return spoofCall({ timestamp: geoTimestamp }, originalPositionObject, succesCallback); } originalWatchPosition.call(this, wrappedSuccessCallback, errorCallback, origOptions); } diff --git a/common/wrappingS-GP.js b/common/wrappingS-GP.js new file mode 100644 index 0000000..abc09cc --- /dev/null +++ b/common/wrappingS-GP.js @@ -0,0 +1,140 @@ +/** \file + * \brief This file contains wrappers for the Gamepad API + * + * \see https://w3c.github.io/gamepad/ + * + * \author Copyright (C) 2021 Libor Polcak + * + * \license SPDX-License-Identifier: GPL-3.0-or-later + */ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +/** \file + * \ingroup wrappers + * + * navigator.getGamepads() allows any page script to learn the gampepads + * connected to the computer if the feature is not blocked by the + * Feature-Policy. + * + * U. Iqbal, S. Englehardt and Z. Shafiq, "Fingerprinting the + * Fingerprinters: Learning to Detect Browser Fingerprinting Behaviors," + * in 2021 2021 IEEE Symposium on Security and Privacy (SP), San Francisco, + * CA, US, 2021 pp. 283-301 observed + * (https://github.com/uiowa-irl/FP-Inspector/blob/master/Data/potential_fingerprinting_APIs.md) + * that the interface is used in the wild to fingerprint users. As it is + * likely that only a minority of users have a gamepad connected and the API + * provides additional information on the HW, it is likely that users with + * a gamepad connected are easily fingerprintable. + * + * As we expect that the majority of the users does not have a gamepad + * connected, we provide only a single mitigation - the wrapped APIs returns + * an empty list. + * + * \bug The standard provides an event *gamepadconnected* and + * *gamepaddisconnected* that fires at least on the window object. We do not + * mitigate the event to fire and consequently, it is possible that an + * adversary can learn that a gamepad was (dis)connected but there was no + * change in the result of the navigator.getGamepads() API. + * + * The gamepad representing object carries a timestamp of the last change of + * the gamepad. As we allow wrapping of several ways to obtain timestamps, + * we need to provide the same precision for the Gamepad object. + */ + +(function() { + var remember_past_ts_values = `var precision = args[0]; + var doNoise = args[1]; + var pastValues = {}; + ${rounding_function} + ${noise_function} + var mitigationF = rounding_function; + if (doNoise === true){ + mitigationF = function(value, precision) { + let params = [value, precision]; + if (params in pastValues) { + return pastValues[params]; + } + let result = noise_function(...params); + pastValues[params] = result; + return result; + } + } + `; + var wrappers = [ + { + parent_object: "navigator", + parent_object_property: "getGamepads", + wrapped_objects: [{ + original_name: "navigator.getGamepads()", + wrapped_name: "origGamepads", + }], + helping_code: "", + wrapping_function_body: ` + if (Array.isArray(origGamepads)) { + // e.g. Gecko + return new window.Array(); + } + else { + // e.g. Chromium based + var l = new window.Object(); + // Based on our experiments and web search results like + // https://stackoverflow.com/questions/41251051/is-the-html5-gamepad-api-limited-to-only-4-controllers + // https://stackoverflow.com/questions/32619456/navigator-getgamepads-return-an-array-of-undefineds + // we try to mimic common value of the property + l[0] = null; + l[1] = null; + l[2] = null; + l[3] = null; + l.length = 4; + window.Object.setPrototypeOf(l, origGamepads.__proto__); + return l; + } + `, + }, + { + /** + * \see https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/timestamp + * \note that at the time of the writing of the prototype, the timestamp + * property was not supported by any browser. + */ + parent_object: "Gamepad.prototype", + parent_object_property: "timestamp", + wrapped_objects: [], + helping_code: remember_past_ts_values + `let origGet = Object.getOwnPropertyDescriptor(Gamepad.prototype, "timestamp").get`, + post_wrapping_code: [ + { + code_type: "object_properties", + parent_object: "Gamepad.prototype", + parent_object_property: "timestamp", + wrapped_objects: [], + /** \brief replaces Gamepad.timestamp getter to create + * a timestamp with the desired precision. + */ + wrapped_properties: [ + { + property_name: "get", + property_value: ` + function() { + return mitigationF(origGet.call(this), precision); + }`, + }, + ], + } + ], + }, + ] + add_wrappers(wrappers); +})() diff --git a/common/wrappingS-H-C.js b/common/wrappingS-H-C.js index b81c5d3..f9814dc 100644 --- a/common/wrappingS-H-C.js +++ b/common/wrappingS-H-C.js @@ -35,6 +35,7 @@ // \copyright Copyright (c) 2020 The Brave Authors. /** \file + * \ingroup wrappers * This file contains wrappers for calls related to the Canvas API, about which you can read more at MDN: * * [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) * * [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) @@ -62,62 +63,19 @@ * Create private namespace */ (function() { - /** \fn fake create_post_wrappers - * \brief This function is used to prevent access to unwrapped APIs through iframes. - * - * \param The object to wrap like HTMLIFrameElement.prototype - */ - function create_post_wrappers(parent_object) { - return [{ - code_type: "object_properties", - parent_object: parent_object, - parent_object_property: "contentWindow", - wrapped_objects: [{ - original_name: "Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow')['get'];", - wrapped_name: "cw", - }], - wrapped_properties: [{ - property_name: "get", - property_value: ` - function() { - var parent=cw.call(this); - try { - parent.HTMLCanvasElement; - } - catch(d) { - return; // HTMLIFrameElement.contentWindow properties could not be accessed anyway - } - wrapping(parent); - return parent; - }`, - }], - }, - { - code_type: "object_properties", - parent_object: parent_object, - parent_object_property: "contentDocument", - wrapped_objects: [{ - original_name: "Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentDocument')['get'];", - wrapped_name: "cd", - }, ], - wrapped_properties: [{ - property_name: "get", - property_value: ` - function() { - var parent=cw.call(this); - try{ - parent.HTMLCanvasElement; - } - catch(d) { - return; // HTMLIFrameElement.contentDocument properties could not be accessed anywaya - } - wrapping(parent); - return cd.call(this); - }`, - }, ], - }, - ]; - } + + const DEF_CANVAS_COPY = ` + let canvasCopy = ctx => { + let {width, height} = ctx.canvas; + let fake = document.createElement("canvas"); + fake.setAttribute("width", width); + fake.setAttribute("height", height); + let stx = fake.getContext("2d"); + let imageData = window.CanvasRenderingContext2D.prototype.getImageData.call(ctx, 0, 0, width, height); + stx.putImageData(imageData, 0, 0); + return fake; + }; + `; /** @var String helping_code. * Selects if the canvas should be cleared (1) or a fake image should be created based on session @@ -148,13 +106,8 @@ wrapping_function_body: ` var ctx = this.getContext("2d"); if(ctx){ - var fake = document.createElement("canvas"); - fake.setAttribute("width", this.width); - fake.setAttribute("height", this.height); - var stx = fake.getContext("2d"); - var imageData = ctx.getImageData(0, 0, this.width, this.height); - stx.putImageData(imageData, 0, 0); - return origToDataURL.call(fake, ...args); + ${DEF_CANVAS_COPY} + return origToDataURL.call(canvasCopy(ctx), ...args); } else { var ctx = this.getContext("webgl2", {preserveDrawingBuffer: true}) || @@ -168,11 +121,10 @@ fake.setAttribute("height", this.height); var stx = fake.getContext("2d"); stx.drawImage(ctx.canvas, 0, 0); - return fake.toDataURL(); + return HTMLCanvasElement.prototype.toDataURL.call(fake); } } `, - post_wrapping_code: create_post_wrappers("HTMLIFrameElement.prototype"), }, { parent_object: "CanvasRenderingContext2D.prototype", @@ -181,39 +133,39 @@ original_name: "CanvasRenderingContext2D.prototype.getImageData", wrapped_name: "origGetImageData", }], - helping_code: helping_code + ` + helping_code: helping_code + strToUint + ` function lfsr_next(v) { return BigInt.asUintN(64, ((v >> 1n) | (((v << 62n) ^ (v << 61n)) & (~(~0n << 63n) << 62n)))); } var farble = function(context, fake) { if(approach === 1){ - fake.fillStyle = "white"; - fake.fillRect(0, 0, context.canvas.width, context.canvas.height); - return; - } - else if(approach === 0){ - const width = context.canvas.width; - const height = context.canvas.height; - var imageData = origGetImageData.call(context, 0, 0, width, height); - fake.putImageData(imageData, 0, 0); - var fakeData = origGetImageData.call(fake, 0, 0, width, height); - var pixel_count = BigInt(width * height); - var channel = domainHash[0].charCodeAt(0) % 3; - var canvas_key = domainHash; - var v = BigInt(sessionHash); + fake.fillStyle = "white"; + fake.fillRect(0, 0, context.canvas.width, context.canvas.height); + return; + } + else if(approach === 0){ + const width = context.canvas.width; + const height = context.canvas.height; + var imageData = origGetImageData.call(context, 0, 0, width, height); + fake.putImageData(imageData, 0, 0); + var fakeData = origGetImageData.call(fake, 0, 0, width, height); + var pixel_count = BigInt(width * height); + var channel = domainHash[0].charCodeAt(0) % 3; + var canvas_key = domainHash; + var v = BigInt(strToUint(domainHash,8)); - for (let i = 0; i < 32; i++) { - var bit = canvas_key[i]; - for (let j = 8; j >= 0; j--) { - var pixel_index = (4 * Number(v % pixel_count) + channel); - fakeData.data[pixel_index] = fakeData.data[pixel_index] ^ (bit & 0x1); - bit = bit >> 1; - v = lfsr_next(v); + for (let i = 0; i < 32; i++) { + var bit = canvas_key[i]; + for (let j = 8; j >= 0; j--) { + var pixel_index = (4 * Number(v % pixel_count) + channel); + fakeData.data[pixel_index] = fakeData.data[pixel_index] ^ (bit & 0x1); + bit = bit >> 1; + v = lfsr_next(v); + } } + fake.putImageData(fakeData, 0, 0); } - fake.putImageData(fakeData, 0, 0); - } - };`, + };`, wrapping_code_function_name: "wrapping", wrapping_code_function_params: "parent", wrapping_code_function_call_window: true, @@ -235,7 +187,6 @@ farble(this,stx); return origGetImageData.call(stx, sx, sy, sw, sh); `, - post_wrapping_code: create_post_wrappers("HTMLIFrameElement.prototype"), }, { parent_object: "HTMLCanvasElement.prototype", @@ -258,16 +209,9 @@ * CanvasRenderingContext2D.getImageData() that detemines the result. */ wrapping_function_body: ` - var ctx = this.getContext("2d"); - var fake = document.createElement("canvas"); - fake.setAttribute("width", this.width); - fake.setAttribute("height", this.height); - var stx = fake.getContext("2d"); - var imageData = ctx.getImageData(0,0,this.width,this.height); - stx.putImageData(imageData, 0, 0); - return origToBlob.call(fake, ...args); + ${DEF_CANVAS_COPY} + return origToBlob.call(canvasCopy(this.getContext("2d")), ...args); `, - post_wrapping_code: create_post_wrappers("HTMLIFrameElement.prototype"), }, { parent_object: "OffscreenCanvas.prototype", @@ -290,16 +234,9 @@ * CanvasRenderingContext2D.getImageData() that detemines the result. */ wrapping_function_body: ` - var ctx = this.getContext("2d"); - var fake = document.createElement("canvas"); - fake.setAttribute("width", this.width); - fake.setAttribute("height", this.height); - var stx = fake.getContext("2d"); - var imageData = ctx.getImageData(0,0,this.width,this.height); - stx.putImageData(imageData, 0, 0); - return origConvertToBlob.call(fake, ...args); + ${DEF_CANVAS_COPY} + return origConvertToBlob.call(canvasCopy(this.getContext("2d")), ...args); `, - post_wrapping_code: create_post_wrappers("HTMLIFrameElement.prototype"), }, { parent_object: "CanvasRenderingContext2D.prototype", @@ -315,7 +252,7 @@ return (ret && ((prng()*20) > 1)); } else if(approach === 1){ - return origIsPointInPath.call(ctx, ...args); + return false; } }; `, @@ -337,7 +274,6 @@ wrapping_function_body: ` return farbleIsPointInPath(this, ...args); `, - post_wrapping_code: create_post_wrappers("HTMLIFrameElement.prototype"), }, { parent_object: "CanvasRenderingContext2D.prototype", @@ -353,7 +289,7 @@ return (ret && ((prng()*20) > 1)); } else if(approach === 1){ - return origIsPointInStroke.call(ctx, ...args); + return false; } }; `, @@ -375,7 +311,6 @@ wrapping_function_body: ` return farbleIsPointInStroke(this, ...args); `, - post_wrapping_code: create_post_wrappers("HTMLIFrameElement.prototype"), }, ] add_wrappers(wrappers); diff --git a/common/wrappingS-HTML.js b/common/wrappingS-HTML.js index c91e993..af33272 100644 --- a/common/wrappingS-HTML.js +++ b/common/wrappingS-HTML.js @@ -50,7 +50,6 @@ parent_object_property: "name", wrapped_objects: [], helping_code: "window.name = '';", - nofreeze: true, }, ] add_wrappers(wrappers); diff --git a/common/wrappingS-MCS.js b/common/wrappingS-MCS.js index 2c04267..5778ab3 100644 --- a/common/wrappingS-MCS.js +++ b/common/wrappingS-MCS.js @@ -61,39 +61,32 @@ * * (0,1) - promise with modified device array * * (2) - empty promise */ - function farbleEnumerateDevices(){ - if(args[0] == 0 || args[0] == 1){ - return devices; - } - else if(args[0] == 2){ - return new Promise((resolve) => resolve([])); - } + function farbleEnumerateDevices() { + // to emulate correctly we need a fresh Promise and a fresh Array (with same values) on every call + return cachedDevices.then(r => { + r = r.concat(); + return r; + }); } /** - * \brief create and return MediaDeviceInfo object + * \brief create and return MediaDeviceInfo object by overlaying a native one with fake properties * * \param browserEnum enum specifying browser 0 - Chrome 1 - Firefox */ - function fakeDevice(browserEnum){ + function fakeDevice(device){ var kinds = ["videoinput", "audioinput", "audiooutput"]; + let browserEnum = device.groupId.length == 44 ? 1 : 0; var deviceId = browserEnum == 1 ? randomString(43, browserEnum)+ "=" : ""; - var ret = Object.create(MediaDeviceInfo.prototype); - Object.defineProperties(ret, { - deviceId:{ - value: deviceId - }, - groupId:{ - value: deviceRandomString(browserEnum) - }, - kind:{ - value: kinds[Math.floor(prng()*3)] - }, - label:{ - value: "" - } - }); - ret.__proto__.toJSON = JSON.stringify; - return ret; + let fakeData = { + deviceId, + groupId: deviceRandomString(browserEnum), + kind: kinds[Math.floor(prng() * 3)], + label: "", + }; + let json = JSON.stringify(fakeData); + fakeData.toJSON = () => json; + let overlay = WrapHelper.overlay(device, fakeData); + return overlay; } /** * \brief return random string for MediaDeviceInfo parameters @@ -119,28 +112,43 @@ parent_object_property: "enumerateDevices", wrapped_objects: [{ original_name: "MediaDevices.prototype.enumerateDevices", - wrapped_name: "origEnumerateDevices", + callable_name: "origEnumerateDevices", }], - helping_code: farbleEnumerateDevices+shuffleArray+deviceRandomString+randomString+fakeDevice+` - if(args[0]==0){ - var devices = origEnumerateDevices.call(navigator.mediaDevices); - devices.then(function(result) { - shuffleArray(result); - }); - } - if(args[0]==1){ - var until = Math.floor(prng()*4); - var devices = origEnumerateDevices.call(navigator.mediaDevices); - devices.then(function(result) { - var browserEnum = 0; - if(result[0].groupId.length == 44) - browserEnum = 1; - for(var i = 0; i < until; i++){ - result.push(fakeDevice(browserEnum)); + helping_code: farbleEnumerateDevices + shuffleArray + deviceRandomString + randomString + fakeDevice + ` + let [level] = args; + let cachedDevices = level < 2 ? + origEnumerateDevices.call(navigator.mediaDevices).then(result => { + try { + let shuffle = () => { + if (result.length > 1) shuffleArray(result); + console.log("Shuffled array", result); + return result; + }; + if (level === 1 && result.length) { + let additional = Math.floor(prng()*4); + console.debug("Random additional devices to add:", additional); + if (additional > 0) { + let adding = []; + while (additional-- > 0) { + adding.push(origEnumerateDevices.call(navigator.mediaDevices).then(([device]) => { + let fake = fakeDevice(device); + console.debug("Faking", fake); + result.push(fake); + })); + } + return Promise.all(adding).then(r => { + console.debug("Faked array", result); + return shuffle(); + }); + } + } + return shuffle(); + } catch (e) { + console.error("Error in farble promise callback", e); + throw e; } - shuffleArray(result); - }); - } + }) + : Promise.resolve([]); `, wrapping_function_args: "", /** \fn fake MediaDevices.prototype.enumerateDevices diff --git a/common/wrappingS-NP.js b/common/wrappingS-NP.js index 29babf2..d00df98 100644 --- a/common/wrappingS-NP.js +++ b/common/wrappingS-NP.js @@ -51,220 +51,222 @@ * Create private namespace */ (function() { - /** - * \brief create and return fake MimeType object - * - */ - function fakeMime(){ - var ret = Object.create(MimeType.prototype); - Object.defineProperties(ret, { - type:{ - value: "" - }, - suffixes:{ - value: "" - }, - description:{ - value: randomString(32, 0) - }, - enabledPlugin:{ - value: null - } - }); - return ret; - } - /** + /** + * \brief create and return fake MimeType object + * + */ + function fakeMime(){ + var ret = Object.create(MimeType.prototype); + Object.defineProperties(ret, { + type:{ + value: "" + }, + suffixes:{ + value: "" + }, + description:{ + value: randomString(32, 0) + }, + enabledPlugin:{ + value: null + } + }); + return ret; + } + /** * \brief create and return fake MimeType object created from given mime and plugin * * \param mime original MimeType object https://developer.mozilla.org/en-US/docs/Web/API/MimeType - * \param plugin original Plugin object https://developer.mozilla.org/en-US/docs/Web/API/Plugin + * \param plugin original Plugin object https://developer.mozilla.org/en-US/docs/Web/API/Plugin + */ + function farbleMime(mime, plugin){ + var ret = Object.create(MimeType.prototype); + Object.defineProperties(ret, { + type:{ + value: mime.type + }, + suffixes:{ + value: mime.suffixes + }, + description:{ + value: mime.description + }, + enabledPlugin:{ + value: plugin + } + }); + return ret; + } + /** + * \brief create and return fake Plugin object + * + * \param descLength enum specifying browser 0 - Chrome 1 - Firefox + * \param filenameLength enum specifying browser 0 - Chrome 1 - Firefox + * \param nameLength enum specifying browser 0 - Chrome 1 - Firefox */ - function farbleMime(mime, plugin){ - var ret = Object.create(MimeType.prototype); - Object.defineProperties(ret, { - type:{ - value: mime.type - }, - suffixes:{ - value: mime.suffixes - }, - description:{ - value: mime.description - }, - enabledPlugin:{ - value: plugin - } - }); - return ret; - } - /** - * \brief create and return fake Plugin object - * - * \param descLength enum specifying browser 0 - Chrome 1 - Firefox - * \param filenameLength enum specifying browser 0 - Chrome 1 - Firefox - * \param nameLength enum specifying browser 0 - Chrome 1 - Firefox - */ - function fakePlugin(descLength, filenameLength, nameLength){ - var ret = Object.create(Plugin.prototype); - var mime = fakeMime(); - Object.defineProperties(ret, { - 0:{ - value: mime - }, - "":{ - value: mime - }, - name:{ - value: randomString(nameLength, 0) - }, - filename:{ - value: randomString(filenameLength, 0) - }, - description:{ - value: randomString(descLength, 0) - }, - version:{ - value: null - }, - length:{ - value: 1 - } - }); - ret.__proto__.item = item; - ret.__proto__.namedItem = namedItem; - return ret; - } - /** + function fakePlugin(descLength, filenameLength, nameLength){ + var ret = Object.create(Plugin.prototype); + var mime = fakeMime(); + Object.defineProperties(ret, { + 0:{ + value: mime + }, + "":{ + value: mime + }, + name:{ + value: randomString(nameLength, 0) + }, + filename:{ + value: randomString(filenameLength, 0) + }, + description:{ + value: randomString(descLength, 0) + }, + version:{ + value: null + }, + length:{ + value: 1 + } + }); + ret.__proto__.item = item; + ret.__proto__.namedItem = namedItem; + return ret; + } + /** * \brief create and return fake PluginArray object containing given plugins * * \param plugins array of Plugin objects https://developer.mozilla.org/en-US/docs/Web/API/Plugin */ - function fakePluginArray(plugins){ - var ret = Object.create(PluginArray.prototype); - var count = 0; - for(var i = 0; i=0;j++){ - if((typeof plugins[i][j] != 'undefined') && (ret.namedItem(plugins[i][j].name)==null) && (plugins[i][j].type != "")){ - ret[counter] = plugins[i][j]; - ret[plugins[i][j].type] = plugins[i][j]; - counter++; - } - else{ - break; - } - } - } - Object.defineProperty(ret, 'length', { - value: counter - }); - return ret; - } - function item(arg){ - if(typeof arg != 'undefined' && Number.isInteger(Number(arg))) - return this[arg]; - else return null; - } + function fakePluginArray(plugins){ + var ret = Object.create(PluginArray.prototype); + var count = 0; + for(var i = 0; i=0;j++){ + if((typeof plugins[i][j] != 'undefined') && (ret.namedItem(plugins[i][j].name)==null) && (plugins[i][j].type != "")){ + ret[counter] = plugins[i][j]; + ret[plugins[i][j].type] = plugins[i][j]; + counter++; + } + else{ + break; + } + } + } + + Object.defineProperty(ret, 'length', { + value: counter + }); + return ret; + } + function item(arg){ + if(typeof arg != 'undefined' && Number.isInteger(Number(arg))) + return this[arg]; + else return null; + } - function namedItem(arg){ - if(typeof arg != 'undefined' && this[arg]) - return this[arg]; - else return null; - } - function refresh(){ - return undefined; - } - /** - * \brief create modified Plugin object from given plugin - * - * \param plugin original Plugin object https://developer.mozilla.org/en-US/docs/Web/API/Plugin - * - * Replaces words in name and description parameters in PDF plugins (default plugins in most browsers) - */ - function farblePlugin(plugin){ - var chrome = ["Chrome ", "Chromium ", "Web ", "Browser ", "OpenSource ", "Online ", "JavaScript ", ""]; - var pdf = ["PDF ", "Portable Document Format ", "portable-document-format ", "document ", "doc ", "PDF and PS ", "com.adobe.pdf "]; - var viewer = ["Viewer", "Renderer", "Display", "Plugin", "plug-in", "plug in", "extension", ""]; - var name = plugin.name; - var description = plugin.description; - if(plugin.name.includes("PDF")){ - name = chrome[Math.floor(prng() * (chrome.length))]+pdf[Math.floor(prng() * (pdf.length))]+viewer[Math.floor(prng() * (viewer.length))]; - description = pdf[Math.floor(prng() * (pdf.length))]; - } - var ret = Object.create(Plugin.prototype); - var counter = 0; - while(1){ - if(typeof plugin[counter] != 'undefined'){ - Object.defineProperties(ret, { - [counter]:{ - value: farbleMime(plugin[counter],ret) - }, - [plugin[counter].type]:{ - value: farbleMime(plugin[counter],ret) - } - }); - } - else { - break; - } - counter++; - } - Object.defineProperties(ret, { - name:{ - value: name - }, - filename:{ - value: randomString(32, 0), - }, - description:{ - value: description - }, - version:{ - value: null - }, - length:{ - value: 1 - } - }); - ret.__proto__.item = item; - ret.__proto__.namedItem = namedItem; - ret.__proto__.refresh = refresh; - return ret; - } - var methods = item + namedItem + refresh + shuffleArray + randomString; - var farbles = farblePlugin + farbleMime; - var fakes = fakeMime + fakePlugin + fakePluginArray + fakeMimeTypeArray; + function namedItem(arg){ + if(typeof arg != 'undefined' && this[arg]) + return this[arg]; + else return null; + } + function refresh(){ + return undefined; + } + /** + * \brief create modified Plugin object from given plugin + * + * \param plugin original Plugin object https://developer.mozilla.org/en-US/docs/Web/API/Plugin + * + * Replaces words in name and description parameters in PDF plugins (default plugins in most browsers) + */ + function farblePlugin(plugin){ + var name = plugin.name; + var description = plugin.description; + if(plugin.name.includes("PDF")){ + let chrome = ["Chrome ", "Chromium ", "Web ", "Browser ", "OpenSource ", "Online ", "JavaScript ", ""]; + let pdf = ["PDF ", "Portable Document Format ", "portable-document-format ", "document ", "doc ", "PDF and PS ", "com.adobe.pdf "]; + let viewer = ["Viewer", "Renderer", "Display", "Plugin", "plug-in", "plug in", "extension", ""]; + name = chrome[Math.floor(prng() * (chrome.length))]+pdf[Math.floor(prng() * (pdf.length))]+viewer[Math.floor(prng() * (viewer.length))]; + description = pdf[Math.floor(prng() * (pdf.length))]; + } + var ret = Object.create(Plugin.prototype); + var counter = 0; + while(1){ + if(typeof plugin[counter] != 'undefined'){ + Object.defineProperties(ret, { + [counter]:{ + value: farbleMime(plugin[counter],ret) + }, + [plugin[counter].type]:{ + value: farbleMime(plugin[counter],ret) + } + }); + } + else { + break; + } + counter++; + } + Object.defineProperties(ret, { + name:{ + value: name + }, + filename:{ + value: randomString(32, 0), + }, + description:{ + value: description + }, + version:{ + value: null + }, + length:{ + value: 1 + } + }); + ret.__proto__.item = item; + ret.__proto__.namedItem = namedItem; + ret.__proto__.refresh = refresh; + return ret; + } + var methods = item + namedItem + refresh + shuffleArray + randomString; + var farbles = farblePlugin + farbleMime; + var fakes = fakeMime + fakePlugin + fakePluginArray + fakeMimeTypeArrayF; var wrappers = [ - { + { parent_object: "navigator", parent_object_property: "plugins", + apply_if: "navigator.plugins.length > 0", wrapped_objects: [], helping_code: methods + farbles + fakes +` @@ -282,7 +284,7 @@ shuffleArray(buffer); } var fakePluginArray = fakePluginArray(buffer); - var fakeMimeTypeArray = fakeMimeTypeArray(fakePluginArray); + var fakeMimeTypeArray = fakeMimeTypeArrayF(fakePluginArray); `, post_wrapping_code: [ { @@ -307,12 +309,12 @@ }, ], }, - { + { code_type: "object_properties", parent_object: "navigator", parent_object_property: "mimeTypes", wrapped_objects: [], - /** \brief replaces navigator.plugins getter + /** \brief replaces navigator.mimeTypes getter * * Depending on level chosen this property returns: * * (0) - modified MimeTypeArray with links to updated Plugins @@ -329,7 +331,7 @@ ], } ], - }, + }, ]; add_wrappers(wrappers); })(); diff --git a/common/wrappingS-VR.js b/common/wrappingS-VR.js new file mode 100644 index 0000000..e85eb34 --- /dev/null +++ b/common/wrappingS-VR.js @@ -0,0 +1,118 @@ +/** \file + * \brief This file contains wrappers for the original Virtual Reality API + * + * \see https://immersive-web.github.io/webvr/spec/1.1/ + * + * \author Copyright (C) 2021 Libor Polcak + * + * \license SPDX-License-Identifier: GPL-3.0-or-later + */ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +/** \file + * \ingroup wrappers + * + * navigator.activeVRDisplays() allows any page script to learn the VR + * displays attached to the computer. + * + * U. Iqbal, S. Englehardt and Z. Shafiq, "Fingerprinting the + * Fingerprinters: Learning to Detect Browser Fingerprinting Behaviors," + * in 2021 2021 IEEE Symposium on Security and Privacy (SP), San Francisco, + * CA, US, 2021 pp. 283-301 observed + * (https://github.com/uiowa-irl/FP-Inspector/blob/master/Data/potential_fingerprinting_APIs.md) + * that the interface is used in the wild to fingerprint users. As it is + * likely that only a minority of users have a VR display connected and the API + * provides additional information on the HW, it is likely that users with + * a VR display connected are easily fingerprintable. + * + * As we expect that the majority of the users does not have a VR display + * connected, we provide only a single mitigation - the wrapped APIs returns + * an empty list. + * + * \bug The standard provides events *vrdisplayconnect*, *vrdisplaydisconnect* + * *vrdisplayactivate* and *vrdisplaydeactivate* that fires at least on the + * window object. We do not mitigate the event to fire and consequently, it is + * possible that an adversary can learn that a VR display was (dis)connected but + * there was no change in the result of the navigator.activeVRDisplays() API. + * + * The VRFrameData object carries a timestamp. As we allow wrapping of several + * ways to obtain timestamps, we need to provide the same precision for the + * VRFrameData object. + */ + +(function() { + var remember_past_ts_values = `var precision = args[0]; + var doNoise = args[1]; + var pastValues = {}; + ${rounding_function} + ${noise_function} + var mitigationF = rounding_function; + if (doNoise === true){ + mitigationF = function(value, precision) { + let params = [value, precision]; + if (params in pastValues) { + return pastValues[params]; + } + let result = noise_function(...params); + pastValues[params] = result; + return result; + } + } + `; + var wrappers = [ + { + parent_object: "navigator", + parent_object_property: "activeVRDisplays", + wrapped_objects: [], + helping_code: "", + wrapping_function_body: ` + return Promise.resolve(new window.Array()); + `, + }, + { + /** + * \see https://developer.mozilla.org/en-US/docs/Web/API/VRFrameData/timestamp + * \note that at the time of the writing of the prototype, we did not have + * access to a VR display and this code was not tested. + */ + parent_object: "VRFrameData.prototype", + parent_object_property: "timestamp", + wrapped_objects: [], + helping_code: remember_past_ts_values + `let origGet = Object.getOwnPropertyDescriptor(VRFrameData.prototype, "timestamp").get`, + post_wrapping_code: [ + { + code_type: "object_properties", + parent_object: "VRFrameData.prototype", + parent_object_property: "timestamp", + wrapped_objects: [], + /** \brief replaces VRFrameData.timestamp getter to create + * a timestamp with the desired precision. + */ + wrapped_properties: [ + { + property_name: "get", + property_value: ` + function() { + return mitigationF(origGet.call(this), precision); + }`, + }, + ], + } + ], + }, + ] + add_wrappers(wrappers); +})() diff --git a/common/wrappingS-WEBA.js b/common/wrappingS-WEBA.js index f2f2e7d..2ca7940 100644 --- a/common/wrappingS-WEBA.js +++ b/common/wrappingS-WEBA.js @@ -56,19 +56,6 @@ */ (function() { /** - * \brief makes number from substring of given string - should work as reinterpret_cast - * \param str String - * \param length Number specifying substring length - */ - function strToUint(str, length){ - var sub = str.substring(0,length); - var ret = ""; - for (var i = sub.length-1; i >= 0; i--) { - ret += ((sub[i].charCodeAt(0)).toString(2).padStart(8, "0")); - } - return "0b"+ret; - }; - /** * \brief shifts number bits to pick new number * \param v number to shift */ diff --git a/common/wrappingS-WEBGL.js b/common/wrappingS-WEBGL.js index 6546a49..abce544 100644 --- a/common/wrappingS-WEBGL.js +++ b/common/wrappingS-WEBGL.js @@ -65,16 +65,17 @@ */ (function() { /** - * \brief subtract one from given number with ~50% probability and return it + * \brief subtract one from given number with ~50% probability relative to given enum and return it * * \param number original Number value to edit + * \param pname enum of argument given to getParameter * */ function farbleGLint(number, pname) { var ret = 0; if(number > 0){ - var temp = (Number("0x"+domainHash.slice(26,domainHash.length))^pname)%3; - ret = number - (temp >= 1 ? 1:0); + var temp = (Number("0x"+domainHash.slice(29,domainHash.length-28))^pname)%3; + ret = number - (temp == 1 ? 1:0); } return ret; } @@ -167,7 +168,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns null or output of given function * * \param name of original function * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) @@ -186,7 +187,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns 0 or output of given function * * \param name of original function * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) @@ -205,7 +206,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns -1 or output of given function * * \param name of original function * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) @@ -224,7 +225,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns [] or output of given function * * \param name of original function * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) @@ -236,14 +237,14 @@ */ function farbleNullArray(name, ctx, ...fcarg) { if(args[0]===1) { - return []; + return new window.Array; } else if(args[0]===0) { return name.call(ctx, ...fcarg); } }; /** - * \brief Modifies return value + * \brief Returns empty WebGLShaderPrecisionFormat object or real value * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param ...fcarg delegated arguments depending on function @@ -273,7 +274,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns empty WebGLActiveInfo object or real value * * \param name of original function * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) @@ -304,7 +305,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns modified WebGLRenderingContext.getFramebufferAttachmentParameter output for some specific parameters, original value for the rest * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param target GLenum (https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants) @@ -347,7 +348,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns modified WebGLRenderingContext.getVertexAttrib output for some specific parameters, original value for the rest * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param index GLuint specifying index @@ -385,7 +386,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns modified WebGLRenderingContext.getBufferParameter output for some specific parameters, original value for the rest * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param target GLenum (https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants) @@ -413,7 +414,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns modified WebGLRenderingContext.getShaderParameter output for some specific parameters, original value for the rest * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param program WebGLShader object (https://developer.mozilla.org/en-US/docs/Web/API/WebGLShader) @@ -442,7 +443,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns modified WebGLRenderingContext.getRenderbufferParameter output for some specific parameters, original value for the rest * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param target GLenum (https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants) @@ -478,7 +479,7 @@ } }; /** - * \brief Modifies return value + * \brief Returns modified WebGLRenderingContext.getProgramParameter output for some specific parameters, original value for the rest * * \param ctx WebGLRenderingContext (https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext) * \param program WebGLProgram object (https://developer.mozilla.org/en-US/docs/Web/API/WebGLProgram) @@ -540,7 +541,7 @@ var pixel_count = BigInt(width * height); var channel = domainHash[0].charCodeAt(0) % 3; var canvas_key = domainHash; - var v = BigInt(sessionHash); + var v = BigInt(strToUint(domainHash,8)); for (let i = 0; i < 32; i++) { var bit = canvas_key[i]; for (let j = 8; j >= 0; j--) { @@ -936,7 +937,7 @@ wrapped_name: "origReadPixels", } ], - helping_code: lfsr_next + farblePixels, + helping_code: lfsr_next + strToUint + farblePixels, original_function: "parent.WebGLRenderingContext.prototype.readPixels", wrapping_function_args: "x, y, width, height, format, type, pixels, offset", /** \fn fake WebGLRenderingContext.prototype.readPixels @@ -959,7 +960,7 @@ wrapped_name: "origReadPixels", } ], - helping_code: lfsr_next + farblePixels, + helping_code: lfsr_next + strToUint + farblePixels, original_function: "parent.WebGL2RenderingContext.prototype.readPixels", wrapping_function_args: "x, y, width, height, format, type, pixels, offset", /** \fn fake WebGL2RenderingContext.prototype.readPixels diff --git a/common/wrappingS-XR.js b/common/wrappingS-XR.js new file mode 100644 index 0000000..fb245b6 --- /dev/null +++ b/common/wrappingS-XR.js @@ -0,0 +1,64 @@ +/** \file + * \brief This file contains wrappers for the current Virtual Reality API (WebXR) + * + * \see https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API + * \see https://immersive-web.github.io/webxr/ + * + * \author Copyright (C) 2021 Libor Polcak + * + * \license SPDX-License-Identifier: GPL-3.0-or-later + */ +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +/** \file + * \ingroup wrappers + * + * navigator.xr allows any page script to learn the VR displays attached + * to the computer and more. + * + * U. Iqbal, S. Englehardt and Z. Shafiq, "Fingerprinting the + * Fingerprinters: Learning to Detect Browser Fingerprinting Behaviors," + * in 2021 2021 IEEE Symposium on Security and Privacy (SP), San Francisco, + * CA, US, 2021 pp. 283-301 observed + * (https://github.com/uiowa-irl/FP-Inspector/blob/master/Data/potential_fingerprinting_APIs.md) + * that the orginal WebVR API is used in the wild to fingerprint users. As it is + * likely that only a minority of users have a VR display connected and the API + * provides additional information on the HW, it is likely that users with + * a VR display connected are easily fingerprintable. + * + * As all the API calls are accessible through the navigator.xr API, we provide + * a single mitigation. We disable the API completely. This might need to be + * revised once this API is commonly enabled in browsers. + */ + +(function() { + var wrappers = [ + { + parent_object: "navigator", + parent_object_property: "xr", + wrapped_objects: [], + helping_code: "", + post_wrapping_code: [ + { + code_type: "delete_properties", + parent_object: "navigator", + delete_properties: ["xr"], + } + ], + }, + ] + add_wrappers(wrappers); +})() diff --git a/docs/blog.md b/docs/blog.md new file mode 100644 index 0000000..984436a --- /dev/null +++ b/docs/blog.md @@ -0,0 +1,6 @@ +# The content of the blog + +1. [We received support from NGI0 PET Fund](blogarticles/support.md) +2. [Measurement of JavaScript API usage on the web](blogarticles/crawling.md) +3. [How JavaScript restrictor prevents other parties from sniffing on your local applications?](blogarticles/localportscanning.md) +4. [Farbling-based wrappers to hinder browser fingerprinting](blogarticles/farbling.md) diff --git a/docs/blogarticles/farbling.md b/docs/blogarticles/farbling.md new file mode 100644 index 0000000..a094244 --- /dev/null +++ b/docs/blogarticles/farbling.md @@ -0,0 +1,92 @@ +--- +title: Farbling-based wrappers to hinder browser fingerprinting +--- + +[Browser fingerprinting](https://arxiv.org/pdf/1905.01051.pdf) is a more and more popular technique used to identify browsers. The fingerprint is computed based on the results of JavaScript calls, the content of HTTP headers, hardware characteristics, underlying operating system and other software information. Consequently, browser fingerprints are used for cross-domain tracking. However, users cannot clear their browser fingerprint as it is not stored on the client-side. It is also challenging to determine whether a browser is being fingerprinted. + +Another issue that hinders fingerprinting protection is the ever-changing variety of supported APIs. Browsers implement new APIs over time, and existing APIs change. Consequently, it is necessary to continuously monitor the APIs being used for fingerprinting purposes to block fingerprinting attempts. + +Due to fingerprinting scripts being [more prevalent](https://www.cs.princeton.edu/~arvindn/publications/OpenWPM_1_million_site_tracking_measurement.pdf), various web browsers - for example, Tor, Brave, and Firefox - started implementing fingerprinting protection to protect users and their privacy. + +## Brave fingerprinting protection + +Why is Brave's Farbling special? Until recently, [Tor browser](https://2019.www.torproject.org/projects/torbrowser/design/#fingerprinting-linkability) had the most robust defence against fingerprinting. It (1) implemented modifications in various APIs, (2) blocks some other APIs, (3) runs in a window of predefined size, etc. to ensure all users have the same fingerprint. This approach is very effective at producing uniform fingerprint for all users, which makes it difficult for fingerprinters to differentiate between browsers. Still, such fingerprint is also brittle -- minor changes like resizing the window could cause the browser to have a unique fingerprint. Hence, users need to follow inconvenient steps to keep their fingerprint uniform. + +With all this in [mind](https://brave.com/brave-fingerprinting-and-privacy-budgets/), Brave software decided to improve their fingerprinting protection. They [proposed](https://brave.com/privacy-updates-3/) new fingerprinting protection, Farbling, arguing that it is (almost) impossible to produce uniform fingerprint without compromising user experience. Their countermeasures involve randomising values based on previous research papers [PriVaricator](https://www.doc.ic.ac.uk/~livshits/papers/pdf/www15.pdf) and [FPRandom](https://hal.inria.fr/hal-01527580/document) Both papers have shown promising results, and Brave has perfected this approach, creating effective defence while retaining almost full user experience. Farbling is a comprehensive collection of modifications that aim at producing a unique fingerprint on every domain and in every session. + +### How does farbling work? + +Farbling uses generated session and [eTLD+1](https://web.dev/same-site-same-origin/) keys to deterministically change outputs of certain APIs commonly used for browser fingerprinting. These white lies result in different websites calculating different fingerprints. Moreover, a previously visited website calculates a different fingerprint in a new browsing session. + +Farbling implementation is publicly available on Github [issue](https://github.com/brave/brave-browser/issues/8787) with discussions on design decisions, future plans and possible changes in a separate [issue](https://github.com/brave/brave-browser/issues/11770). + +Farbling operates on three levels: + 1. **Off** - countermeasures are not active + 2. **Balanced** - various APIs have modified values based on domain/session keys + 3. **Maximum** - various APIs values replaced by randomised values based on domain/session keys + +Now, what changes did actually Brave implement to specific APIs? + +### Canvas + +Canvas modifications are tracked in a separate [issue](https://github.com/brave/brave-browser/issues/9186). +Both *balanced* and *maximum* approach modify API calls `CanvasRendering2dContext.getImageData`, +`HTMLCanvasElement.toDataURL`, +`HTMLCanvasElement.toBlob`, and +`OffscreenCanvas.convertToBlob`. A [Filter function](https://github.com/brave/brave-core/blob/master/chromium_src/third_party/blink/renderer/core/execution_context/execution_context.cc) changes values of certain pixels chosen based on session/domain keys, resulting in a unique canvas fingerprint. +On *maximum* level, methods `CanvasRenderingContext2D.isPointInPath` and `CanvasRenderingContext2D.isPointInStroke` always return *false*. + +### WebGL + +Modifications for both WebGL and WebGL2 are described in issues [webgl](https://github.com/brave/brave-browser/issues/9188) , [webgl2](https://github.com/brave/brave-browser/issues/9189). +On *balanced* level +`WebGLRenderingContext.getParameter` and other methods return slightly modified values. +`WebGLRenderingContext.readPixels` is modified similarly to canvas methods. +On *maximum* level, `WebGLRenderingContext.getParameter` returns random strings for unmasked vendor and renderer, bottom values for other arguments. Other modified calls return bottom values. All modifications can be found in the issues mentioned above or directly in the [code](https://github.com/brave/brave-core/tree/master/chromium_src/third_party/blink/renderer/modules/webgl). + +### Web Audio + +The [issue](https://github.com/brave/brave-browser/issues/9187) modifies +several endpoints of `AnalyserNode` and `AudioBuffer` APIs used for audio data handling are modified. On the *balanced* level, the amplitude of returned audio data is slightly changed based on the domain key. However, data are replaced by white noise generated from domain hash on the maximum level, so there is no relation with original data. + +### Plugins + +Currently, +`navigator.plugins` and `navigator.mimeTypes` are modified on *balanced* level to return an array with altered plugins and two fake plugins. On *maximum* level, the returned array contains only two fake plugins. +See [issue1](https://github.com/brave/brave-browser/issues/9435) and [issue2](https://github.com/brave/brave-browser/issues/10597) for more details. + +### User agent + +Brave employs the default Chrome UA and the newest OS version as the user agent string. Also, a random number of blank spaces (up to 5) appended to the end of the user agent string. +For more details, see the [GitHub issue](https://github.com/brave/brave-browser/issues/9190). + +### EnumerateDevices + +This API is used to list I/O media devices like microphone or speakers. When fingerprinting protection is active, Brave returns a shuffled list of devices. For more details, see +[issue1](https://github.com/brave/brave-browser/issues/11271) and +[issue2](https://github.com/brave/brave-browser/issues/8666). + +### HardwareConcurrency + +The number of logical processors returned by this interface is modified as follows -- on *balanced* level, a valid value between 2 and the true value, on *maximum* level, a valid value between 2 and 8. +See the [GitHub issue](https://github.com/brave/brave-browser/issues/10808) for more details. + +# Porting Farbling to JSR + +Our goal was to extend JSR anti-fingerprinting protections with similar measures to those available in Brave's Farbling. +We decided to implement Brave Farbling with minor tweaks. As Brave is an open-source project based on [Chromium](https://www.chromium.org/Home), core changes are available in the public [repository](https://github.com/brave/brave-core). Furthermore, as Brave is licensed under [MPL 2.0](https://www.mozilla.org/en-US/MPL/2.0/) license, its countermeasures can be ported to JSR. +Similarly to Brave, JSR utilises session and domain hashes (currently, we use a different domain hash based on origin, however, we consider switching to the eTLD+1 approach used by Brave). Nevertheless, we ported only those changes that an extension can reasonably apply. So we do not plan to change system fonts as the true set of fonts can leak in several ways (e.g., CSS, canvas). We will keep a close eye on anti-fingerprining techniquest applied by Brave in the future. + +Former JSR defences were left as an option so user can choose which protection they want. For example, for **Canvas API**, JSR retains the old defence that returns a white image, but it is also possible to use Farbling and slightly modify the image. + +`CanvasRenderingContext2D.isPointInPath` and `CanvasRenderingContext2D.isPointInStroke` are modified to return *false* with 5% probability, returning *false* to every call seems to be easily identifiable and it limits the usablity of the calls. + +**WebGL**, **Web audio**, **plugins**, **hardwareConcurrency** and **deviceMemory** have been changed accordingly to Brave. API **enumerateDevices** has the same functionality as in Brave. In addition, we add fake devices to the list. **User agent** wasn't modified because it can cause compatibility issues as we support multiple browsers. Adding empty spaces at the end of UAS seems to be quite a weak countermeasure. We will continue to watch changes in the user agent and may implement some defence in future, although it looks like a [better solution](https://datatracker.ietf.org/doc/html/rfc8942) is on the way. + +JSR 0.5 changes the default level -- **level 2** to apply the farbling-based defence for all covered APIs, and it will be very similar to the *balanced* level of *Brave*. **Level 3** is redesigned to partly apply new and partly old countermeasures to provide as little information as possible. Please report websites that does not work correctly with Farbling. + +During the examination of the ported code, we [identified and reported](https://github.com/brave/brave-browser/issues/15882) an issue in the original Brave implementation. The issue was acknowledged and fixed by Brave. This is the beauty of the free software: several projects can benefit from the same code-base and mutualy improve the quality. + +# Conclusion + +Farbling-based wrappers produce very similar outputs to Brave. So with JSR, Farbling-like capabilities are available in multiple browsers. Nevertheless, keep in mind that the best anti-fingerprinting techniques are still a research question, fingerprinting techniques are deployed for security reasons (and farbling-like anti-fingerprinting masking may complicate some log in processes), so it is not completely clear what defences are the best and the choice of the defences also depends on specific use cases. We will investigate fingerprinting scripts further during the future work on this project. diff --git a/docs/blogarticles/localportscanning.md b/docs/blogarticles/localportscanning.md index 719ef73..cd1312e 100644 --- a/docs/blogarticles/localportscanning.md +++ b/docs/blogarticles/localportscanning.md @@ -65,10 +65,11 @@ local applications. So why does a browser leak the information? Well, the browser employs so called [same origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) (SOP) that in abstract theory should prevent websites from the scans in question. As your local computer is of a different origin from the remote website, your computer should be protected by SOP. Nevertheless, SOP has its limitations. First of all, some [cross-origin resource sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) is beneficial, so the browser cannot block outgoing requests to other origins. Such behaviour opens possibilities for [side-channels](https://www.forcepoint.com/sites/default/files/resources/files/report-attacking-internal-network-en_0.pdf) to be identified. So even though the web page cannot communicate with applications on your computer (or in your network) without the cooperation of these applications, it can observe the behaviour and make some conclusions based on the observed errors, timing, etc. -An (ad) blocker can prevent you from the activity. As the blockers typically leverage blocklists, +An (ad) blocker can protect you from the activity. As the blockers typically leverage blocklists, such a port scanning script URL needs to match a rule in a block list. Once information about a -misbehaving script becomes public, a rule can be added to a block list. However, this could take some time. Additional techniques like [DNS de-cloaking](https://blog.lukaszolejnik.com/large-scale-analysis-of-dns-based-tracking-evasion-broad-data-leaks-included/) -need to be applied in this case. +misbehaving script becomes public, a rule can be added to a block list. However, blockers do not +protect in the time span between the deployment and rule update at your computer. Moreover, additional techniques like [DNS de-cloaking](https://blog.lukaszolejnik.com/large-scale-analysis-of-dns-based-tracking-evasion-broad-data-leaks-included/) +need to be applied in this case. Not all blockers (can) use DNS de-cloacking. ## Network Boundary Shield to the rescue diff --git a/docs/build.md b/docs/build.md index 81c43f2..7e33a71 100644 --- a/docs/build.md +++ b/docs/build.md @@ -3,19 +3,18 @@ Title: Building from scratch ### GNU/Linux and Mac OS 1. Go to the project repository: [https://github.com/polcak/jsrestrictor](https://github.com/polcak/jsrestrictor). -1. Download the desired branch, e.g. as zip archive. -1. Unpack the zip archive. -1. Run `git submodule update` -1. Run `make`. +2. Download the desired branch, e.g. as zip archive. +3. Unpack the zip archive. +4. Run `make`. * You will need common software, such as `zip`, `wget`, `bash`, `awk`, `sed`. -1. Import the extension to the browser. +5. Import the extension to the browser. * Firefox: [https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/](https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/) * Use the file `firefox_JSR.zip` created by `make`. * Chromium-based browsers: 1. Open `chrome://extensions`. - 1. Enable developper mode. - 1. Click `Load unpacked`. - 1. Import the `chrome_JSR/` directory created by `make`. + 2. Enable developper mode. + 3. Click `Load unpacked`. + 4. Import the `chrome_JSR/` directory created by `make`. ### Windows @@ -23,8 +22,7 @@ Title: Building from scratch 2. Go to the project repository: [https://github.com/polcak/jsrestrictor](https://github.com/polcak/jsrestrictor). 3. Download the desired branch, e.g. as zip archive. 4. Unpack the zip archive. -5. Run `git submodule update` -6. Open the JSR project folder in WSL, run `make`. +5. Open the JSR project folder in WSL, run `make`. * Make sure that `zip` and all other necessary tools are installed. * Note that EOL in `fix_manifest.sh` must be set to `LF` (you can use the tool `dos2unix` in WSL to convert `CR LF` to `LF`). -7. On Windows, import the extension to the browser according to the instructions for Linux (above). +6. On Windows, import the extension to the browser according to the instructions for Linux (above). diff --git a/docs/credits.md b/docs/credits.md index b898b38..430e8a6 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -14,14 +14,18 @@ Title: Credits **Pavel Pohner** developed the Network Boundary Scanner as a part of his master's thesis. -**Pater Horňák** ported functionality from [Chrome Zero](https://github.com/IAIK/ChromeZero) as a part of his bachelor thesis. He also provided several small fixes to the code base. +Matúš Švancár ported Farbling anti-fingerprinting measures from the Brave browser as a part of his [master's thesis](https://www.fit.vut.cz/study/thesis/23310/). -### Key ideas +We thank all other minor contributors of the project that are not listed in this section. + +# Key ideas The development of this extension is influenced by the paper [JavaScript Zero: Real JavaScript and Zero Side-Channel Attacks](https://graz.pure.elsevier.com/de/publications/javascript-zero-real-javascript-and-zero-side-channel-attacks). It appeared during the work of Zbyněk Červinka and provided basically the same approach to restrict APIs as was at the time developed by Zbyněk Červinka. The [Force Point report](https://www.forcepoint.com/sites/default/files/resources/files/report-attacking-internal-network-en_0.pdf) was a key inspiration for the development of the Network Boundary Shield. +Some of the fingerprinting counter-measures are inspired by [Farbling of the Brave browser](blogarticles/farbling.md). + ### Borrowed code We borrowed code from other free software project: diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..b261dd1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,77 @@ +> **Disclaimer**: This is a research project under development, see the [issue page](https://github.com/polcak/jsrestrictor/issues) and the [webextension home page](https://polcak.github.io/jsrestrictor/) for more details about the current status. + +A JS-enabled web page can access any of the APIs that a web browser provides. The user has only a limited control and some APIs cannot be restricted by the user easily. JavaScript Restrictor aims to improve the user control of the web browser. Similarly to a firewall that controls the network traffic, JavaScript Restrictor controls the APIs provided by the browser. The goal is to improve the privacy and security of the user running the extension. + +## Installation + +JavaScript Restrictor (JSR) is a browser extension with support for multiple browsers: [Firefox](https://addons.mozilla.org/cs/firefox/addon/javascript-restrictor/), [Google Chrome](https://chrome.google.com/webstore/detail/javascript-restrictor/ammoloihpcbognfddfjcljgembpibcmb), and [Opera](https://addons.opera.com/en/extensions/details/javascript-restrictor/). The extension also works with Brave, Microsoft Edge, and most likely any Chromium-based browser. [Let us know](https://github.com/polcak/jsrestrictor/issues) if you want to add the extension to additional store. + +## Goals + +Various websites collect information about users without their awareness. The collected information is used to track users. Malicious websites can fingerprint user browsers or computers. JavaScript Restrictor protects the user by restricting or modifying several web browser APIs used to create side-channels and identify the user, the browser or the computer. JavaScript Restrictor can block access to JavaScript objects, functions and properties or provide a less precise implementation of their functionality, for example, by modifying or spoofing values returned by the JS calls. The goal is to mislead websites by providing false data or no data at all. + +Another goal of the extension is not to break the visited websites. As the deployment of JavaScript only websites rise, it is necessary to fine-tune the API available to the websites to prevent unsolicited tracking and protect against data thefts. + +### Protected APIs + +JavaScript Restrictor currently supports modifying and restricting the following APIs (for more details visit [levels of protection page](https://polcak.github.io/jsrestrictor/levels.html)): + +* **Network boundary shield** (NBS) prevents web pages to use the browser as a proxy between local network and the public Internet. See the [Force Point report](https://www.forcepoint.com/sites/default/files/resources/files/report-attacking-internal-network-en_0.pdf) for an example of the attack. The protection encapsulates the WebRequest API, so it captures all outgoing requests including all elements created by JavaScript. +* **window.Date object**, **window.performance.now()**, **window.PerformanceEntry**, **Event.prototype.timeStamp**, **Gamepad.prototype.timestamp**, and **VRFrameData.prototype.timestamp** provide high-resolution timestamps that can be used to [idenfity the user](http://www.jucs.org/jucs_21_9/clock_skew_based_computer) or can be used for microarchitectural attacks and [timing attacks](https://lirias.kuleuven.be/retrieve/389086). +* **HTMLCanvasElement**: Functions canvas.toDataURL(), canvas.toBlob(), CanvasRenderingContext2D.getImageData, OffscreenCanvas.convertToBlob() return either - modified image data based on session and domain keys, making canvas fingerprint unique, or white image. Canvas element provides access to HW acceleration which may reveal the card and consequently be used as a fingerprinting source. +* **AudioBuffer and AnalyserNode**: These API can be used to create fingerprint by analysing audio signal. JSR modifies AudioBuffer.getChannelData(), AudioBuffer.copyFromChannel(), AnalyserNode.getByteTimeDomainData(), AnalyserNode.getFloatTimeDomainData(), AnalyserNode.getByteFrequencyData() and AnalyserNode.getFloatFrequencyData() to alter audio data based on domain key, or return white noise based on domain key, making audio fingerprint unique. +* **WebGLRenderingContext**: WebGL parameters and functions can expose hardware and software uniqueness. JSR modifies function WebGLRenderingContext.getParameter() to return bottom values (null, 0, empty string, etc) or alter return values for certain arguments. WebGLRenderingContext.getActiveAttrib, WebGLRenderingContext.getActiveUniform, +WebGLRenderingContext.getAttribLocation, WebGLRenderingContext.getBufferParameter, WebGLRenderingContext.getFramebufferAttachmentParameter, +WebGLRenderingContext.getProgramParameter, WebGLRenderingContext.getRenderbufferParameter, WebGLRenderingContext.getShaderParameter, +WebGLRenderingContext.getShaderPrecisionFormat, WebGLRenderingContext.getTexParameter, WebGLRenderingContext.getUniformLocation, +WebGLRenderingContext.getVertexAttribOffset, WebGLRenderingContext.getSupportedExtensions, WebGLRenderingContext.getExtension are modified to return bottom values. WebGLRenderingContext.readPixels() is modified to return either empty image or modified image data based on session and domain keys. +* **MediaDevices.prototype.enumerateDevices** provides a unique strings identifying cameras and + microphones. This strings can be used to fingerprint the user (user session). +* **navigator.deviceMemory** or **navigator.hardwareConcurrency** can reveal hardware specification of the device. +* **XMLHttpRequest (XHR)** performs requests to the server after the page is displayed and gathered information available through other APIs. Such information might carry identification data or results of other attacks. +* **ArrayBuffer** can be exploited for microarchitectural attacks. + * Encapsulates window.DataView, window.Uint8Array, window.Int8Array, window.Uint8ClampedArray, window.Int16Array, window.Uint16Array, window.Int32Array, window.Uint32Array, window.Float32Array, window.Float64Array +* **SharedArrayBuffer (window.SharedArrayBuffer)** can be exploited for [timing attacks](https://graz.pure.elsevier.com/de/publications/fantastic-timers-and-where-to-find-them-high-resolution-microarch). +* **WebWorker (window.Worker)** can be exploited for [timing attacks](https://graz.pure.elsevier.com/de/publications/practical-keystroke-timing-attacks-in-sandboxed-javascript). +* **[Geolocation API](https://www.w3.org/TR/geolocation-API/) (navigator.geolocation)**: Although + the browser should request permission to access to the Geolocation API, the user can be unwilling + to share the exact position. JSR allows the user to limit the precission of the API or disable the + API. JSR also modifies the timestamps provided by Geolocation API in consistency with its time + precision settings. +* **[Gamepad API](https://w3c.github.io/gamepad/) (navigator.getGamepads())**: fingerprinters were + [observed](https://github.com/uiowa-irl/FP-Inspector/blob/master/Data/potential_fingerprinting_APIs.md) to fingerprint users based on the gamepads connected to the computer. As we expect that the majority of the users does not have a gamepad connected, we provide only a single mitigation - the wrapped APIs returns an empty list. +* **[Virtual Reality API](https://immersive-web.github.io/webvr/spec/1.1/)** and **[Mixed Reality](https://immersive-web.github.io/webxr/)** APIs allow web pages to access information on connected VR sets and similar devices. Fingerprinters were already [observed to use the attach devices to identify the browser](https://github.com/uiowa-irl/FP-Inspector/blob/master/Data/potential_fingerprinting_APIs.md). As we expect that the majority of the users does not have VR sets connected, we provide only a single mitigation - the wrapped APIs returns an empty list and we disable Web XR completely. +* **window.name** provides a very simple cross-origin tracking method of the same tab, see https://github.com/polcak/jsrestrictor/issues/72, https://developer.mozilla.org/en-US/docs/Web/API/Window/name, https://2019.www.torproject.org/projects/torbrowser/design/, https://bugzilla.mozilla.org/show_bug.cgi?id=444222, and https://html.spec.whatwg.org/#history-traversal. JSR provides an option to remove any `window.name` content on each page load. +* **navigator.sendBeacon** is an API desinged for analytics. JSR provides an option to disable the API. The call returns success but nothing is sent to any web server. + +Note that the spoofing and rounding actions performed by the extension can break the functionality of a website (e.g. Netflix). Please [report to us](https://github.com/polcak/jsrestrictor/issues) any malfunction websites that do not track users. + +### Levels of Protection + +JavaScript Restrictor provides four in-built levels of protection: + +* 0 - the functionality of the extension is turned off. All web pages are displayed as intended without any interaction from JavaScript Restrictor. +* 1 - the minimal level of protection. Only changes that should not break most pages are enabled. + Note that timestamps are rounded so pages relying on precise time may be broken. +* 2 - intended to be used as a default level of protection, this level should not break any site + while maintaining strong protection. +* 3 - maximal level of protection: enable all functionality. + +For more accurate description of the restrictions see [levels of protection page](https://polcak.github.io/jsrestrictor/levels.html). + +The default level of protection can be set by a popup (clicking on JSR icon) or through options of the extension. Specific level of protection for specific domains can be set in options by adding them to the list of websites with specific level of protection. This can be done also by a popup during a visit of the website. + +## Contributing + +If you have any questions or you have spotted a bug, please [let us know](https://github.com/polcak/jsrestrictor/issues). + +If you would like to give us [feedback](https://github.com/polcak/jsrestrictor/issues), we would really appreciate it. + +Once you install the extension, see the [test page](test/test.html) for the working demo on how the +extension can help in restricting JS capabilities. + +## License Information + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either [version +3](https://www.gnu.org/licenses/gpl-3.0) of the License, or (at your option) any later version. diff --git a/docs/levels.md b/docs/levels.md index 2c4b70a..5e3b93f 100644 --- a/docs/levels.md +++ b/docs/levels.md @@ -11,7 +11,7 @@ NBS is active independently on the levels defined below. If necessary, you can w ### Level 1 -* **Manipulate the time precision provided by Date, performance, and Geolocation API --** *ON* +* **Manipulate the time precision provided by Date, performance, events, gamepads, virtual reality, and Geolocation API --** *ON* * Round time to: *hundredths of a second (1.230 -- Date, 1230 -- performance, Geolocation API)* * **Protect against canvas fingerprinting --** *OFF* * **List of microphones and cameras: --** *original* @@ -24,12 +24,15 @@ NBS is active independently on the levels defined below. If necessary, you can w * **Protect against SharedArrayBuffer exploitation --** *OFF* * **Protect against WebWorker exploitation --** *OFF* * **Limit Geolocation API --** *Use accuracy of hundreds of meters* +* **Gamepad API --** *List all attached gamepads* +* **Original virtual reality API --** *List all attached VR sets* +* **Mixed reality API --** *Enabled* +* **navigator.sendBeacon --** *Do not send anything and return true* * **Disable Battery status API --** *ON* * **window.name --** *Clear with each page load* -* **navigator.sendBeacon --** *Do not send anything and return true* ### Level 2 -* **Manipulate the time precision provided by Date, performance, and Geolocation API --** *ON* +* **Manipulate the time precision provided by Date, performance, events, gamepads, virtual reality, and Geolocation API --** *ON* * Round time to: *tenths of a second (1.200 -- Date, 1200 -- performance, Geolocation API)* * **Protect against canvas fingerprinting: --** *ON* * Reading from canvas returns white image. @@ -42,12 +45,15 @@ NBS is active independently on the levels defined below. If necessary, you can w * **Protect against SharedArrayBuffer exploitation --** *OFF* * **Protect against WebWorker exploitation --** *OFF* * **Limit Geolocation API --** *Use accuracy of kilometers* +* **Gamepad API --** *Hide all attached gamepads* +* **Original virtual reality API --** *Hide all attached VR sets* +* **Mixed reality API --** *Disabled* +* **navigator.sendBeacon --** *Do not send anything and return true* * **Disable Battery status API --** *ON* * **window.name --** *Clear with each page load* -* **navigator.sendBeacon --** *Do not send anything and return true* ### Level 3 -* **Manipulate the time precision provided by Date, performance, and Geolocation API --** *ON* +* **Manipulate the time precision provided by Date, performance, events, gamepads, virtual reality, and Geolocation API --** *ON* * Round time to: *full seconds (1.000 -- Date, 1000 -- performance)* * *Randomize time* * **Protect against canvas fingerprinting: --** *ON* @@ -64,6 +70,9 @@ NBS is active independently on the levels defined below. If necessary, you can w * **Protect against WebWorker exploitation --** *ON* * *Remove real parallelism* -- Use Worker polyfill instead of the native Worker. * **Limit Geolocation API --** *Disabled* +* **Gamepad API --** *Hide all attached gamepads* +* **Original virtual reality API --** *Hide all attached VR sets* +* **Mixed reality API --** *Disabled* +* **navigator.sendBeacon --** *Do not send anything and return true* * **Disable Battery status API --** *ON* * **window.name --** *Clear with each page load* -* **navigator.sendBeacon --** *Do not send anything and return true* diff --git a/docs/new-wrapper.md b/docs/new-wrapper.md index 10bf0c2..de58230 100644 --- a/docs/new-wrapper.md +++ b/docs/new-wrapper.md @@ -45,12 +45,26 @@ Each wrapping object must have the following mandatory properties: * `parent_object` and `parent_object_property` are used to define the name of the wrapping (`parent_object.parent_object_property`) that is referenced by level wrappers. Additionally, it is -used if `wrapper_prototype` is defined to provide the object name to have the prototype changed. Finally, `Object.freeze` is called on `parent_object.parent_object_property`. -* `wrapped_objects` is a list of objects, each having two properties: - * `original_name` - the original name of the object to be wrapped. Do not mention `window` here! - * `wrapped_name` - the vatiable name that can be used by `wrapping_function_body`, `helping_code` +used if `wrapper_prototype` is defined to provide the object name to have the prototype changed. Finally, `Object.freeze` can be optionally called on `parent_object.parent_object_property`. +* `apply_if` optionally provides a condition that needs to be fullfilled to apply the wrapper. For + example if a wrapper should be applied only when an API already provides some information. For + example, `apply_if: "navigator.plugins.length > 0"`. +* `wrapped_objects` is a list of objects, each having the following properties (1 mandatory, 2 + optional, typically, wrappers use one of the optional names in the wrapper code to access the + original result of the call): + * `original_name` (_mandatory_) - the original name of the object to be wrapped. Do not mention `window` here! + * `wrapped_name` - the variable name that points to the actual original object. Wrappers might need it for identity comparisons or other instance-specific use cases. The variable name can be used by `wrapping_function_body`, `helping_code` and other code fragments to reference the original object. Note that this name is not available outside the code generated by this wrappper. + * `callable_name` - is similar to the `wrapped_name` but the variable refers to a proxy of the original object. The proxy intercepts calls and automatically marshals parameters and return values. Wrappers MUST define `callable_name` if the object is passed to other code like `Promise` objects or +callbacks. The variable is available only + in the code generate by this wrapper. `callable_name` is specifically **meant to be used for native methods and functions** + which the wrapper needs to call. This is especially important if it **accepts callback arguments or returns `Promise` objects**: + invoking them through their `callable_name` automates complex steps otherwise required for [sandboxed browser extensions to interact with web pages](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts). + +Generally speaking, use `wrapped_name` whenever you need to access the original objects only inside +the wrapper and you do not need to pass the object to other code, such as `Promise` objects or +callbacks. Compared to `callable_name`, `wrapped_name` has less overhead and is the preferred way. Each wrapping object can have the following optional properties: @@ -72,7 +86,7 @@ prototype identifier provided by `wrapper_prototype`. * `replace_original_function` is used to control which function should be replaced * `post_replacement_code` Allows to provide additional code with the access to the original function and to the wrapped function -* `nofreeze` if set to `true` causes the API not to be freeed. Use this option with caution and make sure that the wrapping is not deletable. +* `freeze` if set to `true` causes the API to be freezed. It should not be necessary if the wrapping is performed at the right level of the object's prototype chain (i.e. where the property to be modified was originally defined, i.e. usually in the object's prototype). Use this option with caution, only if strictly needed, because native APIs are configurable (otherwise our wrapping couldn't work) and therefore freezing them introduces a "weirdness", exploitable for fingerprinting. * `post_wrapping_code` is a list of additional wrapping objects with a similar structure to the wrappers, see the section bellow. @@ -157,7 +171,31 @@ Currently jShelter supports additional wrapping of: delete_properties: ["geolocation"], } ``` - +### The WrapHelper API + +A `WrapHelper` object is globally available to wrappers code, exposing some methods and properties which are mostly +used internally by the code builders to automate tasks such as handling [Firefox's content script sandbox](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts) or making replacement objects look as native as possible. +However a few of them may be useful to complex wrappers or in edge case not covered by `callable_name` and other declarative object replacement / property definition wrapper constructs: + +* `WrapHelper.shared: {}` - a "bare" JavaScript object which a wrapper can use to share information with other wrappers + (e.g. to coordinate behavior between related parts of the same API requiring multiple wrappings) + by attaching its own data objects as properties. __Warning__: namespacing is not enforced and up to the wrapper implementor, but obviously recommended. +* `WrapHelper.overlay(obj, data)` - Proxies the prototype of the `obj` object in order to return the properties of the `data` object + as if they were native properties (e.g. as if they were returned by getters on the prototype chain, + rather than defined on the instance). This allows spoofing some native objects data in a less detectable / fingerprintable way + than by using `Object.defineProperty()`. See `wrappingS-MCS.js` for an example. +* `WrapHelper.forPage(obj)` - it's mostly used internally and transparently by `code_builder.js`, + but may be useful to very complex proxies in edge cases needing to explicitly + prepare an object/function created in [Firefox's sandboxed content script environment](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts) to be consumed/called from the page context, and to make replacements for native + objects and functions provided by the wrappers look as much native as possible (on Chromium, too). + In most cases, however, this gets automated by the code builders replacing + Object methods with their WrapHelper counterpart in the wrapper sources + and by proxying "callable_name" function references through `WrapHelper.pageAPI()` (see below). +* `WrapHelper.pageAPI(f)` - proxies the function/method f so that arguments and return values, and especially callbacks and + `Promise` objects, are recursively managed in order to transparently marshal objects back and forth + [Firefox's sandbox for extensions (https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts). + __Wrapper implementors should almost never need to use this API directly__, since any function referenced via its "callable_name" + goes automatically through it. ### The generated wrapper structure @@ -189,7 +227,7 @@ wrapping_code_function_name(window if wrapping_code_function_call_window) // The ## Compiling the wrappers -Of course the wrappers need to be compiled to JavaScript before inserting the code to page scripts. See `code_builders.js` and `ffbug1267027.js`. +Of course the wrappers need to be compiled to JavaScript before inserting the code to page scripts. See `code_builders.js`. ## Registering a new wrapper diff --git a/docs/permissions.md b/docs/permissions.md index 8942484..beaf2fa 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -1,6 +1,6 @@ Title: Permissions -jShelter requires these permissions: +JShelter requires these permissions: * **storage**: for storing extension configuration and user options * **tabs**: for updating the extension's icon badge on tab change diff --git a/docs/test/date.js b/docs/test/date.js index a54710b..12d7408 100644 --- a/docs/test/date.js +++ b/docs/test/date.js @@ -44,3 +44,13 @@ function initClock() { window.setInterval("updateClock()", 1); } +window.addEventListener("DOMContentLoaded", function () { + let origEventEl = document.getElementById("orig-event"); + let currentEventEl = document.getElementById("current-event"); + let origEvent = new Event("test"); + setInterval(function() { + let currentEvent = new Event("test"); + origEventEl.innerHTML = origEvent.timeStamp; + currentEventEl.innerHTML = currentEvent.timeStamp; + }, 100); +}); diff --git a/docs/test/enumerateDevices.js b/docs/test/enumerateDevices.js index 9685477..a6f65c3 100644 --- a/docs/test/enumerateDevices.js +++ b/docs/test/enumerateDevices.js @@ -2,9 +2,7 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -let ul = document.getElementById("enumerateDevices"); - -function appendLi(text) +function appendLi(ul, text) { let li = document.createElement("li"); li.innerText = text; @@ -12,17 +10,40 @@ function appendLi(text) } if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - appendLi("Your browser does not support enumerateDevices() so it does not leak IDs of cameras and microphones."); + appendLi(document.getElementById("enumerateDevices"), "Your browser does not support enumerateDevices() so it does not leak IDs of cameras and microphones."); } else { + let ul = document.getElementById("enumerateDevices"); navigator.mediaDevices.enumerateDevices() .then(function(devices) { - appendLi(devices.length.toString() + " cameras and microphones detected"); + appendLi(ul, devices.length.toString() + " cameras and microphones detected"); devices.forEach(function(device) { - appendLi(device.kind + ": " + device.label + " id = " + device.deviceId); + appendLi(ul, device.kind + ": " + device.label + " id = " + device.deviceId); }); }) .catch(function(err) { - appendLi(err.name + ": " + err.message); + appendLi(ul, err.name + ": " + err.message); }); } + +if (navigator.getGamepads().length === 0) { + appendLi(document.getElementById("gamepads"), "Your browser does not report any attached gamepad."); +} +else { + let ul = document.getElementById("gamepads"); + navigator.getGamepads().forEach(function(gp) { + appendLi(ul, gp.id); + }); +} + +if (!navigator.getVRDisplays) { + appendLi(document.getElementById("vr"), "Your browser does not report any attached VR set."); +} + + +if (!navigator.xr) { + document.getElementById("webxr").innerText = "Your browser does not support mixed reality and does not leak any identifying information from WebXR APIs."; +} +else { + document.getElementById("webxr").innerText = "Your browser is at risk to provide fingerprintable information from WebXR APIs."; +} diff --git a/docs/test/iframe.html b/docs/test/iframe.html new file mode 100644 index 0000000..6da8ec9 --- /dev/null +++ b/docs/test/iframe.html @@ -0,0 +1,10 @@ + + + Iframe content + + + + + + + diff --git a/docs/test/iframe.js b/docs/test/iframe.js new file mode 100644 index 0000000..255d508 --- /dev/null +++ b/docs/test/iframe.js @@ -0,0 +1,21 @@ +"use strict"; + +function createResults(canvas_el, result_id) { + var html = "

      toDataURL

      "; + html += "
      " + canvas_el.toDataURL + "
      "; + html += "

      getImageData

      "; + html += "
      " + canvas_el.getContext('2d').getImageData + "
      "; + html += "

      toBlob

      "; + html += "
      " + canvas_el.toBlob + "
      "; + + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + // Native toString function from iframe context which can be used later on. + var iframeToString = iframe.contentWindow.window.Function.prototype.toString; + iframe.parentNode.removeChild(iframe); + html += "

      performance.now

      "; + html += "
      " + iframeToString.call(performance.now) + "
      "; + + document.getElementById(result_id).innerHTML = html; +} +createResults(document.createElement("canvas"), "iframe_result"); diff --git a/docs/test/plugins.js b/docs/test/plugins.js index 731ebd1..3de9050 100644 --- a/docs/test/plugins.js +++ b/docs/test/plugins.js @@ -2,18 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -function getUserAgent(){ - var userAgent = navigator.userAgent; - var appVersion = navigator.appVersion; - var vendor = navigator.vendor; - var doNotTrack = navigator.doNotTrack? "True": "False"; - document.getElementById('userAgent').innerHTML += userAgent; - document.getElementById('appVersion').innerHTML += appVersion ; - document.getElementById('browserVendor').innerHTML += vendor ; - document.getElementById('doNotTrack').innerHTML += doNotTrack; - -} - function getPlugins(){ var plugins = navigator.plugins; var mimeTypes = navigator.mimeTypes; @@ -34,7 +22,6 @@ function getPlugins(){ } document.addEventListener('DOMContentLoaded', function() { - getUserAgent(); setTimeout(function() { getPlugins(); }, 100); diff --git a/docs/test/poc.js b/docs/test/poc.js new file mode 100644 index 0000000..680548d --- /dev/null +++ b/docs/test/poc.js @@ -0,0 +1,4 @@ +"use strict"; + +createResults(document.getElementById("poc_iframe").contentDocument.createElement("canvas"), "poc"); +createResults(document.getElementById("regular_iframe").contentDocument.createElement("canvas"), "regular"); diff --git a/docs/test/test.html b/docs/test/test.html index 9c1a1eb..468b842 100644 --- a/docs/test/test.html +++ b/docs/test/test.html @@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - Simple examples for implemented browser extension + jShelter Test Page @@ -22,6 +22,8 @@ SPDX-License-Identifier: GPL-3.0-or-later + + @@ -29,33 +31,28 @@ SPDX-License-Identifier: GPL-3.0-or-later

      You can try Am I Unique or Panopticlick to learn how identifiable you are on the Internet and test JavaScript Restrictor Extension


      -

      Simple examples for implemented browser extension

      +

      jShelter Test Page


      -

      window.Date example

      +

      High precision timer sources

      + Date object 00: 00: - 00: + 00. 000
      -



      - - -

      window.performance example

      + performance.now Current performance is: -
      +
      + Events +
      Timestamp of an event created during page load is: -
      +
      Timestamp of a recent event is: -
      +




      -

      Navigator info

      -

      User agent:

      -

      App Version:

      -

      Vendor:

      -

      Do Not Track:

      -


      -
      -

      Canvas fingerprinting

      window.HTMLCanvasElement.toDataURL() example

      @@ -207,12 +204,42 @@ SPDX-License-Identifier: GPL-3.0-or-later



      -

      Your multimedia devices (cameras, microphones) connected to the computer

      +

      Your devices connected to the computer

      +

      Cameras and microphones

        +

        Gamepads

        +
          +

          Virtual reality sets

          +
            +



            + +

            Mixed reality support

            +




            window.XMLHttpRequest example

            +



            + +

            Wrappers and iframes

            +

            Iframes can be misused to evade wrappers. Prior to 0.5, jShelter limited some wrappers but not + all. To be fully protected, you should see [native code] as a body + of all displayed functions. Note that you should see the same content even if you do not use any + extension that modifies the calls. We want not to be distinguishable even if fully protected to + limit fingerprinting.

            +

            JavaScript executed in the main page

            + + + +

            JavaScript executed in a regular iframe

            + + + +

            JavaScript executed in a same origin iframe

            + + diff --git a/docs/test/tests.js b/docs/test/tests.js deleted file mode 100644 index cb1c160..0000000 --- a/docs/test/tests.js +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Peter Horňák -// -// SPDX-License-Identifier: GPL-3.0-or-later - -QUnit.assert.arrayEqual = function (a, b, msg) { - function toArray(arr) { - var resultArr = []; - for (let i = 0; i < arr.length; ++i) - resultArr[i] = arr[i]; - return resultArr; - } - - this.deepEqual(toArray(a), toArray(b), msg); -}; - -QUnit.test('ArrayBufferViews', function (assert) { - assert.expect(6); - let buffer = new ArrayBuffer(56); - let typedArr = new Uint32Array(buffer, 16); - typedArr.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - assert.deepEqual(typedArr.buffer, buffer); - assert.deepEqual(typedArr.byteOffset, 16); - assert.deepEqual(typedArr.byteLength, 40); - let dataView = new DataView(buffer, 32); - assert.deepEqual(dataView.buffer, buffer); - assert.deepEqual(dataView.byteOffset, 32); - assert.deepEqual(dataView.byteLength, 24); -}); - -QUnit.test('TypedArraysInit', function (assert) { - let typedArray; - - typedArray = new Uint32Array([1, 100, 1000, 2, 200]); - assert.arrayEqual(typedArray, [1, 100, 1000, 2, 200], 'array'); - typedArray[0] = 10; - assert.arrayEqual(typedArray, [10, 100, 1000, 2, 200], 'array'); - assert.deepEqual(typedArray.BYTES_PER_ELEMENT, 4, 'array'); - assert.deepEqual(typedArray.length * typedArray.BYTES_PER_ELEMENT, typedArray.byteLength, 'array'); - assert.deepEqual(typedArray.length, 5, 'array'); - assert.deepEqual(typedArray.byteOffset, 0, 'array'); - - typedArray = new Uint8Array(5); - assert.arrayEqual(typedArray, [0, 0, 0, 0, 0], 'len'); - typedArray.set([1, 2, 3, 4, 5]); - assert.arrayEqual(typedArray, [1, 2, 3, 4, 5], 'len'); - typedArray[0] = 100; - assert.arrayEqual(typedArray, [100, 2, 3, 4, 5], 'len'); - assert.deepEqual(typedArray.BYTES_PER_ELEMENT, 1, 'len'); - assert.deepEqual(typedArray.length * typedArray.BYTES_PER_ELEMENT, typedArray.byteLength, 'len'); - assert.deepEqual(typedArray.length, 5, 'len'); - assert.deepEqual(typedArray.byteOffset, 0, 'len'); - - arrayBuffer = new ArrayBuffer(5); - typedArray = new Uint8Array(arrayBuffer); - assert.arrayEqual(typedArray, [0, 0, 0, 0, 0], 'buffer'); - typedArray.set([1, 2, 3, 4, 5]); - assert.arrayEqual(typedArray, [1, 2, 3, 4, 5], 'buffer'); - typedArray[0] = 100; - assert.arrayEqual(typedArray, [100, 2, 3, 4, 5], 'buffer'); - assert.deepEqual(typedArray.BYTES_PER_ELEMENT, 1, 'buffer'); - assert.deepEqual(typedArray.length * typedArray.BYTES_PER_ELEMENT, typedArray.byteLength, 'buffer'); - assert.deepEqual(typedArray.length, 5, 'buffer'); - assert.deepEqual(typedArray.byteOffset, 0, 'buffer'); -}); - -QUnit.test('TypedArraysMethods', function (assert) { - let defaultVals = [1, 2, 3, 4, 5]; - let typedArray = new Uint8Array(5); - typedArray.set(defaultVals); - assert.arrayEqual(typedArray, defaultVals); - typedArray.reverse(); - assert.arrayEqual(typedArray, [5, 4, 3, 2, 1]); - typedArray.sort(); - assert.arrayEqual(typedArray, defaultVals); - typedArray.fill(10, 0, 2); - assert.arrayEqual(typedArray, [10, 10, 3, 4, 5]); - typedArray.copyWithin(2, 0, 2,); - assert.arrayEqual(typedArray, [10, 10, 10, 10, 5]); - let sub = typedArray.subarray(0, 4); - assert.arrayEqual(sub, [10, 10, 10, 10]); - sub[0] = 1; - assert.deepEqual(sub[0], typedArray[0]); - let slice = typedArray.slice(0, 2); - assert.arrayEqual(slice, [1, 10]); - slice[0] = 100; - assert.notDeepEqual(slice[0], typedArray[0]); - map = typedArray.map(x => x * 2); - assert.arrayEqual(map, [2, 20, 20, 20, 10]); - filter = typedArray.filter(x => x === 10); - assert.arrayEqual(filter, [10, 10, 10]); - reduce = typedArray.reduce(function (prev, curr) { - return prev + curr; - }); - assert.deepEqual(reduce, 36); - reduceR = typedArray.reduce(function (prev, curr) { - return prev + curr; - }); - assert.deepEqual(reduceR, 36); - assert.deepEqual(typedArray.lastIndexOf(10), 3); - let forEachArr = []; - typedArray.forEach(x => forEachArr.push(x)); - assert.deepEqual(forEachArr, [1, 10, 10, 10, 5]); - let find = typedArray.find(function (value, index, obj) { - return value > 1; - }); - assert.deepEqual(find, 10); - assert.deepEqual(typedArray.join(), "1,10,10,10,5"); - assert.deepEqual(typedArray.entries().next().value, [0, 1]); - assert.deepEqual(typedArray.keys().next().value, 0); - assert.deepEqual(typedArray.values().next().value, 1); - - assert.arrayEqual(Uint8Array.from([1, 2, 3]), [1, 2, 3]); - assert.arrayEqual(Uint8Array.of(1, 2, 3, 4), [1, 2, 3, 4]); -}); - -QUnit.test('DataViewInit', function (assert) { - let buff = new ArrayBuffer(16); - let dataView = new DataView(buff); - assert.deepEqual(dataView.byteLength, 16); - assert.deepEqual(dataView.byteOffset, 0); - assert.deepEqual(dataView.buffer, buff); - dataView = new DataView(buff, 4, 8); - assert.deepEqual(dataView.byteLength, 8); - assert.deepEqual(dataView.byteOffset, 4); -}); - -const dataViewGets = ['getInt8', 'getInt16', 'getInt32', 'getUint8', 'getUint16', 'getUint32', 'getFloat32', 'getFloat64', 'getBigInt64', 'getBigUint64']; -const dataViewSets = ['setInt8', 'setInt16', 'setInt32', 'setUint8', 'setUint16', 'setUint32', 'setFloat32', 'setFloat64', 'setBigInt64', 'setBigUint64']; - -QUnit.test('DataViewMethods', function (assert) { - buff = new ArrayBuffer(128); - dataView = new DataView(buff); - for (let i in dataViewGets) { - let n = i * 10 + 1; - if (i >= dataViewGets.length - 2) { - n = BigInt(n); - } - dataView[dataViewSets[i]](0, n); - assert.deepEqual(dataView[dataViewGets[i]](0), n, 'Big endian'); - } - - for (let i in dataViewGets) { - let n = i * 10 + 1; - if (i >= dataViewGets.length - 2) { - n = BigInt(n); - } - dataView[dataViewSets[i]](0, n, true); - assert.deepEqual(dataView[dataViewGets[i]](0, true), n, 'Little endian'); - } - - dataView.setFloat64(1, 123456.7891); - assert.deepEqual(dataView.getFloat64(1), 123456.7891, 'Floats'); - - dataView.setInt32(2, -12345); - assert.deepEqual(dataView.getInt32(2), -12345, 'Signed int'); - - dataView.setBigInt64(0, -1234567890123456789n); - res = dataView.getBigInt64(0); - assert.deepEqual(res, -1234567890123456789n, 'Signed BigInt') -}); - -QUnit.test('OneBufferMoreViews', function (assert) { - let aBuff = new ArrayBuffer(12); - let typedArray = new Int8Array(aBuff); - let dataView = new DataView(aBuff); - typedArray[0] = 10; - assert.deepEqual(typedArray[0], dataView.getInt8(0), 'Known failure'); -}); - -QUnit.test('Worker', function (assert) { - let worker; - worker = new Worker(''); - - assert.equal(worker.onmessage, null); - assert.equal(worker.onerror, null); - assert.equal(typeof worker.addEventListener, 'function'); - assert.equal(typeof worker.dispatchEvent, 'function'); - assert.equal(typeof worker.postMessage, 'function'); - assert.equal(typeof worker.removeEventListener, 'function'); - assert.equal(typeof worker.terminate, 'function'); -}); diff --git a/docs/test/unit_test.html b/docs/test/unit_test.html deleted file mode 100644 index 06748d7..0000000 --- a/docs/test/unit_test.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - Unit tests for Chrome Zero related wrapping - - - -
            -
            - - - - diff --git a/docs/versions.md b/docs/versions.md index 4746a6c..2094d81 100644 --- a/docs/versions.md +++ b/docs/versions.md @@ -1,5 +1,27 @@ Title: Release history +## 0.5 + +* Add fingerprinting defenses based on Farbling developed by the Brave browser (improved or added + wrappers for Canvas, Audio, Web GL, device memory, hardware concurrency, enumerateDevices). Most + wrappers support provisioning of white lies that differ between origins and sessions (the + fingeprint is different across origins and across sessions). + * We claimed to generate white image fake Canvas value but instead generated fully transparent black image. We now generate the white image as it is more common in other anti-canvas fingerprinting tools (level 3). + * toDataUrl() no longer destructs the original canvas. +* We use NoScript Commons Library to simplify some tasks like cross-browser support. + * More reliable early content script configuration. + * CSP headers no longer prevents the extension from wrapping JS APIs in Firefox (Github issue #25) + * Wrappers should be injected reliably before page scripts start to operate (Github issue #40) + * We use NSCL to wrap APIs in iframes and workers + * It is no longer possible to access unwrapped functions from iframes and workers (Pagure issue #2, Github issue #56) +* Ignore trailing '.' in domain names when selecting appropriate custom level. +* Do not freeze wrappers to prevent fingeprintability of the users of JSR. We wrap the correct function + in the prototype chain instead. +* navigator.getGamepads() wrapper added +* navigator.activeVRDisplays() and navigator.xr wrappers added +* Limit precision of high resolution timestamps in the Event, VRFrameData, and Gamepad interface to be consistent + with Date and Performance precision + ## 0.4.7 * Wrap Beacon API diff --git a/firefox/manifest.json b/firefox/manifest.json index b6b4d95..5501282 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -40,13 +40,13 @@ "all_frames": true, "match_about_blank": true, "js": [ + "nscl/common/uuid.js", "nscl/content/patchWindow.js", + "nscl/lib/sha256.js", "alea.js", "helpers.js", - "inject.js", WRAPPING, "code_builders.js", - "ffbug1267027.js", "document_start.js"], "run_at": "document_start" } @@ -80,7 +80,7 @@ "notifications" ], "short_name": "JSR", - "version": "0.4.6", + "version": "0.5", "browser_specific_settings": { "gecko": { "id": "jsr@javascriptrestrictor", diff --git a/nscl b/nscl index c4458ef..d31c527 160000 --- a/nscl +++ b/nscl @@ -1 +1 @@ -Subproject commit c4458ef291e12c35fd458ea7d0cd519238efcd3e +Subproject commit d31c52710bbc9dfd958a4b20574b008bad96f947 diff --git a/tests/integration_tests/README.md b/tests/integration_tests/README.md index 6ff92fe..c11cea6 100644 --- a/tests/integration_tests/README.md +++ b/tests/integration_tests/README.md @@ -67,7 +67,7 @@ Download the correct GeckoDriver to folder `../common_files/webbrowser_drivers` 1. Install Windows Subsystem for Linux (WSL): https://docs.microsoft.com/en-us/windows/wsl/install-win10. -2. Convert EOL in the script `fix_manifest.sh` (in the root directory of JSR project) from Windows (CR LF) to Unix (LF) - you can use the tool `dos2unix` in WSL to convert CR LF to LF. +2. Convert EOL in the scripts `fix_manifest.sh` (in the root directory of JSR project) and `nscl/include.sh` from Windows (CR LF) to Unix (LF) - you can use the tool `dos2unix` in WSL to convert CR LF to LF. 3. Open root directory of JSR project in WSL and run command `make`. diff --git a/tests/integration_tests/testing/configuration.py b/tests/integration_tests/testing/configuration.py index f611181..9be1c85 100644 --- a/tests/integration_tests/testing/configuration.py +++ b/tests/integration_tests/testing/configuration.py @@ -35,7 +35,7 @@ class __Config: # Browsers in which tests will be run. tested_browsers = [BrowserType.CHROME, BrowserType.FIREFOX] # Default levels of JSR which will be tested. - tested_jsr_levels = [0, 1, 2, 3, 4] + tested_jsr_levels = [0, 1, 2, 3] diff --git a/tests/integration_tests/testing/math_operations.py b/tests/integration_tests/testing/math_operations.py index 7d3e54a..cb72f4c 100644 --- a/tests/integration_tests/testing/math_operations.py +++ b/tests/integration_tests/testing/math_operations.py @@ -21,6 +21,10 @@ # along with this program. If not, see . # + +from math import radians, sin, cos, sqrt, atan2 + + ## Check if number is in given accuracy and return True/False. # # Function expects two number (int/float) or string(s) containing valid number(s). @@ -30,7 +34,7 @@ # E.g.: is_in_accuracy(1654800, 10) => True # E.g.: is_in_accuracy(1654800, 1000) => False def is_in_accuracy(number, accuracy): - number_str = str(int(number))[::-1] + number_str = str(int(float(number)))[::-1] accuracy_str = str(int(accuracy))[::-1] index = 0 while accuracy_str[index] == '0': @@ -40,3 +44,28 @@ def is_in_accuracy(number, accuracy): return False index += 1 return True + + +## Calculate distance between two GEO coordinates (latitude and longitude) in meters. +# +# Function expects two pairs of GEO coordinates. Coordinates have to be float or int type (not string). +# {lat1, lon1} is the first position. +# {lat2, lon2} is the second position. +# Function returns distance between two position on Earth in meters. +# The function takes into account the curved surface of the Earth. +def calc_distance(lat1, lon1, lat2, lon2): + # approximate radius of earth in km + R = 6371.0 + + # difference between longitude converted from degrees to radians + dlon = radians(lon2 - lon1) + # difference between latitude converted from degrees to radians + dlat = radians(lat2 - lat1) + + a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 + c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + distance_km = R * c + distance_m = distance_km * 1000 + + return distance_m diff --git a/tests/integration_tests/testing/tests_definition/test_canvas.py b/tests/integration_tests/testing/tests_definition/test_canvas.py index 8abea5e..522ad77 100644 --- a/tests/integration_tests/testing/tests_definition/test_canvas.py +++ b/tests/integration_tests/testing/tests_definition/test_canvas.py @@ -82,23 +82,23 @@ def test_to_blob(browser, expected): assert image == browser.real.canvas_blob def test_is_point_in_path(browser, expected): - point = get_point_in_path(browser.driver,"canvas4") + point = get_point_in_path(browser.driver,"canvas4", (expected.canvas_point_path == 'FALSE VALUE')) if point == "ERROR": print("\n isPointInPath error.") assert False else: - if expected.canvas_point_path == 'SPOOF VALUE': + if expected.canvas_point_path in {'SPOOF VALUE','FALSE VALUE'}: assert point == False else: assert point == True def test_is_point_in_stroke(browser, expected): - point = get_point_in_stroke(browser.driver,"canvas5") + point = get_point_in_stroke(browser.driver,"canvas5", (expected.canvas_point_stroke == 'FALSE VALUE')) if point == "ERROR": print("\n isPointInStroke error.") assert False else: - if expected.canvas_point_stroke == 'SPOOF VALUE': + if expected.canvas_point_stroke in {'SPOOF VALUE','FALSE VALUE'} : assert point == False else: assert point == True diff --git a/tests/integration_tests/testing/tests_definition/test_gps.py b/tests/integration_tests/testing/tests_definition/test_gps.py index 0d81a02..90bcaaf 100644 --- a/tests/integration_tests/testing/tests_definition/test_gps.py +++ b/tests/integration_tests/testing/tests_definition/test_gps.py @@ -25,8 +25,7 @@ import pytest from time import time from values_getters import get_position -from math_operations import is_in_accuracy - +from math_operations import is_in_accuracy, calc_distance ## Setup method - it is run before gps tests execution starts. # @@ -58,10 +57,18 @@ def test_accuracy(browser, position, expected): # If current value is null, real value has to be null too. assert position['accuracy'] == browser.real.geolocation.accuracy else: - if expected.geolocation.accuracy['accuracy'] == 'EXACTLY': - # Values do not have to be strictly equal. - # A deviation of less than 50 meters is tolerated. - assert abs(float(position['accuracy']) - float(browser.real.geolocation.accuracy)) < 50 + if expected.geolocation.accuracy['accuracy'] == 'EXACTLY': + # x is real position (position returned without JSR) + # y should be real position too (position returned with JSR level 0) + # + # It is clear that x and y will not be exact same values. This is due to the netural GPS inaccuracy. + # A small difference is tolerated. + # x.accuracy and y.accuracy will be probably different. + # But distance between x and y should be less than (x.accuracy + y.accuracy). + assert calc_distance(float(browser.real.geolocation.latitude), + float(browser.real.geolocation.longitude), + float(position['latitude']), + float(position['longitude'])) < (float(browser.real.geolocation.accuracy) + float(position['accuracy'])) else: # Should be rounded real value in accuracy. assert is_in_accuracy(position['accuracy'], expected.geolocation.accuracy['accuracy']) diff --git a/tests/integration_tests/testing/tests_definition/test_hw.py b/tests/integration_tests/testing/tests_definition/test_hw.py index fb97509..74e2157 100644 --- a/tests/integration_tests/testing/tests_definition/test_hw.py +++ b/tests/integration_tests/testing/tests_definition/test_hw.py @@ -46,12 +46,11 @@ def IOdevices(browser): ## Test device memory. def test_device_memory(browser, device, expected): - if expected.device.deviceMemory[browser.type] == 'REAL VALUE': - assert device['deviceMemory'] == browser.real.device.deviceMemory - elif expected.device.deviceMemory['value'] == 'SPOOF VALUE': + if expected.device.deviceMemory[browser.type] == 'SPOOF VALUE': assert device['deviceMemory'] in expected.device.deviceMemory['valid_values'] + assert device['deviceMemory'] <= browser.real.device.deviceMemory else: - assert device['deviceMemory'] == expected.device.deviceMemory[browser.type] + assert device['deviceMemory'] == browser.real.device.deviceMemory ## Test hardware concurrency. @@ -59,9 +58,12 @@ def test_hardware_concurrency(browser, device, expected): if expected.device.hardwareConcurrency['value'] == 'REAL VALUE': assert device['hardwareConcurrency'] == browser.real.device.hardwareConcurrency elif expected.device.hardwareConcurrency['value'] == 'SPOOF VALUE': - assert device['hardwareConcurrency'] in expected.device.hardwareConcurrency['valid_values'] + expectedval = expected.device.hardwareConcurrency['valid_values'] + if expectedval == "UP TO REAL VALUE": + expectedval = range(browser.real.device.hardwareConcurrency + 1) + assert device['hardwareConcurrency'] in expectedval else: - assert device['hardwareConcurrency'] == expected.device.hardwareConcurrency + assert False # We should not get here ## Test IOdevices. @@ -70,5 +72,11 @@ def test_IOdevices(browser, IOdevices, expected): assert len(IOdevices) == len(browser.real.device.IOdevices) for i in range(len(IOdevices)): assert IOdevices[i]['kind'] == browser.real.device.IOdevices[i]['kind'] + elif expected.device.IOdevices == 'EMPTY': + if IOdevices == 'ERROR': + assert IOdevices == 'ERROR' + else: + assert IOdevices == [] + assert len(IOdevices) == 0 else: - assert len(IOdevices) == expected.device.IOdevices + assert len(IOdevices) in expected.device.IOdevices diff --git a/tests/integration_tests/testing/tests_definition/test_navigator.py b/tests/integration_tests/testing/tests_definition/test_navigator.py index 4c61119..10adec8 100644 --- a/tests/integration_tests/testing/tests_definition/test_navigator.py +++ b/tests/integration_tests/testing/tests_definition/test_navigator.py @@ -107,19 +107,32 @@ def test_oscpu(browser, navigator, expected): else: assert navigator['oscpu'] == expected.navigator.oscpu -## Test plugins -def test_plugins(browser, navigator, expected): - if expected.navigator.plugins == 'SPOOF VALUE': - assert navigator['plugins'] != browser.real.navigator.plugins +## Test plugins count +def test_plugins_count(browser, navigator, expected): + if expected.navigator.plugins['count'][browser.type] == 'REAL VALUE': + assert len(navigator['plugins']) == len(browser.real.navigator.plugins) + elif expected.navigator.plugins['count'][browser.type] == 'PLUS_2': + assert len(navigator['plugins']) == len(browser.real.navigator.plugins) + 2 else: + assert len(navigator['plugins']) == expected.navigator.plugins['count'][browser.type] + +## Test plugins array value +def test_plugins(browser, navigator, expected): + if expected.navigator.plugins['value'][browser.type] == 'REAL VALUE': assert navigator['plugins'] == browser.real.navigator.plugins + elif expected.navigator.plugins['value'][browser.type] == 'EMPTY': + assert not navigator['plugins'] + else: + assert navigator['plugins'] != browser.real.navigator.plugins ## Test mimeTypes def test_mime_types(browser, navigator, expected): - if expected.navigator.mimeTypes == 'SPOOF VALUE': - if navigator['mimeTypes'] == browser.real.navigator.mimeTypes: + if expected.navigator.mimeTypes == 'EMPTY': + assert navigator['mimeTypes'] == [] + elif expected.navigator.mimeTypes == 'SPOOF VALUE': + if browser.real.navigator.mimeTypes == []: assert navigator['mimeTypes'] == [] else: - assert True + assert navigator['mimeTypes'] != browser.real.navigator.mimeTypes else: assert navigator['mimeTypes'] == browser.real.navigator.mimeTypes diff --git a/tests/integration_tests/testing/tests_definition/test_webgl.py b/tests/integration_tests/testing/tests_definition/test_webgl.py index d69d8a7..b43e59a 100644 --- a/tests/integration_tests/testing/tests_definition/test_webgl.py +++ b/tests/integration_tests/testing/tests_definition/test_webgl.py @@ -50,25 +50,44 @@ def test_unmasked_renderer(browser, webgl_params, expected): # Test WebGLRenderingContext.getParameter def test_other_parameters(browser, webgl_params, expected): if expected.webgl_parameters == 'SPOOF VALUE': - assert ((webgl_params['MAX_VERTEX_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_COMPONENTS']-1}) and - (webgl_params['MAX_VERTEX_UNIFORM_BLOCKS'] in {browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_BLOCKS'],browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_BLOCKS']-1}) and - (webgl_params['MAX_VERTEX_OUTPUT_COMPONENTS'] in {browser.real.webgl_parameters['MAX_VERTEX_OUTPUT_COMPONENTS'],browser.real.webgl_parameters['MAX_VERTEX_OUTPUT_COMPONENTS']-1}) and - (webgl_params['MAX_VARYING_COMPONENTS'] in {browser.real.webgl_parameters['MAX_VARYING_COMPONENTS'],browser.real.webgl_parameters['MAX_VARYING_COMPONENTS']-1}) and - (webgl_params['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS'] in {browser.real.webgl_parameters['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS'],browser.real.webgl_parameters['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS']-1}) and - (webgl_params['MAX_FRAGMENT_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_COMPONENTS']-1}) and - (webgl_params['MAX_FRAGMENT_UNIFORM_BLOCKS'] in {browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_BLOCKS'],browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_BLOCKS']-1}) and - (webgl_params['MAX_FRAGMENT_INPUT_COMPONENTS'] in {browser.real.webgl_parameters['MAX_FRAGMENT_INPUT_COMPONENTS'],browser.real.webgl_parameters['MAX_FRAGMENT_INPUT_COMPONENTS']-1}) and - (webgl_params['MAX_UNIFORM_BUFFER_BINDINGS'] in {browser.real.webgl_parameters['MAX_UNIFORM_BUFFER_BINDINGS'],browser.real.webgl_parameters['MAX_UNIFORM_BUFFER_BINDINGS']-1}) and - (webgl_params['MAX_COMBINED_UNIFORM_BLOCKS'] in {browser.real.webgl_parameters['MAX_COMBINED_UNIFORM_BLOCKS'],browser.real.webgl_parameters['MAX_COMBINED_UNIFORM_BLOCKS']-1}) and - (webgl_params['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS']-1}) and - (webgl_params['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS']-1}) and - (webgl_params['MAX_VERTEX_ATTRIBS'] in {browser.real.webgl_parameters['MAX_VERTEX_ATTRIBS'],browser.real.webgl_parameters['MAX_VERTEX_ATTRIBS']-1}) and - (webgl_params['MAX_VERTEX_UNIFORM_VECTORS'] in {browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_VECTORS'],browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_VECTORS']-1}) and - (webgl_params['MAX_VERTEX_TEXTURE_IMAGE_UNITS'] in {browser.real.webgl_parameters['MAX_VERTEX_TEXTURE_IMAGE_UNITS'],browser.real.webgl_parameters['MAX_VERTEX_TEXTURE_IMAGE_UNITS']-1}) and - (webgl_params['MAX_TEXTURE_SIZE'] in {browser.real.webgl_parameters['MAX_TEXTURE_SIZE'],browser.real.webgl_parameters['MAX_TEXTURE_SIZE']-1}) and - (webgl_params['MAX_CUBE_MAP_TEXTURE_SIZE'] in {browser.real.webgl_parameters['MAX_CUBE_MAP_TEXTURE_SIZE'],browser.real.webgl_parameters['MAX_CUBE_MAP_TEXTURE_SIZE']-1}) and - (webgl_params['MAX_3D_TEXTURE_SIZE'] in {browser.real.webgl_parameters['MAX_3D_TEXTURE_SIZE'],browser.real.webgl_parameters['MAX_3D_TEXTURE_SIZE']-1}) and - (webgl_params['MAX_ARRAY_TEXTURE_LAYERS'] in {browser.real.webgl_parameters['MAX_ARRAY_TEXTURE_LAYERS'],browser.real.webgl_parameters['MAX_ARRAY_TEXTURE_LAYERS']-1})) + if browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_COMPONENTS'] != None: + assert webgl_params['MAX_VERTEX_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_BLOCKS'] != None: + assert webgl_params['MAX_VERTEX_UNIFORM_BLOCKS'] in {browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_BLOCKS'],browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_BLOCKS']-1} + if browser.real.webgl_parameters['MAX_VERTEX_OUTPUT_COMPONENTS'] != None: + assert webgl_params['MAX_VERTEX_OUTPUT_COMPONENTS'] in {browser.real.webgl_parameters['MAX_VERTEX_OUTPUT_COMPONENTS'],browser.real.webgl_parameters['MAX_VERTEX_OUTPUT_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_VARYING_COMPONENTS'] != None: + assert webgl_params['MAX_VARYING_COMPONENTS'] in {browser.real.webgl_parameters['MAX_VARYING_COMPONENTS'],browser.real.webgl_parameters['MAX_VARYING_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS'] != None: + assert webgl_params['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS'] in {browser.real.webgl_parameters['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS'],browser.real.webgl_parameters['MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_COMPONENTS'] != None: + assert webgl_params['MAX_FRAGMENT_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_BLOCKS'] != None: + assert webgl_params['MAX_FRAGMENT_UNIFORM_BLOCKS'] in {browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_BLOCKS'],browser.real.webgl_parameters['MAX_FRAGMENT_UNIFORM_BLOCKS']-1} + if browser.real.webgl_parameters['MAX_FRAGMENT_INPUT_COMPONENTS'] != None: + assert webgl_params['MAX_FRAGMENT_INPUT_COMPONENTS'] in {browser.real.webgl_parameters['MAX_FRAGMENT_INPUT_COMPONENTS'],browser.real.webgl_parameters['MAX_FRAGMENT_INPUT_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_UNIFORM_BUFFER_BINDINGS'] != None: + assert webgl_params['MAX_UNIFORM_BUFFER_BINDINGS'] in {browser.real.webgl_parameters['MAX_UNIFORM_BUFFER_BINDINGS'],browser.real.webgl_parameters['MAX_UNIFORM_BUFFER_BINDINGS']-1} + if browser.real.webgl_parameters['MAX_COMBINED_UNIFORM_BLOCKS'] != None: + assert webgl_params['MAX_COMBINED_UNIFORM_BLOCKS'] in {browser.real.webgl_parameters['MAX_COMBINED_UNIFORM_BLOCKS'],browser.real.webgl_parameters['MAX_COMBINED_UNIFORM_BLOCKS']-1} + if browser.real.webgl_parameters['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS'] != None: + assert webgl_params['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS'] != None: + assert webgl_params['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS'] in {browser.real.webgl_parameters['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS'],browser.real.webgl_parameters['MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS']-1} + if browser.real.webgl_parameters['MAX_VERTEX_ATTRIBS'] != None: + assert webgl_params['MAX_VERTEX_ATTRIBS'] in {browser.real.webgl_parameters['MAX_VERTEX_ATTRIBS'],browser.real.webgl_parameters['MAX_VERTEX_ATTRIBS']-1} + if browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_VECTORS'] != None: + assert webgl_params['MAX_VERTEX_UNIFORM_VECTORS'] in {browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_VECTORS'],browser.real.webgl_parameters['MAX_VERTEX_UNIFORM_VECTORS']-1} + if browser.real.webgl_parameters['MAX_VERTEX_TEXTURE_IMAGE_UNITS'] != None: + assert webgl_params['MAX_VERTEX_TEXTURE_IMAGE_UNITS'] in {browser.real.webgl_parameters['MAX_VERTEX_TEXTURE_IMAGE_UNITS'],browser.real.webgl_parameters['MAX_VERTEX_TEXTURE_IMAGE_UNITS']-1} + if browser.real.webgl_parameters['MAX_TEXTURE_SIZE'] != None: + assert webgl_params['MAX_TEXTURE_SIZE'] in {browser.real.webgl_parameters['MAX_TEXTURE_SIZE'],browser.real.webgl_parameters['MAX_TEXTURE_SIZE']-1} + if browser.real.webgl_parameters['MAX_CUBE_MAP_TEXTURE_SIZE'] != None: + assert webgl_params['MAX_CUBE_MAP_TEXTURE_SIZE'] in {browser.real.webgl_parameters['MAX_CUBE_MAP_TEXTURE_SIZE'],browser.real.webgl_parameters['MAX_CUBE_MAP_TEXTURE_SIZE']-1} + if browser.real.webgl_parameters['MAX_3D_TEXTURE_SIZE'] != None: + assert webgl_params['MAX_3D_TEXTURE_SIZE'] in {browser.real.webgl_parameters['MAX_3D_TEXTURE_SIZE'],browser.real.webgl_parameters['MAX_3D_TEXTURE_SIZE']-1} + if browser.real.webgl_parameters['MAX_ARRAY_TEXTURE_LAYERS'] != None: + assert webgl_params['MAX_ARRAY_TEXTURE_LAYERS'] in {browser.real.webgl_parameters['MAX_ARRAY_TEXTURE_LAYERS'],browser.real.webgl_parameters['MAX_ARRAY_TEXTURE_LAYERS']-1} elif expected.webgl_parameters == 'ZERO VALUE': assert ((webgl_params['MAX_VERTEX_UNIFORM_COMPONENTS'] == 0) and (webgl_params['MAX_VERTEX_UNIFORM_BLOCKS'] == 0) and diff --git a/tests/integration_tests/testing/values_expected.py b/tests/integration_tests/testing/values_expected.py index 0400d6d..0e1cf44 100644 --- a/tests/integration_tests/testing/values_expected.py +++ b/tests/integration_tests/testing/values_expected.py @@ -65,14 +65,16 @@ level0 = TestedValues( timestamp={'value': 'REAL VALUE', 'accuracy': 'EXACTLY'}, device_memory={BrowserType.FIREFOX: 'REAL VALUE', - BrowserType.CHROME: 'REAL VALUE', - 'value':'REAL VALUE'}, + BrowserType.CHROME: 'REAL VALUE'}, hardware_concurrency={'value':'REAL VALUE'}, IOdevices='REAL VALUE', referrer='REAL VALUE', time={'value': 'REAL VALUE', 'accuracy': 'EXACTLY'}, - plugins='REAL VALUE', + plugins={'count': {BrowserType.FIREFOX: 'REAL VALUE', + BrowserType.CHROME: 'REAL VALUE'}, + 'value': {BrowserType.FIREFOX: 'REAL VALUE', + BrowserType.CHROME: 'REAL VALUE'}}, mimeTypes='REAL VALUE', get_channel= 'REAL VALUE', copy_channel= 'REAL VALUE', @@ -127,15 +129,18 @@ level1 = TestedValues( timestamp={'value': 'REAL VALUE', 'accuracy': 0.01}, device_memory={BrowserType.FIREFOX: None, - BrowserType.CHROME: 4, - 'value':'REAL VALUE'}, + BrowserType.CHROME: 'SPOOF VALUE', + 'valid_values': {0.25,0.5,1,2,4,8,16,32,64,128,256,512,1024}}, hardware_concurrency={'value':'SPOOF VALUE', - 'valid_values': [2]}, + 'valid_values': "UP TO REAL VALUE"}, IOdevices='REAL VALUE', referrer='REAL VALUE', time={'value': 'REAL VALUE', 'accuracy': 0.01}, - plugins='REAL VALUE', + plugins={'count': {BrowserType.FIREFOX: 'REAL VALUE', + BrowserType.CHROME: 'REAL VALUE'}, + 'value': {BrowserType.FIREFOX: 'REAL VALUE', + BrowserType.CHROME: 'REAL VALUE'}}, mimeTypes='REAL VALUE', get_channel= 'REAL VALUE', copy_channel= 'REAL VALUE', @@ -190,15 +195,18 @@ level2 = TestedValues( timestamp={'value': 'REAL VALUE', 'accuracy': 0.1}, device_memory={BrowserType.FIREFOX: None, - BrowserType.CHROME: 4, - 'value':'REAL VALUE'}, + BrowserType.CHROME: 'SPOOF VALUE', + 'valid_values': {0.25,0.5,1,2,4,8}}, hardware_concurrency={'value':'SPOOF VALUE', - 'valid_values': [2]}, - IOdevices=0, + 'valid_values': "UP TO REAL VALUE"}, + IOdevices= {0,1,2,3,4,5,6,7,8,9}, referrer='REAL VALUE', time={'value': 'REAL VALUE', 'accuracy': 0.1}, - plugins='SPOOF VALUE', + plugins={'count': {BrowserType.FIREFOX: 'REAL VALUE', + BrowserType.CHROME: 'PLUS_2'}, + 'value': {BrowserType.FIREFOX: 'EMPTY', + BrowserType.CHROME: 'SPOOF VALUE'}}, mimeTypes='SPOOF VALUE', get_channel= 'SPOOF VALUE', copy_channel= 'SPOOF VALUE', @@ -208,14 +216,14 @@ level2 = TestedValues( float_frequency= 'SPOOF VALUE', performance={'value': 'REAL VALUE', 'accuracy': 100}, - protect_canvas=True, + protect_canvas=False, canvas_imageData = 'SPOOF VALUE', canvas_dataURL = 'SPOOF VALUE', canvas_blob = 'SPOOF VALUE', - canvas_point_path = 'REAL VALUE', - canvas_point_stroke = 'REAL VALUE', - webgl_parameters = 'ZERO VALUE', - webgl_precisions = 'SPOOF VALUE', + canvas_point_path = 'SPOOF VALUE', + canvas_point_stroke = 'SPOOF VALUE', + webgl_parameters = 'SPOOF VALUE', + webgl_precisions = 'REAL VALUE', webgl_pixels = 'SPOOF VALUE', webgl_dataURL = 'SPOOF VALUE', methods_toString='REAL VALUE' @@ -243,16 +251,19 @@ level3 = TestedValues( speed={'value': "null"}, timestamp={'value': "null"}, device_memory={BrowserType.FIREFOX: None, - BrowserType.CHROME: 4, - 'value':'REAL VALUE'}, + BrowserType.CHROME: 'SPOOF VALUE', + 'valid_values': {4}}, hardware_concurrency={'value':'SPOOF VALUE', - 'valid_values': [2]}, - IOdevices=0, + 'valid_values': {2}}, + IOdevices= "EMPTY", referrer='REAL VALUE', time={'value': 'REAL VALUE', 'accuracy': 1.0}, - plugins='SPOOF VALUE', - mimeTypes='SPOOF VALUE', + plugins={'count': {BrowserType.FIREFOX: 0, + BrowserType.CHROME: 0}, + 'value': {BrowserType.FIREFOX: 'EMPTY', + BrowserType.CHROME: 'EMPTY'}}, + mimeTypes='EMPTY', get_channel= 'SPOOF VALUE', copy_channel= 'SPOOF VALUE', byte_time_domain= 'SPOOF VALUE', @@ -265,8 +276,8 @@ level3 = TestedValues( canvas_imageData = 'SPOOF VALUE', canvas_dataURL = 'SPOOF VALUE', canvas_blob = 'SPOOF VALUE', - canvas_point_path = 'REAL VALUE', - canvas_point_stroke = 'REAL VALUE', + canvas_point_path = 'FALSE VALUE', + canvas_point_stroke = 'FALSE VALUE', webgl_parameters = 'ZERO VALUE', webgl_precisions = 'SPOOF VALUE', webgl_pixels = 'SPOOF VALUE', @@ -307,11 +318,10 @@ level4 = TestedValues( timestamp={'value': 'REAL VALUE', 'accuracy': 0.1}, device_memory={BrowserType.FIREFOX: None, - BrowserType.CHROME: 4, - 'value':'SPOOF VALUE', - 'valid_values': [None,0.25,0.5,1,2,4,8]}, + BrowserType.CHROME: 'SPOOF VALUE', + 'valid_values': {0.25,0.5,1,2,4,8}}, hardware_concurrency={'value':'SPOOF VALUE', - 'valid_values': [2,3,4,5,6,7,8]}, + 'valid_values': {2,3,4,5,6,7,8}}, IOdevices=0, referrer='REAL VALUE', time={'value': 'REAL VALUE', diff --git a/tests/integration_tests/testing/values_getters.py b/tests/integration_tests/testing/values_getters.py index 8d62559..1c3f143 100644 --- a/tests/integration_tests/testing/values_getters.py +++ b/tests/integration_tests/testing/values_getters.py @@ -37,7 +37,7 @@ from configuration import get_config # Javascript is called and returned values are processed and returned. -## Get geolocation data through JST test page. +## Get geolocation data through JSR test page. # # Geolocation data is obtained asynchronously. Interaction with page is needed. # We need element on page where geolocation data is shown after its loading. @@ -175,14 +175,16 @@ def get_blob_canvas(driver, name): return img ## returns output of HTMLCanvasElement.toDataURL where name is id of chosen canvas -def get_point_in_path(driver, name): +def get_point_in_path(driver, name, alwaysFalse): try: driver.get(get_config("testing_page")) sleep(0.3) - img = driver.execute_script("var ret = true; var canvas = document.getElementById('"+name+"');var ctx = canvas.getContext('2d');" + op = "||" if alwaysFalse else "&&" + initial = "false" if alwaysFalse else "true" + img = driver.execute_script("var ret = "+initial+" ; var canvas = document.getElementById('"+name+"');var ctx = canvas.getContext('2d');" "const circle = new Path2D();circle.arc(100, 75, 50, 0, 2 * Math.PI);" "for (var i = 0; i < 200; i++) {" - "ret = ret && ctx.isPointInPath(circle, 100, 100) " + "ret = ret "+op+" ctx.isPointInPath(circle, 100, 100) " "}" "return ret") except: @@ -191,15 +193,17 @@ def get_point_in_path(driver, name): return img ## returns output of HTMLCanvasElement.toDataURL where name is id of chosen canvas -def get_point_in_stroke(driver, name): +def get_point_in_stroke(driver, name, alwaysFalse): try: driver.get(get_config("testing_page")) sleep(0.3) - img = driver.execute_script("var ret = true; var canvas = document.getElementById('"+name+"');var ctx = canvas.getContext('2d');" + op = "||" if alwaysFalse else "&&" + initial = "false" if alwaysFalse else "true" + img = driver.execute_script("var ret = "+initial+" ; var canvas = document.getElementById('"+name+"');var ctx = canvas.getContext('2d');" "const ellipse = new Path2D(); ellipse.ellipse(100, 75, 40, 60, Math.PI * .25, 0, 2 * Math.PI);" "ctx.lineWidth = 20;" "for (var i = 0; i < 200; i++) {" - "ret = ret && ctx.isPointInStroke(ellipse, 100, 25) " + "ret = ret "+op+" ctx.isPointInStroke(ellipse, 100, 25) " "}" "return ret") except: diff --git a/tests/integration_tests/testing/web_browser.py b/tests/integration_tests/testing/web_browser.py index 74800b2..5a9d324 100644 --- a/tests/integration_tests/testing/web_browser.py +++ b/tests/integration_tests/testing/web_browser.py @@ -111,7 +111,6 @@ class Browser: self.real = values_real.init(self.driver) self.driver.install_addon(get_config("firefox_jsr_extension"), temporary=True) self.find_options_jsr_page_url() - self.define_test_level() elif type == BrowserType.CHROME: driver_tmp = webdriver.Chrome(executable_path=get_config("chrome_driver")) self.real = values_real.init(driver_tmp) @@ -121,7 +120,6 @@ class Browser: options.add_extension(get_config("chrome_jsr_extension")) self.driver = webdriver.Chrome(executable_path=get_config("chrome_driver"), options=options) self.find_options_jsr_page_url() - self.define_test_level() ## Get current level of JSR in browser. @property diff --git a/tests/unit_tests/config/global-example.json b/tests/unit_tests/config/global-example.json index 0dcbd76..b1daefe 100644 --- a/tests/unit_tests/config/global-example.json +++ b/tests/unit_tests/config/global-example.json @@ -3,6 +3,7 @@ { "name": "name of the source script without file extension (.js)", "remove_custom_namespace": true "or" false, + "let_to_var": true "or" false, "src_script_requirements": [ { "type": "const or var or ...", diff --git a/tests/unit_tests/config/global-schema.json b/tests/unit_tests/config/global-schema.json index d96adac..46338ab 100644 --- a/tests/unit_tests/config/global-schema.json +++ b/tests/unit_tests/config/global-schema.json @@ -14,6 +14,9 @@ "remove_custom_namespace": { "type": "boolean" }, + "let_to_var": { + "type": "boolean" + }, "src_script_requirements": { "type": "array", "items": [ diff --git a/tests/unit_tests/config/global.json b/tests/unit_tests/config/global.json index 9763c04..05b4c70 100644 --- a/tests/unit_tests/config/global.json +++ b/tests/unit_tests/config/global.json @@ -3,6 +3,7 @@ { "name": "helpers", "remove_custom_namespace": false, + "let_to_var": false, "src_script_requirements": [ ], "test_script_requirements": [ @@ -21,39 +22,9 @@ ] }, { - "name": "browser", - "remove_custom_namespace": false, - "src_script_requirements": [ - { - "type": "const", - "requirements": [ - { - "import": "sinon-chrome", - "as": "chrome" - } - ] - } - ], - "test_script_requirements": [ - { - "type": "const", - "requirements": [ - { - "from": "./browser.js", - "objects": [ - "browser" - ] - } - ] - } - ], - "extra_exports": [ - "browser" - ] - }, - { "name": "url", "remove_custom_namespace": false, + "let_to_var": false, "src_script_requirements": [ ], "test_script_requirements": [ @@ -73,13 +44,14 @@ { "name": "levels", "remove_custom_namespace": false, + "let_to_var": false, "src_script_requirements": [ { "type": "const", "requirements": [ { "import": "sinon-chrome", - "as": "chrome" + "as": "browser" }, { "import": "navigator", @@ -154,6 +126,7 @@ { "name": "code_builders", "remove_custom_namespace": false, + "let_to_var": false, "src_script_requirements": [ { "type": "const", @@ -198,6 +171,7 @@ { "name": "wrapping", "remove_custom_namespace": false, + "let_to_var": false, "src_script_requirements": [ ], "test_script_requirements": [ @@ -220,6 +194,7 @@ { "name": "background", "remove_custom_namespace": false, + "let_to_var": false, "src_script_requirements": [ { "type": "const", @@ -232,7 +207,7 @@ }, { "import": "sinon-chrome", - "as": "chrome" + "as": "browser" } ] } @@ -257,6 +232,7 @@ { "name": "wrappingS-GEO", "remove_custom_namespace": true, + "let_to_var": true, "src_script_requirements": [ { "type": "const", @@ -287,7 +263,7 @@ ], "inject_code_to_src": { - "begin": "function gen_random32() { return 0.2 * 4294967295; }" + "begin": "function gen_random32() { return 0.2 * 4294967295; } WrapHelper = { XRAY: false, shared: {}, forPage: function(param) {return param}, isForPage: obj => pageReady.has(obj), defineProperty: Object.defineProperty, defineProperties: Object.defineProperties, create: Object.create, OriginalProxy: Proxy, Proxy: Proxy };" } } ] diff --git a/tests/unit_tests/package-lock.json b/tests/unit_tests/package-lock.json index e96c855..e139865 100644 --- a/tests/unit_tests/package-lock.json +++ b/tests/unit_tests/package-lock.json @@ -807,9 +807,9 @@ } }, "urijs": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.6.tgz", - "integrity": "sha512-eSXsXZ2jLvGWeLYlQA3Gh36BcjF+0amo92+wHPyN1mdR8Nxf75fuEuYTd9c0a+m/vhCjRK0ESlE9YNLW+E1VEw==" + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.7.tgz", + "integrity": "sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==" }, "uuid": { "version": "3.4.0", diff --git a/tests/unit_tests/start_unit_tests.sh b/tests/unit_tests/start_unit_tests.sh index 3d3f68f..8c7ac43 100755 --- a/tests/unit_tests/start_unit_tests.sh +++ b/tests/unit_tests/start_unit_tests.sh @@ -125,6 +125,13 @@ for k in $(jq '.scripts | keys | .[]' ./config/global.json); do sed -i -e "s/(function() {//" -e "s/})();//" -e "s/successCallback/return/" ./tmp/$source_script_name fi + # Modify source script - convert "let" variables to "var" variables if necessary. + let_to_var=$(jq -r '.let_to_var' <<< "$script") + if [ $let_to_var == "true" ] + then + sed -i -e "s/\ /var /" ./tmp/$source_script_name + fi + # Replace source code if required. replace_in_src=$(jq -r '.replace_in_src' <<< "$script") diff --git a/tests/unit_tests/tests/wrappingS-GEO_tests.js b/tests/unit_tests/tests/wrappingS-GEO_tests.js index 0765670..1996d5f 100644 --- a/tests/unit_tests/tests/wrappingS-GEO_tests.js +++ b/tests/unit_tests/tests/wrappingS-GEO_tests.js @@ -56,6 +56,22 @@ describe("GEO", function() { }); }); describe("processOriginalGPSDataObject", function() { + var GeolocationPosition = function() {}; + GeolocationPosition.prototype = { + coords: { + latitude: 0, + longitude: 0, + altitude: null, + accuracy: 0, + altitudeAccuracy: null, + heading: null, + speed: null, + __proto__: Object + }, + timestamp: 0, + __proto__: Object + }; + var originalPositions = { fit_vut: { coords: { @@ -308,8 +324,8 @@ describe("GEO", function() { }); it("should return given coordinates when flag provideAccurateGeolocationData is set.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = true; var original_coords; var changed_coords; @@ -323,8 +339,8 @@ describe("GEO", function() { }); it("should return changed timestamp when flag provideAccurateGeolocationData is set.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = true; var original_timestamp; var changed_timestamp; @@ -338,8 +354,8 @@ describe("GEO", function() { }); it("should return given coordinates when previouslyReturnedCoords are set.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = false; var original_coords; var changed_coords; @@ -354,8 +370,8 @@ describe("GEO", function() { }); it("should return changed timestamp when previouslyReturnedCoords are set.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = false; var original_timestamp; var changed_timestamp; @@ -370,8 +386,8 @@ describe("GEO", function() { }); it("should return coords that are not equal to original coords.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = false; previouslyReturnedCoords = undefined; var desiredAccuracyArray = [ 0.1, 1, 10, 100 ] @@ -395,8 +411,8 @@ describe("GEO", function() { // Required accuracy overview: https://github.com/polcak/jsrestrictor/blob/master/common/levels.js#L254 it("should return changed coords that are in required accuracy.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = false; previouslyReturnedCoords = undefined; var desiredAccuracyArray = [ 0.1, 1, 10, 100 ] @@ -422,8 +438,8 @@ describe("GEO", function() { }); it("should return changed timestamp.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = false; previouslyReturnedCoords = undefined; var desiredAccuracyArray = [ 0.1, 1, 10, 100 ] @@ -442,8 +458,8 @@ describe("GEO", function() { }); it("should not return nonsence coords.",function() { if (typeof processOriginalGPSDataObject !== undefined && typeof processOriginalGPSDataObject_globals !== undefined) { - eval(processOriginalGPSDataObject); eval(processOriginalGPSDataObject_globals); + eval(processOriginalGPSDataObject); provideAccurateGeolocationData = false; previouslyReturnedCoords = undefined; var desiredAccuracyArray = [ 0.1, 1, 10, 100 ]