diff options
Diffstat (limited to 'data/extensions/jsr@javascriptrestrictor/fp_detect_background.js')
-rw-r--r-- | data/extensions/jsr@javascriptrestrictor/fp_detect_background.js | 1207 |
1 files changed, 0 insertions, 1207 deletions
diff --git a/data/extensions/jsr@javascriptrestrictor/fp_detect_background.js b/data/extensions/jsr@javascriptrestrictor/fp_detect_background.js deleted file mode 100644 index 0e4e27d..0000000 --- a/data/extensions/jsr@javascriptrestrictor/fp_detect_background.js +++ /dev/null @@ -1,1207 +0,0 @@ -/** \file - * \brief Fingerprint Detector (FPD) main logic, fingerprinting evaluation and other essentials - * - * \author Copyright (C) 2021-2022 Marek Salon - * - * \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 <https://www.gnu.org/licenses/>. -// - -/** - * \defgroup FPD Fingerprint Detector - * - * \brief Fingerprint Detector (FPD) is a module that detects browser fingerprint extraction and prevents - * its sharing. To learn more about Browser Fingerprinting topic, see study "Browser Fingerprinting: A survey" available - * here: https://arxiv.org/pdf/1905.01051.pdf - * - * The FPD module uses wrapping technique to inject code that allows log API calls and accesses for every visited web page - * and its frames. Logged JS APIs can be specified in wrappers-lvl_X.json file, where X represents FPD config identifier. - * - * Fingerprint Detector is based on heuristic system that can be defined in form of API groups. A group represents - * a set of APIs that may have something in common. Access to the group is triggered when a certain amount APIs is accessed. - * Hierarchy of groups creates a tree-like structure, where access to the root group means fingerprinting activity. Groups can - * be configured in groups-lvl_X.json file, where X represents FPD config identifier. - * - * The FPD evaluate API groups with every request made in scope of certain browser tab. When FPD detects fingerprinting activity, - * blocking of subsequent requests is issued (if allowed in settings). Local browsing data of fingerprinting origin are also cleared - * to prevent caching extracted fingerprint in browser storage. - * - */ - - /** \file - * - * \brief This file is part of Fingerprint Detector (FPD) and contains API groups evaluation logic. File also contains - * event listeners used for API logging, requests blocking and tabs management. - * - * \ingroup FPD - */ - - -// START persistent configuration data - -/** - * FPD enable flag. Evaluate only when active. - */ -var fpDetectionOn; - -/** - * Associative array of hosts, that are currently among trusted ones. - */ -var fpdWhitelist = {}; - -/** - * Associative array of settings supported by this module. - */ -var fpdSettings = {}; - -// END persistent configuration data - - -/** - * Array containing names of unsupported wrappers that should be treated like supported ones during groups evaluation. - */ -const exceptionWrappers = ["CSSStyleDeclaration.prototype.fontFamily"]; - -/** - * API logs database of following structure: - * "tabId" : { - * "resource" : { - * "type" : { - * arguments { - * arg : "access count" - * }, - * total : "total access count" - * } - * } - * } - * - * *values in quotations are substituted by concrete names - * - */ -var fpdObservable = new Observable(); - -// depends on /nscl/common/CachedStorage.js -// session-bound globals -var fpdGlobals = CachedStorage.init({ - - fpDb: {}, - -/** - * Contains information about tabs current state. - */ - availableTabs: {}, - - /** - * Store if the user was already notified about fingerprinting activity in the tab. - */ - stopNotifyFlag: {}, - -/** - * A global variable shared with level_cache that controls the collection of calling scripts for FPD - * report. - */ - fpd_track_callers_tab: null, - -}); - -/** - * Stores latest evaluation statistics for every examined tab. This statistics contains data about accessed groups and resources - * and their weights after evaluation. It can be used for debugging or as an informative statement in GUI. - * It also contains flag for every tab to limit number of notifications. - */ -let latestEvals = {}; - -/** - * Parsed groups object containing necessary group information needed for evaluation. - * Groups are indexed by level and name for easier and faster access. - */ -let fpGroups = {}; - -/** - * Object containing information about unsupported wrappers for given browser. - */ -let unsupportedWrappers = {}; - -/** -* Definition of settings supported by this module. -*/ -const FPD_DEF_SETTINGS = { - behavior: { - label: browser.i18n.getMessage("fpdBehavior"), - description: browser.i18n.getMessage("fpdBehaviorDescription"), - description2: [], - params: [ - { - // 0 - short: browser.i18n.getMessage("fpdBehaviorPassive"), - description: browser.i18n.getMessage("fpdBehaviorPassiveDescription") - }, - { - // 1 - short: browser.i18n.getMessage("fpdBehaviorLimitedBlocking"), - description: browser.i18n.getMessage("fpdBehaviorBlockingDescription"), - description2: [ - browser.i18n.getMessage("fpdBehaviorBlockingDescription2"), - browser.i18n.getMessage("fpdBehaviorLimitedBlockingDescription3"), - browser.i18n.getMessage("fpdBehaviorLimitedBlockingDescription4"), - browser.i18n.getMessage("fpdBehaviorBlockingDescriptionWarning") - ] - }, - { - // 2 - short: browser.i18n.getMessage("fpdBehaviorFullBlocking"), - description: browser.i18n.getMessage("fpdBehaviorBlockingDescription"), - description2: [ - browser.i18n.getMessage("fpdBehaviorBlockingDescription2"), - browser.i18n.getMessage("fpdBehaviorFullBlockingDescription3"), - browser.i18n.getMessage("fpdBehaviorFullBlockingDescription4"), - browser.i18n.getMessage("fpdBehaviorBlockingDescriptionWarning") - ], - permissions: ["browsingData"] - } - ] - }, - notifications: { - label: browser.i18n.getMessage("shieldNotifications"), - description: browser.i18n.getMessage("fpdNotificationsDescription"), - description2: [browser.i18n.getMessage("fpdNotificationsDescription2")], - params: [ - { - // 0 - short: browser.i18n.getMessage("protectionConfigurationOptionActivatedOff"), - description: browser.i18n.getMessage("fpdNotificationsOffDescription") - }, - { - // 1 - short: browser.i18n.getMessage("protectionConfigurationOptionActivatedOn"), - description: browser.i18n.getMessage("fpdNotificationsOnDescription") - } - ] - }, - detection: { - label: browser.i18n.getMessage("fpdDetection"), - description: browser.i18n.getMessage("fpdDetectionDescription"), - description2: [], - params: [ - { - // 0 - short: browser.i18n.getMessage("fpdDetectionDefault"), - description: browser.i18n.getMessage("fpdDetectionDefaultDescription"), - description2: [ - browser.i18n.getMessage("fpdDetectionDefaultDescription2"), - browser.i18n.getMessage("fpdDetectionDefaultDescription3"), - ] - }, - { - // 1 - short: browser.i18n.getMessage("fpdStrict"), - description: browser.i18n.getMessage("fpdStrictDescription"), - description2: [ - browser.i18n.getMessage("fpdStrictDescription2"), - browser.i18n.getMessage("fpdStrictDescription3") - ] - } - ] - } -}; - - -var actionApi = browser.browserAction || browser.action; -// unify default color of popup badge background between different browsers -actionApi.setBadgeBackgroundColor({color: "#6E7378"}); - -// unify default color of popup badge text between different browsers -if (typeof actionApi.setBadgeTextColor === "function") { - actionApi.setBadgeTextColor({color: "#FFFFFF"}); -} - -/** - * This function initializes FPD module, loads configuration from storage, and registers listeners needed for fingerprinting detection. - */ -async function initFpd() { - // fill up fpGroups object with necessary data for evaluation - for (let groupsLevel in fp_levels.groups) { - fpGroups[groupsLevel] = fpGroups[groupsLevel] || {}; - processGroupsRecursive(fp_levels.groups[groupsLevel], groupsLevel); - - // Translate severity names - for (record of fp_levels.groups[groupsLevel].severity) { - record[1] = browser.i18n.getMessage(record[1]); - } - } - - // load configuration and settings from storage - await fpdLoadConfiguration(); - await fpdGlobals; - - // take care of unsupported resources for cross-browser behaviour uniformity - balanceUnsupportedWrappers(); - - // listen for messages intended for FPD module - browser.runtime.onMessage.addListener(fpdCommonMessageListener); - - // listen for click on notification when FPD detects fingerprinting to create a report - browser.notifications.onClicked.addListener((notificationId) => { - if (notificationId.startsWith("fpd")) { - var tabId = notificationId.split("-")[1]; - generateFpdReport(tabId); - } - }); - - // listen for update of browser tabs and start periodic FP evaluation - browser.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { - availableTabs[tabId] = tab; - if (changeInfo.status == "loading") { - refreshDb(tabId); - fpDb[tab.id] = {}; - periodicEvaluation(tab.id, 500); - } - }); - - // listen for removal of browser tabs to clean fpDb object - browser.tabs.onRemoved.addListener(function (tabId) { - refreshDb(tabId); - delete availableTabs[tabId]; - }); - - // listen for removal of optional permissions to adjust settings accordingly - browser.permissions.onRemoved.addListener((permissions) => { - correctSettingsForRemovedPermissions(permissions.permissions, fpdSettings, FPD_DEF_SETTINGS); - browser.storage.sync.set({"fpdSettings": fpdSettings}); - }); - - if (self.window) { - // Firefox, event page has window - // listen for requests through webRequest API and decide whether to block them - browser.webRequest.onBeforeRequest.addListener( - fpdRequestCancel, - {urls: ["<all_urls>"]}, - ["blocking"] - ); - } else { - // mv3: cannot block! - // hide behavior settings - // TODO: shame Google in the setting panels for the missing feature - delete FPD_DEF_SETTINGS["behavior"]; - // force passive behavior setting - fpdSettings.behavior = 0; - browser.storage.sync.set({"fpdSettings": fpdSettings}); - } - // listen for navigation events to initiate storage clearing of fingerprinting web pages - browser.webNavigation.onBeforeNavigate.addListener((details) => { - if (latestEvals[details.tabId] && stopNotifyFlag[details.tabId] && fpdSettings.behavior > 0) { - // clear storages (using content script) for every frame in this tab - if (details.tabId >= 0) { - browser.tabs.sendMessage(details.tabId, { - cleanStorage: true, - ignoreWorkaround: fpdSettings.behavior > 1 - }); - } - } - }); - - // get state of all existing tabs and start periodic FP evaluation - browser.tabs.query({}).then(function(results) { - results.forEach(function(tab) { - availableTabs[tab.id] = tab; - fpDb[tab.id] ||= {}; - periodicEvaluation(tab.id, 500); - }); - }); -} - -/* - * -------------------------------------------------- - * CRITERIA CORRECTION - * -------------------------------------------------- - */ - -/** - * This function provides ability to balance/optimalize evaluation heuristics for cross-browser behaviour uniformity. - * It checks which resources are unsupported for given browser and adjust criteria of loaded FPD configuration accordingly. - */ -function balanceUnsupportedWrappers() { - if (!self.window) return false; // mv3 service worker? - // object containing groups effected by criteria adjustment and corresponding deleted subgroups - var effectedGroups = {}; - - // check supported wrappers for each level of FPD configuration - for (let level in fp_levels.wrappers) { - unsupportedWrappers[level] = []; - effectedGroups = {}; - - // find out which wrappers are unsupported and store them in "unsupportedWrappers" object - for (let wrapper of fp_levels.wrappers[level]) { - // split path of object from its name - var resourceSplitted = split_resource(wrapper.resource); - - // access nested object in browser's "window" object using path string - var resolvedPath = resourceSplitted["path"].split('.').reduce((o, p) => o ? o[p] : undefined, window); - - // if resource or resource path is undefined -> resource unsupported && no exception for the resource - if (!(resolvedPath && resourceSplitted["name"] in resolvedPath) && !exceptionWrappers.includes(wrapper.resource)) { - // store wrapper object to "unsupportedWrappers" object - unsupportedWrappers[level].push(wrapper); - - // mark all groups as effected if containing unsupported resource - for (let groupObj of wrapper.groups) { - effectedGroups[groupObj.group] = effectedGroups[groupObj.group] || []; - } - - // remove wrapper from FPD configuration (in "fp_levels") - fp_levels.wrappers[level] = fp_levels.wrappers[level].filter((x) => (x != wrapper)); - } - } - - // adjust/correct effected groups criteria - correctGroupCriteria(fp_levels.groups[level], effectedGroups, level); - } - - // refresh "fpGroups" object after criteria adjustment for upcoming evaluations - for (let groupsLevel in fp_levels.groups) { - fpGroups[groupsLevel] = {}; - processGroupsRecursive(fp_levels.groups[groupsLevel], groupsLevel); - } -} - -/** - * The function that corrects groups criteria according to unsupported wrappers. - * Groups should be deleted when more than half of weights (resources) cannot be obtained in the same group (whole group is invalidated). - * Groups criteria of "value" type are also recomputed to take into account unsupported resources and deleted groups/subgroups. - * - * \param rootGroup Group object of certain level from FPD configuration (in "fp_levels.wrappers" format). - * \param effectedGroups Object containing groups effected by criteria adjustment and corresponding deleted subgroups. - * \param level FPD configuration level identifier. - * - * \returns true if group should be deleted because of unsupported wrappers - * \returns false if group don't need to be deleted - */ -function correctGroupCriteria(rootGroup, effectedGroups, level) { - // if rootGroup has subgroups, then process all subgroups recursively - if (rootGroup.groups) { - for (let groupIdx in rootGroup.groups) { - // if correctGroupCriteria return true, subgroup is deleted - if (correctGroupCriteria(rootGroup.groups[groupIdx], effectedGroups, level)) { - // rootGroup is now also effected and added to "effectedGroups" with deleted subgroup - effectedGroups[rootGroup.name] = effectedGroups[rootGroup.name] || []; - effectedGroups[rootGroup.name].push(rootGroup.groups[groupIdx].name); - } - - // rootGroup is effected because at least one of its child was effected too - if (Object.keys(effectedGroups).includes(rootGroup.groups[groupIdx].name)) { - effectedGroups[rootGroup.name] = effectedGroups[rootGroup.name] || []; - } - } - - // remove deleted subgroups from original group object - if (effectedGroups[rootGroup.name]) { - rootGroup.groups = rootGroup.groups.filter((x) => (!effectedGroups[rootGroup.name].includes(x.name))) - } - } - - // if group is effected, try to correct its criteria - if (Object.keys(effectedGroups).includes(rootGroup.name)) { - // original sum of max weights of direct children - var maxOriginalWeight = 0; - - // sum of max weights after deletion of unsupported wrappers - var maxNewWeight = 0; - - // get values of "maxOriginalWeight" and "maxNewWeight" - for (let resource in fpGroups[level][rootGroup.name].items) { - // if resource is subgroup - if (fpGroups[level][rootGroup.name].items[resource] == "group") { - let maxWeight = fpGroups[level][resource].criteria.reduce((x, {weight}) => (x > weight ? x : weight), 0); - - maxOriginalWeight += maxWeight; - maxNewWeight += !effectedGroups[rootGroup.name].includes(resource) ? maxWeight : 0; - } - // if resource is property or function - else { - // specific resource object from FPD configuration of wrappers - var resourceObj; - - // array of groups where resource can be found - var groupsArray; - - // flag - resource is supported (resource found in "fp_levels.wrappers" instead of "unsupportedWrappers") - var supported = false; - - // get resource from wrappers and extract all group objects in context of parent group (rootGroup) - if (resourceObj = fp_levels.wrappers[level].filter((x) => (x.resource == resource))[0]) { - groupsArray = resourceObj.groups.filter((x) => (x.group == rootGroup.name)); - supported = true; - } - else { - resourceObj = unsupportedWrappers[level].filter((x) => (x.resource == resource))[0]; - groupsArray = resourceObj.groups.filter((x) => (x.group == rootGroup.name)); - } - - // get maximal obtainable weight for resource from every group object - for (let groupObj of groupsArray) { - if (groupObj.criteria && groupObj.criteria.length > 0) { - let maxWeight = groupObj.criteria.reduce((x, {weight}) => (x > weight ? x : weight), 0); - - maxOriginalWeight += maxWeight; - maxNewWeight += supported ? maxWeight : 0; - } - else { - maxOriginalWeight += 1; - maxNewWeight += supported ? 1 : 0; - } - } - } - } - - // adjust "value" criteria to follow their original percentage state - for (let criterion of fpGroups[level][rootGroup.name].criteria) { - if ("value" in criterion) { - // original percentage of given criterion - var percValue = (criterion.value*100) / maxOriginalWeight; - - // get new criterion value adjusted for new max weights - criterion.value = Math.round((percValue*maxNewWeight) / 100); - } - } - - // if sum of max weights of unsupported resources is higher than 50% of total original weights - // in this case, group should be deleted or manually corrected in FPD configuration to contain only supported resources - if (2*maxNewWeight < maxOriginalWeight) { - return true; - } - return false; - } -} - -/* - * -------------------------------------------------- - * GROUPS EVALUATION - * -------------------------------------------------- - */ - -/** - * The function initializing evaluation of logged API calls (fpDb) according to groups criteria (fpGroups). - * - * \param tabId Integer number representing ID of browser tab that is going to be evaluated. - * - * \returns object where "weight" property represents evaluated weight of root group and "severity" property contain severity array - */ -function evaluateGroups(tabId) { - // get url of evaluated tab - var url = availableTabs[tabId] ? availableTabs[tabId].url : ""; - var ret = { - weight: 0, - severity: [] - } - - // inaccesible or invalid url - do not evaluate - if (!url) { - return ret; - } - - // clear old evalStats - latestEvals[tabId] = latestEvals[tabId] || {}; - latestEvals[tabId].evalStats = []; - - // check if the level exists within FPD configuration, if not use default FPD configuration - level = fpdSettings.detection ? fpdSettings.detection : 0; - - // getting root group name as a start point for recursive evaluation - var rootGroup = fp_levels.groups[level] ? fp_levels.groups[level].name : undefined; - - // start recursive evaluation if all needed objects are defined - if (rootGroup && fpGroups[level] && fp_levels.wrappers[level]) { - let evalRes = evaluateGroupsCriteria(rootGroup, level, tabId)[0]; - ret.weight = evalRes.actualWeight; - if (fp_levels.groups[level].severity) { - let sortedSeverity = fp_levels.groups[level].severity.sort((x, y) => x[0] - y[0]); - ret.severity = sortedSeverity.filter((x) => (x[0] <= evalRes.actualWeightsSum)).reverse()[0]; - } - } - - return ret; -} - -/** - * The function that evaluates group criteria according to evaluation of its child items (groups/resources). - * - * \param rootGroup Group name that needs to be evaluated. - * \param level Level ID of groups and wrappers used for evaluation. - * \param tabId Integer number representing ID of evaluated browser tab. - * - * \returns Array that contains "Result" objects - * - * Result object contains following properties: - * actualWeight (Obtained weight value of group after evaluation) - * maxWeight (Maximum obtainable weight value of group) - * type (Type of group item - group/call/get/set) - * accesses (Number of accesses to specified resource - groups always 0) - */ -function evaluateGroupsCriteria(rootGroup, level, tabId) { - // result object that is delegated to parent group - var res = {}; - - // all result objects from child items of rootGroup - var scores = []; - - // array of relevant criteria based on groupTypes - var relevantCriteria = []; - - // types of criteria that are relevant for evaluating rootGroup - var groupTypes = []; - - // evaluate every item of rootGroup and add result objects to scores array - for (let item in fpGroups[level][rootGroup].items) { - if (fpGroups[level][rootGroup].items[item] == "group") { - scores = scores.concat(evaluateGroupsCriteria(item, level, tabId)); - } - else { - scores = scores.concat(evaluateResourcesCriteria(item, rootGroup ,level, tabId)); - } - } - - /* - Group type is determined by first criteria object: - - access - evaluation of child items is based on sum of accesses - - value/percentage - evaluation of child items is based on sum of weights or percentage of sum of maxWeights - */ - groupTypes = Object.keys(fpGroups[level][rootGroup].criteria[0]).includes("access") ? ["access"] : ["value", "percentage"]; - relevantCriteria = fpGroups[level][rootGroup].criteria.filter((x) => (groupTypes.some((y) => (Object.keys(x).includes(y))))); - - // now evaluating group - res.type = "group"; - - // get maximal obtainable weight for rootGroup - res.maxWeight = fpGroups[level][rootGroup].criteria.reduce((x, {weight}) => (x > weight ? x : weight), 0); - - // compute actualWeight of rootGroup with value of accesses - res.accesses = 0; - res.actualWeight = 0; - res.actualWeightsSum = 0; - if (groupTypes.length == 2) { - // groupTypes contains "value" and "percetange" - take weight of child items into account - var actualWeightsSum = scores.reduce((x, {actualWeight}) => (x + actualWeight), 0); - var maxWeightsSum = scores.reduce((x, {maxWeight}) => (x + maxWeight), 0); - - // recalculate percentage values of relevant criteria to exact values - var relativeCriteria = []; - for (let criteriaObj of relevantCriteria) { - if (criteriaObj.value) { - relativeCriteria.push(criteriaObj); - } - else { - relativeCriteria.push({ - value: Math.round(maxWeightsSum * (criteriaObj.percentage/100)), - weight: criteriaObj.weight - }); - } - } - - // sort relevant and relative criteria by value - relativeCriteria.sort((x, y) => (x.value - y.value)); - - // filter criteria and take weight of highest achieved criteria - var filteredCriteria = relativeCriteria.filter((x) => (x.value <= actualWeightsSum)).reverse()[0]; - res.actualWeight = filteredCriteria ? filteredCriteria.weight : 0; - res.actualWeightsSum = actualWeightsSum; - } - else { - // groupTypes contains "access" - take access of child items into account - var accessesSum = scores.reduce((x, {accesses}) => (x + accesses), 0); - - // sort relevant criteria - relevantCriteria.sort((x, y) => (x.access - y.access)); - - // filter criteria and take weight of highest achieved criteria - var filteredCriteria = relevantCriteria.filter((x) => (x.access <= accessesSum)).reverse()[0]; - res.actualWeight = filteredCriteria ? filteredCriteria.weight : 0; - res.actualWeightsSum = accessesSum; - } - - // update group statistics in latestEvals - latestEvals[tabId].evalStats.push({ - title: rootGroup, - type: "group", - weight: res.actualWeight, - sum: res.actualWeightsSum - }); - - return [res]; -} - -/** - * The function that evaluates resource (wrapper) criteria according to API calls logs. - * - * \param resource Full name of resource/wrapper. - * \param groupName Name of direct parent group. - * \param level Level ID of groups and wrappers used for evaluation. - * \param tabId Integer number representing ID of evaluated browser tab. - * - * \returns Array that contains "Result" objects - * - * Result object contains following properties (all of them in context of parent group): - * actualWeight (Obtained weight value of resource after evaluation) - * maxWeight (Maximum obtainable weight value of resource) - * type (Type of resource - call/get/set) - * accesses (Number of accesses to specified resource) - */ -function evaluateResourcesCriteria(resource, groupName, level, tabId) { - // all result objects for given resource (set/get/call) - var scores = []; - - // get resource from wrappers and extract all group objects in context of parent group (groupName) - var resourceObj = fp_levels.wrappers[level].filter((x) => (x.resource == resource))[0]; - var groupsArray = resourceObj.groups.filter((x) => (x.group == groupName)); - - // evaluate every retrieved group object - for (let groupObj of groupsArray) { - // initialize new result object - var res = {} - - // get resource type from group object (get/set/call) - if (resourceObj.type == "property") { - if (groupObj.property) { - res.type = groupObj.property - } - else { - // property not defined => implicit get - res.type = "get"; - } - } - else { - res.type = "call" - } - - // get maximal obtainable weight for resource from group object - if (groupObj.criteria && groupObj.criteria.length > 0) { - res.maxWeight = groupObj.criteria.reduce((x, {weight}) => (x > weight ? x : weight), 0); - } - else { - // criteria not defined => set implicit criteria - res.maxWeight = 1; - groupObj.criteria = [{value: 1, weight: 1}]; - } - - // compute actualWeight of resource in context of parent group from logs located in fpDb object - res.actualWeight = 0; - if (fpDb[tabId] && fpDb[tabId][resource] && fpDb[tabId][resource][res.type]) { - let record = fpDb[tabId][resource][res.type]; - // logs for given resource and type exist - if (groupObj.arguments) { - // if arguments logging is defined, evaluate resource accordingly - - if (groupObj.arguments == "diff") { - // "diff" - accesses depend on number of different arguments - res.accesses = Object.keys(record.args).length; - } - else if (groupObj.arguments == "same") { - // "same" - accesses depend on maximum number of same arguments calls - res.accesses = Object.values(record.args).reduce((x, y) => x > y ? x : y); - } - else { - // try to interpret arguments as regular expression and take accesses that match this expression - try { - let re = new RegExp(...groupObj.arguments); - res.accesses = Object.keys(record.args).reduce( - (x, y) => (re.test(y) ? x + record.args[y] : x), 0); - } catch { - res.accesses = 0; - } - } - } - else { - // arguments logging not defined, simply take total accesses to resource - res.accesses = record.total - } - - // sort criteria by value - groupObj.criteria.sort((x, y) => (x.value - y.value)); - - // filter criteria and take weight of highest achieved criteria - var filteredCriteria = groupObj.criteria.filter((x) => (x.value <= res.accesses)).reverse()[0]; - res.actualWeight = filteredCriteria ? filteredCriteria.weight : 0; - } - else { - // no logs of given criteria - res.accesses = 0; - } - scores.push(res) - } - - // update resource statistics in latestEvals - scores.forEach(function (res) { - latestEvals[tabId].evalStats.push({ - title: resource, - type: "resource", - resource: res.type, - group: groupName, - weight: res.actualWeight, - accesses: res.accesses - }); - }); - - return scores; -} - -/* - * -------------------------------------------------- - * EVENT HANDLERS - * -------------------------------------------------- - */ - -/** - * Callback function that parses and handles messages recieved by FPD module. - * Messages may contain wrappers logging data that are stored into fpDb object. - * Also listen for popup messages to update FPD state and whitelist. - * - * \param message Receives full message. - * \param sender Sender of the message. - */ -async function fpdCommonMessageListener(record, sender) { - if (!record) return; - await fpdGlobals; - try { - switch (record.purpose) { - case "fp-detection": - // check objects existance => if do not exist, create new one - fpDb[sender.tab.id] = fpDb[sender.tab.id] || {}; - fpDb[sender.tab.id][record.resource] = fpDb[sender.tab.id][record.resource] || {}; - fpDb[sender.tab.id][record.resource][record.type] = fpDb[sender.tab.id][record.resource][record.type] || {}; - - // object that contains access counters - const fpCounterObj = fpDb[sender.tab.id][record.resource][record.type]; - const argsStr = record.args.join(); - fpCounterObj["args"] = fpCounterObj["args"] || {}; - - // increase counter for accessed arguments - fpCounterObj["args"][argsStr] = fpCounterObj["args"][argsStr] || 0; - fpCounterObj["args"][argsStr] += 1; - - // increase counter for total accesses - fpCounterObj["total"] = fpCounterObj["total"] || 0; - fpCounterObj["total"] += 1; - fpdObservable.update(record.resource, sender.tab.id, record.type, fpCounterObj["total"]); - - // Track callers - fpCounterObj["callers"] = fpCounterObj["callers"] || {}; - if (record.stack !== undefined) { - fpCounterObj["callers"][record.stack] = true; - } - break; - case "fpd-state-change": - browser.storage.sync.get(["fpDetectionOn"]).then(function(result) { - fpDetectionOn = result.fpDetectionOn; - }); - break; - case "fpd-whitelist-check": { - // answer to popup, when asking whether is the site whitelisted - return Promise.resolve(isFpdWhitelisted(record.url)); - } - case "add-fpd-whitelist": - // obtain current hostname and whitelist it - var currentHost = record.url; - fpdWhitelist[currentHost] = true; - browser.storage.sync.set({"fpdWhitelist": fpdWhitelist}); - break; - case "remove-fpd-whitelist": - // obtain current hostname and remove it form whitelist - var currentHost = record.url; - delete fpdWhitelist[currentHost]; - browser.storage.sync.set({"fpdWhitelist": fpdWhitelist}); - break; - case "update-fpd-whitelist": - // update current fpdWhitelist from storage - browser.storage.sync.get(["fpdWhitelist"]).then(function(result) { - fpdWhitelist = result.fpdWhitelist; - }); - break; - case "fpd-get-report-data": { - // get current FPD level for evaluated tab - if (record.tabId) { - level = fpdSettings.detection ? fpdSettings.detection : 0; - return Promise.resolve({ - tabObj: availableTabs[record.tabId], - groups: {root: fp_levels.groups[level].name, all: fpGroups[level]}, - fpDb: fpDb[record.tabId], - latestEvals: latestEvals[record.tabId], - exceptionWrappers: exceptionWrappers - }); - } - } - case "fpd-create-report": - // create FPD report for the tab - if (record.tabId) { - generateFpdReport(record.tabId); - } - break; - case "fpd-fetch-severity": { - // send severity value of the latest evaluation - let severity = []; - if (record.tabId && isFpdOn(record.tabId) && latestEvals[record.tabId]) { - severity = latestEvals[record.tabId].severity; - } - return Promise.resolve(severity); - } - case "fpd-get-settings": { - // send settings definition and current values - return Promise.resolve({ - def: FPD_DEF_SETTINGS, - val: fpdSettings - }); - } - case "fpd-set-settings": - // update current settings - fpdSettings[record.id] = record.value; - browser.storage.sync.set({"fpdSettings": fpdSettings}); - break; - case "fpd-load-config": - // load current configuration - fpdLoadConfiguration(); - break; - case "fpd-clear-storage": - // clear browser storage for the origin - clearStorageFacilities(record.url); - break; - case "fpd-fetch-hits": { - let {tabId} = record; - // filter by tabId; - let hits = Object.create(null); - if (fpDb[tabId]) { - for ([resource, recordObj] of Object.entries(fpDb[tabId])) { - let total = 0; - for (let stat of Object.values(recordObj)) { // by type - total += stat.total; - } - let group_name = wrapping_groups.wrapper_map[resource]; - if (group_name) { - get_or_create(hits, group_name, 0); - hits[group_name] += total; - } - } - } - return Promise.resolve(hits); - } - case "fpd-track-callers": { - let tabId = Number(record.tabId); - fpd_track_callers_tab = tabId; - return browser.tabs.reload(tabId); - } - case "fpd-track-callers-stop": { - fpd_track_callers_tab = undefined; - } - } - } catch (e) { - console.error(e, "Error processing", record); - throw e; - } finally { - CachedStorage.save(); - } -} - -/** - * The function that makes decisions about requests blocking. If blocking enabled, also clear browsing data. - * - * \param requestDetails Details about the request. - * - * \returns Object containing key "cancel" with value true if request is blocked, otherwise with value false - */ -function fpdRequestCancel(requestDetails) { - - // chrome fires onBeforeRequest event before tabs.onUpdated => refreshDb won't happen in time - // need to refreshDb when main_frame request occur, otherwise also user's requests will be blocked - if (requestDetails.type == "main_frame") { - refreshDb(requestDetails.tabId); - } - - return evaluateFingerprinting(requestDetails.tabId) -} - -/* - * -------------------------------------------------- - * MISCELLANEOUS - * -------------------------------------------------- - */ - -/** - * The function that loads module configuration from sync storage. - */ -async function fpdLoadConfiguration() { - ({fpDetectionOn, fpdWhitelist, fpdSettings} = await browser.storage.sync.get({ - fpDetectionOn: false, - fpdWhitelist: {}, - fpdSettings: {}, - })); -} - -/** - * The function transforming recursive groups definition into indexed fpGroups object. - * - * \param input Group object from loaded JSON format. - * \param groupsLevel Level ID of groups to process. - */ -function processGroupsRecursive(input, groupsLevel) { - fpGroups[groupsLevel][input.name] = {}; - fpGroups[groupsLevel][input.name]["description"] = input["description"] || ""; - - // criteria missing => set implicit criteria - fpGroups[groupsLevel][input.name]["criteria"] = input["criteria"] || [{value:1, weight:1}]; - fpGroups[groupsLevel][input.name]["items"] = {}; - - // retrieve associated resources (wrappers) for input group - for (let resourceObj of fp_levels.wrappers[groupsLevel]) { - if (resourceObj.groups.filter((x) => (x.group == input.name)).length > 0) { - fpGroups[groupsLevel][input.name]["items"][resourceObj.resource] = resourceObj.type; - } - } - - // retrieve associated sub-groups for given input group and process them recursively - if (input["groups"]) { - for (let groupObj of input["groups"]) { - fpGroups[groupsLevel][input.name]["items"][groupObj.name] = "group"; - processGroupsRecursive(groupObj, groupsLevel); - } - } -} - -/** - * Check if the hostname or any of it's domains is whitelisted. - * - * \param hostname Any hostname (subdomains allowed). - * - * \returns TRUE when domain (or subdomain) is whitelisted, FALSE otherwise. - */ -function isFpdWhitelisted(hostname) { - var domains = extractSubDomains(hostname); - for (var domain of domains) { - if (fpdWhitelist[domain] != undefined) { - return true; - } - } - return false; -} - -/** - * The function that returns FPD setting for given url. - * - * \param tabId Tab identifier for which FPD setting is needed. - * - * \returns Boolean value TRUE if FPD is on, otherwise FALSE. - */ -function isFpdOn(tabId) { - if (!availableTabs[tabId]) { - return false; - } - let url = getEffectiveDomain(availableTabs[tabId].url); - if (fpDetectionOn && !isFpdWhitelisted(url)) { - return true; - } - return false; -} - -/** - * The function that creates notification and informs user about fingerprinting activity. - * - * \param tabId Integer number representing ID of suspicious browser tab. - */ -function notifyFingerprintBlocking(tabId) { - let msg; - if (fpdSettings.behavior > 0) { - msg = browser.i18n.getMessage("fpdBlockingSubsequent"); - } - if (fpdSettings.behavior == 0) { - msg = browser.i18n.getMessage("fpdClickNotificationDetails"); - } - - browser.notifications.create("fpd-" + tabId, { - type: "basic", - iconUrl: browser.runtime.getURL("img/icon-48.png"), - title: browser.i18n.getMessage("fpdNotificationTitle"), - message: browser.i18n.getMessage("fpdNotificationMessage", - [ - msg, - availableTabs[tabId].title.slice(0, 30), - getEffectiveDomain(availableTabs[tabId].url) - ]) - }); - setTimeout(() => { - browser.notifications.clear("fpd-" + tabId); - }, 6000); -} - -/** - * The function that generates a report about fingerprinting evaluation in a separate window. - * - * \param tabId Integer number representing ID of evaluated browser tab. - */ -function generateFpdReport(tabId) { - // open popup window containing FPD report - browser.windows.create({ - url: "/fp_report.html?id=" + tabId, - type: "popup", - height: 600, - width: 800 - }); -} - -/** - * Clear all stored logs for a tab. - * - * \param tabId Integer number representing ID of browser tab. - */ -function refreshDb(tabId) { - if (fpDb[tabId]) { - delete fpDb[tabId]; - } - if (latestEvals[tabId]) { - delete latestEvals[tabId]; - } - if (availableTabs[tabId] && availableTabs[tabId].timerId) { - clearTimeout(availableTabs[tabId].timerId); - } - CachedStorage.save(); -} - -/** - * The function that periodically starts fingerprinting evaluation without the need for a request. - * Delay is increased exponentially and doubles in every call. - * - * \param tabId Integer number representing ID of browser tab. - * \param delay Initial value of a delay in milliseconds. - */ -function periodicEvaluation(tabId, delay) { - evaluateFingerprinting(tabId); - if (availableTabs[tabId]) { - // limit max delay to 90s per tab - availableTabs[tabId].timerId = setTimeout(periodicEvaluation, delay, tabId, delay > 90000 ? delay : delay*2); - } -} - -/** - * The function that starts evaluation process and if fingerprinting is detected, it reacts accordingly. - * - * \param tabId Integer number representing ID of evaluated browser tab. - * - * \returns Object containing key "cancel" with value true if request is blocked, otherwise with value false - */ -function evaluateFingerprinting(tabId) { - // if FPD enabled for the site continue with FP evaluation - if (isFpdOn(tabId)) { - - // start FP evaluation process and store result array - var evalResult = evaluateGroups(tabId); - - // store latest severity value after evaluation of given tab - if (evalResult.severity) { - latestEvals[tabId].severity = evalResult.severity; - } - - // modify color of action icon - if (evalResult.severity[2]) { - actionApi.setBadgeBackgroundColor({color: evalResult.severity[2], tabId: tabId}); - } - - // if actualWeight of root group is higher than 0 => reactive phase and applying measures - if (evalResult.weight) { - - // create notification for user if behavior is "notification" or higher (only once for every tab load) - if (fpdSettings.notifications == 1 && !stopNotifyFlag[tabId]) { - notifyFingerprintBlocking(tabId); - stopNotifyFlag[tabId] = true; - CachedStorage.save(); - } - - // block request and clear cache data only if "blocking" behavior is set - if (fpdSettings.behavior > 0) { - - // clear storages (using content script) for every frame in this tab - if (tabId >= 0) { - browser.tabs.sendMessage(tabId, { - cleanStorage: true, - ignoreWorkaround: fpdSettings.behavior > 1 - }); - } - - return { - cancel: true - }; - } - } - } - - return { - cancel: false - }; -} - -/** - * The function that clears all supported browser storage mechanisms for a given origin. - * - * \param url URL address of the origin. - */ -function clearStorageFacilities(url) { - // clear all browsing data for origin of tab url to prevent fingerprint caching - if (url && fpdSettings.behavior > 1) { - try { - // "origins" key only supported by Chromium browsers - browser.browsingData.remove({ - "origins": [new URL(url).origin] - }, { - "appcache": true, - "cache": true, - "cacheStorage": true, - "cookies": true, - "fileSystems": true, - "indexedDB": true, - "localStorage": true, - "serviceWorkers": true, - "webSQL": true - }); - } - catch (e) { - // need to use "hostnames" key for Firefox - if (e.message.includes("origins")) { - browser.browsingData.remove({ - "hostnames": extractSubDomains(new URL(url).hostname).filter((x) => (x.includes("."))) - }, { - "cache": true, - "cookies": true, - "indexedDB": true, - "localStorage": true, - "serviceWorkers": true - }); - } - else { - throw e; - } - } - } -} - -/** - * The function splitting resource string into path and name. - * For example: "window.navigator.userAgent" => path: "window.navigator", name: "userAgent" - * - * \param wrappers text String representing full name of resource. - * - * \returns Object consisting of two properties (path, name) for given resource. - */ -function split_resource(text) { - var index = text.lastIndexOf('.'); - return { - path: text.slice(0, index), - name: text.slice(index + 1) - } -} |