Skip to content

Commit 2416353

Browse files
Add fingerprint resistance to WebGL contexts (#48)
* Add fingerprint resistance to WebGL contexts * Add typings to canvas code * Add missing unsafe methods * Add back in fast path for 2d canvases * Add support for feature setting checking and add a check for webgl
1 parent 16e0e3a commit 2416353

File tree

14 files changed

+1184
-407
lines changed

14 files changed

+1184
-407
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16

build/apple/contentScope.js

Lines changed: 182 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
var contentScopeFeatures = (function (exports) {
5454
'use strict';
5555

56-
const sjcl = (() => {
56+
// @ts-nocheck
57+
const sjcl = (() => {
5758
/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
5859
/*global document, window, escape, unescape, module, require, Uint32Array */
5960

@@ -851,7 +852,44 @@
851852
})
852853
}
853854

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+
*/
854886
class DDGProxy {
887+
/**
888+
* @param {string} featureName
889+
* @param {P} objectScope
890+
* @param {string} property
891+
* @param {ProxyObject<P>} proxyObject
892+
*/
855893
constructor (featureName, objectScope, property, proxyObject) {
856894
this.objectScope = objectScope;
857895
this.property = property;
@@ -2119,22 +2157,65 @@
21192157

21202158
var seedrandom = sr;
21212159

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+
}
21272171

21282172
// Make a off-screen canvas and put the data there
21292173
const offScreenCanvas = document.createElement('canvas');
21302174
offScreenCanvas.width = canvas.width;
21312175
offScreenCanvas.height = canvas.height;
21322176
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+
21332194
offScreenCtx.putImageData(imageData, 0, 0);
21342195

21352196
return { offScreenCanvas, offScreenCtx }
21362197
}
21372198

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+
*/
21382219
function modifyPixelData (imageData, domainKey, sessionKey, width) {
21392220
const d = imageData.data;
21402221
const length = d.length / 4;
@@ -2161,7 +2242,13 @@
21612242
return imageData
21622243
}
21632244

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+
*/
21652252
function adjacentSame (imageData, index, width) {
21662253
const widthPixel = width * 4;
21672254
const x = index % widthPixel;
@@ -2212,14 +2299,25 @@
22122299
return true
22132300
}
22142301

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+
*/
22162308
function pixelsSame (imageData, index, index2) {
22172309
return imageData[index] === imageData[index2] &&
22182310
imageData[index + 1] === imageData[index2 + 1] &&
22192311
imageData[index + 2] === imageData[index2 + 2] &&
22202312
imageData[index + 3] === imageData[index2 + 3]
22212313
}
22222314

2315+
/**
2316+
* Returns true if pixel should be ignored
2317+
* @param {Uint8ClampedArray} imageData
2318+
* @param {number} index
2319+
* @returns {boolean}
2320+
*/
22232321
function shouldIgnorePixel (imageData, index) {
22242322
// Transparent pixels
22252323
if (imageData[index + 3] === 0) {
@@ -2232,21 +2330,51 @@
22322330
const { sessionKey, site } = args;
22332331
const domainKey = site.domain;
22342332
const featureName = 'fingerprinting-canvas';
2333+
const supportsWebGl = getFeatureSettingEnabled(featureName, args, 'webGl');
22352334

22362335
const unsafeCanvases = new WeakSet();
2336+
const canvasContexts = new WeakMap();
22372337
const canvasCache = new WeakMap();
22382338

2339+
/**
2340+
* Clear cache as canvas has changed
2341+
* @param {HTMLCanvasElement} canvas
2342+
*/
22392343
function clearCache (canvas) {
2240-
// Clear cache as canvas has changed
22412344
canvasCache.delete(canvas);
22422345
}
22432346

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+
22442367
// Known data methods
22452368
const safeMethods = ['putImageData', 'drawImage'];
22462369
for (const methodName of safeMethods) {
22472370
const safeMethodProxy = new DDGProxy(featureName, CanvasRenderingContext2D.prototype, methodName, {
22482371
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+
}
22502378
return DDGReflect.apply(target, thisArg, args)
22512379
}
22522380
});
@@ -2279,15 +2407,45 @@
22792407
if (methodName in CanvasRenderingContext2D.prototype) {
22802408
const unsafeProxy = new DDGProxy(featureName, CanvasRenderingContext2D.prototype, methodName, {
22812409
apply (target, thisArg, args) {
2282-
unsafeCanvases.add(thisArg.canvas);
2283-
clearCache(thisArg.canvas);
2410+
treatAsUnsafe(thisArg.canvas);
22842411
return DDGReflect.apply(target, thisArg, args)
22852412
}
22862413
});
22872414
unsafeProxy.overload();
22882415
}
22892416
}
22902417

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+
22912449
// Using proxies here to swallow calls to toString etc
22922450
const getImageDataProxy = new DDGProxy(featureName, CanvasRenderingContext2D.prototype, 'getImageData', {
22932451
apply (target, thisArg, args) {
@@ -2296,7 +2454,7 @@
22962454
}
22972455
// Anything we do here should be caught and ignored silently
22982456
try {
2299-
const { offScreenCtx } = getCachedOffScreenCanvasOrCompute(thisArg.canvas, domainKey, sessionKey, getImageDataProxy);
2457+
const { offScreenCtx } = getCachedOffScreenCanvasOrCompute(thisArg.canvas, domainKey, sessionKey);
23002458
// Call the original method on the modified off-screen canvas
23012459
return DDGReflect.apply(target, offScreenCtx, args)
23022460
} catch {
@@ -2307,13 +2465,20 @@
23072465
});
23082466
getImageDataProxy.overload();
23092467

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) {
23122476
let result;
23132477
if (canvasCache.has(canvas)) {
23142478
result = canvasCache.get(canvas);
23152479
} else {
2316-
result = computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy);
2480+
const ctx = canvasContexts.get(canvas);
2481+
result = computeOffScreenCanvas(canvas, domainKey, sessionKey, getImageDataProxy, ctx);
23172482
canvasCache.set(canvas, result);
23182483
}
23192484
return result
@@ -2328,7 +2493,7 @@
23282493
return DDGReflect.apply(target, thisArg, args)
23292494
}
23302495
try {
2331-
const { offScreenCanvas } = getCachedOffScreenCanvasOrCompute(thisArg, domainKey, sessionKey, getImageDataProxy);
2496+
const { offScreenCanvas } = getCachedOffScreenCanvasOrCompute(thisArg, domainKey, sessionKey);
23322497
// Call the original method on the modified off-screen canvas
23332498
return DDGReflect.apply(target, offScreenCanvas, args)
23342499
} catch {

build/chrome/inject.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)