|
53 | 53 | var contentScopeFeatures = (function (exports) {
|
54 | 54 | 'use strict';
|
55 | 55 |
|
56 |
| - const sjcl = (() => { |
| 56 | + // @ts-nocheck |
| 57 | + const sjcl = (() => { |
57 | 58 | /*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
|
58 | 59 | /*global document, window, escape, unescape, module, require, Uint32Array */
|
59 | 60 |
|
|
851 | 852 | })
|
852 | 853 | }
|
853 | 854 |
|
| 855 | + /** |
| 856 | + * @param {string} featureName |
| 857 | + * @param {object} args |
| 858 | + * @param {string} prop |
| 859 | + * @returns {any} |
| 860 | + */ |
| 861 | + function getFeatureSetting (featureName, args, prop) { |
| 862 | + const camelFeatureName = camelcase(featureName); |
| 863 | + return args.featureSettings?.[camelFeatureName]?.[prop] |
| 864 | + } |
| 865 | + |
| 866 | + /** |
| 867 | + * @param {string} featureName |
| 868 | + * @param {object} args |
| 869 | + * @param {string} prop |
| 870 | + * @returns {boolean} |
| 871 | + */ |
| 872 | + function getFeatureSettingEnabled (featureName, args, prop) { |
| 873 | + const result = getFeatureSetting(featureName, args, prop); |
| 874 | + return result === 'enabled' |
| 875 | + } |
| 876 | + |
| 877 | + /** |
| 878 | + * @template {object} P |
| 879 | + * @typedef {object} ProxyObject<P> |
| 880 | + * @property {(target?: object, thisArg?: P, args?: object) => void} apply |
| 881 | + */ |
| 882 | + |
| 883 | + /** |
| 884 | + * @template [P=object] |
| 885 | + */ |
854 | 886 | class DDGProxy {
|
| 887 | + /** |
| 888 | + * @param {string} featureName |
| 889 | + * @param {P} objectScope |
| 890 | + * @param {string} property |
| 891 | + * @param {ProxyObject<P>} proxyObject |
| 892 | + */ |
855 | 893 | constructor (featureName, objectScope, property, proxyObject) {
|
856 | 894 | this.objectScope = objectScope;
|
857 | 895 | this.property = property;
|
|
2119 | 2157 |
|
2120 | 2158 | var seedrandom = sr;
|
2121 | 2159 |
|
2122 |
| - function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageDataProxy) { |
2123 |
| - const ctx = canvas.getContext('2d'); |
2124 |
| - // We *always* compute the random pixels on the complete pixel set, then pass back the subset later |
2125 |
| - let imageData = getImageDataProxy._native.apply(ctx, [0, 0, canvas.width, canvas.height]); |
2126 |
| - imageData = modifyPixelData(imageData, sessionKey, domainKey, canvas.width); |
| 2160 | + /** |
| 2161 | + * @param {HTMLCanvasElement} canvas |
| 2162 | + * @param {string} domainKey |
| 2163 | + * @param {string} sessionKey |
| 2164 | + * @param {any} getImageDataProxy |
| 2165 | + * @param {CanvasRenderingContext2D | WebGL2RenderingContext | WebGLRenderingContext} ctx? |
| 2166 | + */ |
| 2167 | + function computeOffScreenCanvas (canvas, domainKey, sessionKey, getImageDataProxy, ctx) { |
| 2168 | + if (!ctx) { |
| 2169 | + ctx = canvas.getContext('2d'); |
| 2170 | + } |
2127 | 2171 |
|
2128 | 2172 | // Make a off-screen canvas and put the data there
|
2129 | 2173 | const offScreenCanvas = document.createElement('canvas');
|
2130 | 2174 | offScreenCanvas.width = canvas.width;
|
2131 | 2175 | offScreenCanvas.height = canvas.height;
|
2132 | 2176 | const offScreenCtx = offScreenCanvas.getContext('2d');
|
| 2177 | + |
| 2178 | + let rasterizedCtx = ctx; |
| 2179 | + // If we're not a 2d canvas we need to rasterise first into 2d |
| 2180 | + const rasterizeToCanvas = !(ctx instanceof CanvasRenderingContext2D); |
| 2181 | + if (rasterizeToCanvas) { |
| 2182 | + rasterizedCtx = offScreenCtx; |
| 2183 | + offScreenCtx.drawImage(canvas, 0, 0); |
| 2184 | + } |
| 2185 | + |
| 2186 | + // We *always* compute the random pixels on the complete pixel set, then pass back the subset later |
| 2187 | + let imageData = getImageDataProxy._native.apply(rasterizedCtx, [0, 0, canvas.width, canvas.height]); |
| 2188 | + imageData = modifyPixelData(imageData, sessionKey, domainKey, canvas.width); |
| 2189 | + |
| 2190 | + if (rasterizeToCanvas) { |
| 2191 | + clearCanvas(offScreenCtx); |
| 2192 | + } |
| 2193 | + |
2133 | 2194 | offScreenCtx.putImageData(imageData, 0, 0);
|
2134 | 2195 |
|
2135 | 2196 | return { offScreenCanvas, offScreenCtx }
|
2136 | 2197 | }
|
2137 | 2198 |
|
| 2199 | + /** |
| 2200 | + * Clears the pixels from the canvas context |
| 2201 | + * |
| 2202 | + * @param {CanvasRenderingContext2D} canvasContext |
| 2203 | + */ |
| 2204 | + function clearCanvas (canvasContext) { |
| 2205 | + // Save state and clean the pixels from the canvas |
| 2206 | + canvasContext.save(); |
| 2207 | + canvasContext.globalCompositeOperation = 'destination-out'; |
| 2208 | + canvasContext.fillStyle = 'rgb(255,255,255)'; |
| 2209 | + canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height); |
| 2210 | + canvasContext.restore(); |
| 2211 | + } |
| 2212 | + |
| 2213 | + /** |
| 2214 | + * @param {ImageData} imageData |
| 2215 | + * @param {string} sessionKey |
| 2216 | + * @param {string} domainKey |
| 2217 | + * @param {number} width |
| 2218 | + */ |
2138 | 2219 | function modifyPixelData (imageData, domainKey, sessionKey, width) {
|
2139 | 2220 | const d = imageData.data;
|
2140 | 2221 | const length = d.length / 4;
|
|
2161 | 2242 | return imageData
|
2162 | 2243 | }
|
2163 | 2244 |
|
2164 |
| - // Ignore pixels that have neighbours that are the same |
| 2245 | + /** |
| 2246 | + * Ignore pixels that have neighbours that are the same |
| 2247 | + * |
| 2248 | + * @param {Uint8ClampedArray} imageData |
| 2249 | + * @param {number} index |
| 2250 | + * @param {number} width |
| 2251 | + */ |
2165 | 2252 | function adjacentSame (imageData, index, width) {
|
2166 | 2253 | const widthPixel = width * 4;
|
2167 | 2254 | const x = index % widthPixel;
|
|
2212 | 2299 | return true
|
2213 | 2300 | }
|
2214 | 2301 |
|
2215 |
| - // Check that a pixel at index and index2 match all channels |
| 2302 | + /** |
| 2303 | + * Check that a pixel at index and index2 match all channels |
| 2304 | + * @param {Uint8ClampedArray} imageData |
| 2305 | + * @param {number} index |
| 2306 | + * @param {number} index2 |
| 2307 | + */ |
2216 | 2308 | function pixelsSame (imageData, index, index2) {
|
2217 | 2309 | return imageData[index] === imageData[index2] &&
|
2218 | 2310 | imageData[index + 1] === imageData[index2 + 1] &&
|
2219 | 2311 | imageData[index + 2] === imageData[index2 + 2] &&
|
2220 | 2312 | imageData[index + 3] === imageData[index2 + 3]
|
2221 | 2313 | }
|
2222 | 2314 |
|
| 2315 | + /** |
| 2316 | + * Returns true if pixel should be ignored |
| 2317 | + * @param {Uint8ClampedArray} imageData |
| 2318 | + * @param {number} index |
| 2319 | + * @returns {boolean} |
| 2320 | + */ |
2223 | 2321 | function shouldIgnorePixel (imageData, index) {
|
2224 | 2322 | // Transparent pixels
|
2225 | 2323 | if (imageData[index + 3] === 0) {
|
|
2232 | 2330 | const { sessionKey, site } = args;
|
2233 | 2331 | const domainKey = site.domain;
|
2234 | 2332 | const featureName = 'fingerprinting-canvas';
|
| 2333 | + const supportsWebGl = getFeatureSettingEnabled(featureName, args, 'webGl'); |
2235 | 2334 |
|
2236 | 2335 | const unsafeCanvases = new WeakSet();
|
| 2336 | + const canvasContexts = new WeakMap(); |
2237 | 2337 | const canvasCache = new WeakMap();
|
2238 | 2338 |
|
| 2339 | + /** |
| 2340 | + * Clear cache as canvas has changed |
| 2341 | + * @param {HTMLCanvasElement} canvas |
| 2342 | + */ |
2239 | 2343 | function clearCache (canvas) {
|
2240 |
| - // Clear cache as canvas has changed |
2241 | 2344 | canvasCache.delete(canvas);
|
2242 | 2345 | }
|
2243 | 2346 |
|
| 2347 | + /** |
| 2348 | + * @param {HTMLCanvasElement} canvas |
| 2349 | + */ |
| 2350 | + function treatAsUnsafe (canvas) { |
| 2351 | + unsafeCanvases.add(canvas); |
| 2352 | + clearCache(canvas); |
| 2353 | + } |
| 2354 | + |
| 2355 | + const proxy = new DDGProxy(featureName, HTMLCanvasElement.prototype, 'getContext', { |
| 2356 | + apply (target, thisArg, args) { |
| 2357 | + const context = DDGReflect.apply(target, thisArg, args); |
| 2358 | + try { |
| 2359 | + canvasContexts.set(thisArg, context); |
| 2360 | + } catch { |
| 2361 | + } |
| 2362 | + return context |
| 2363 | + } |
| 2364 | + }); |
| 2365 | + proxy.overload(); |
| 2366 | + |
2244 | 2367 | // Known data methods
|
2245 | 2368 | const safeMethods = ['putImageData', 'drawImage'];
|
2246 | 2369 | for (const methodName of safeMethods) {
|
2247 | 2370 | const safeMethodProxy = new DDGProxy(featureName, CanvasRenderingContext2D.prototype, methodName, {
|
2248 | 2371 | apply (target, thisArg, args) {
|
2249 |
| - clearCache(thisArg.canvas); |
| 2372 | + // Don't apply escape hatch for canvases |
| 2373 | + if (methodName === 'drawImage' && args[0] && args[0] instanceof HTMLCanvasElement) { |
| 2374 | + treatAsUnsafe(args[0]); |
| 2375 | + } else { |
| 2376 | + clearCache(thisArg.canvas); |
| 2377 | + } |
2250 | 2378 | return DDGReflect.apply(target, thisArg, args)
|
2251 | 2379 | }
|
2252 | 2380 | });
|
|
2279 | 2407 | if (methodName in CanvasRenderingContext2D.prototype) {
|
2280 | 2408 | const unsafeProxy = new DDGProxy(featureName, CanvasRenderingContext2D.prototype, methodName, {
|
2281 | 2409 | apply (target, thisArg, args) {
|
2282 |
| - unsafeCanvases.add(thisArg.canvas); |
2283 |
| - clearCache(thisArg.canvas); |
| 2410 | + treatAsUnsafe(thisArg.canvas); |
2284 | 2411 | return DDGReflect.apply(target, thisArg, args)
|
2285 | 2412 | }
|
2286 | 2413 | });
|
2287 | 2414 | unsafeProxy.overload();
|
2288 | 2415 | }
|
2289 | 2416 | }
|
2290 | 2417 |
|
| 2418 | + if (supportsWebGl) { |
| 2419 | + const unsafeGlMethods = [ |
| 2420 | + 'commit', |
| 2421 | + 'compileShader', |
| 2422 | + 'shaderSource', |
| 2423 | + 'attachShader', |
| 2424 | + 'createProgram', |
| 2425 | + 'linkProgram', |
| 2426 | + 'drawElements', |
| 2427 | + 'drawArrays' |
| 2428 | + ]; |
| 2429 | + const glContexts = [ |
| 2430 | + WebGL2RenderingContext, |
| 2431 | + WebGLRenderingContext |
| 2432 | + ]; |
| 2433 | + for (const context of glContexts) { |
| 2434 | + for (const methodName of unsafeGlMethods) { |
| 2435 | + // Some methods are browser specific |
| 2436 | + if (methodName in context.prototype) { |
| 2437 | + const unsafeProxy = new DDGProxy(featureName, context.prototype, methodName, { |
| 2438 | + apply (target, thisArg, args) { |
| 2439 | + treatAsUnsafe(thisArg.canvas); |
| 2440 | + return DDGReflect.apply(target, thisArg, args) |
| 2441 | + } |
| 2442 | + }); |
| 2443 | + unsafeProxy.overload(); |
| 2444 | + } |
| 2445 | + } |
| 2446 | + } |
| 2447 | + } |
| 2448 | + |
2291 | 2449 | // Using proxies here to swallow calls to toString etc
|
2292 | 2450 | const getImageDataProxy = new DDGProxy(featureName, CanvasRenderingContext2D.prototype, 'getImageData', {
|
2293 | 2451 | apply (target, thisArg, args) {
|
|
2296 | 2454 | }
|
2297 | 2455 | // Anything we do here should be caught and ignored silently
|
2298 | 2456 | try {
|
2299 |
| - const { offScreenCtx } = getCachedOffScreenCanvasOrCompute(thisArg.canvas, domainKey, sessionKey, getImageDataProxy); |
| 2457 | + const { offScreenCtx } = getCachedOffScreenCanvasOrCompute(thisArg.canvas, domainKey, sessionKey); |
2300 | 2458 | // Call the original method on the modified off-screen canvas
|
2301 | 2459 | return DDGReflect.apply(target, offScreenCtx, args)
|
2302 | 2460 | } catch {
|
|
2307 | 2465 | });
|
2308 | 2466 | getImageDataProxy.overload();
|
2309 | 2467 |
|
2310 |
| - // Get cached offscreen if one exists, otherwise compute one |
2311 |
| - function getCachedOffScreenCanvasOrCompute (canvas, domainKey, sessionKey, getImageDataProxy) { |
| 2468 | + /** |
| 2469 | + * Get cached offscreen if one exists, otherwise compute one |
| 2470 | + * |
| 2471 | + * @param {HTMLCanvasElement} canvas |
| 2472 | + * @param {string} domainKey |
| 2473 | + * @param {string} sessionKey |
| 2474 | + */ |
| 2475 | + function getCachedOffScreenCanvasOrCompute (canvas, domainKey, sessionKey) { |
2312 | 2476 | let result;
|
2313 | 2477 | if (canvasCache.has(canvas)) {
|
2314 | 2478 | result = canvasCache.get(canvas);
|
2315 | 2479 | } else {
|
2316 |
| - result = computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy); |
| 2480 | + const ctx = canvasContexts.get(canvas); |
| 2481 | + result = computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy, ctx); |
2317 | 2482 | canvasCache.set(canvas, result);
|
2318 | 2483 | }
|
2319 | 2484 | return result
|
|
2328 | 2493 | return DDGReflect.apply(target, thisArg, args)
|
2329 | 2494 | }
|
2330 | 2495 | try {
|
2331 |
| - const { offScreenCanvas } = getCachedOffScreenCanvasOrCompute(thisArg, domainKey, sessionKey, getImageDataProxy); |
| 2496 | + const { offScreenCanvas } = getCachedOffScreenCanvasOrCompute(thisArg, domainKey, sessionKey); |
2332 | 2497 | // Call the original method on the modified off-screen canvas
|
2333 | 2498 | return DDGReflect.apply(target, offScreenCanvas, args)
|
2334 | 2499 | } catch {
|
|
0 commit comments