From 4589cf8454f55b993b2210db9d67607e1c0106e7 Mon Sep 17 00:00:00 2001 From: Jonathan Kingston Date: Thu, 16 Sep 2021 16:56:51 +0100 Subject: [PATCH] Simplify canvas layering checks (#55) Co-authored-by: Jonathan Kingston --- privacy-protections/fingerprinting/canvas.js | 144 +++++++++---------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/privacy-protections/fingerprinting/canvas.js b/privacy-protections/fingerprinting/canvas.js index 7555afe..3c9de07 100644 --- a/privacy-protections/fingerprinting/canvas.js +++ b/privacy-protections/fingerprinting/canvas.js @@ -36,86 +36,86 @@ function generateDataURLWithCode (codeInput, alpha = 255) { return canvasElement.toDataURL(); } +function calculateDifferencesOnRandomCanvas (width, height) { + // Make a unique int per pixel for the channels + function checkPixel (d, i) { + return d[i] + (d[i + 1] * 10 ** 3) + (d[i + 2] * 10 ** 6) + (d[i + 3] * 10 ** 9); + } + const size = width * height; + const pixelAlterations = []; + let randData; + // eslint-disable-next-line new-cap + const rng = new Math.seedrandom('something'); + // Compare 10 canvases and ensure per pixel there is more than one result + for (let i = 0; i < 10; i++) { + const canvasElement = createCanvas(width, height); + const canvasContext = canvasElement.getContext('2d'); + + // Generate rand data for all canvases to use + if (i === 0) { + const canvasDataBlank = canvasContext.getImageData(0, 0, canvasElement.width, canvasElement.height); + for (let i = 0; i < size * 4; i += 4) { + canvasDataBlank.data[i] = Math.floor(rng() * 256); + canvasDataBlank.data[i + 1] = Math.floor(rng() * 256); + canvasDataBlank.data[i + 2] = Math.floor(rng() * 256); + canvasDataBlank.data[i + 3] = 255; + } + randData = canvasDataBlank; + } + canvasContext.putImageData(randData, 0, 0); + + // Draw a unique pixel in top left corner + const ctx = canvasElement.getContext('2d'); + ctx.fillStyle = `#f6${i}`; + ctx.fillRect(0, 0, 1, 1); + + // Put pixel data in set if doesn't match reference data + const canvasData = canvasContext.getImageData(0, 0, canvasElement.width, canvasElement.height); + // Start at pixel 1 as pixel 0 was used for making the canvas random + for (let j = 1; j < size; j++) { + const pixelChecksum = checkPixel(canvasData.data, j * 4); + if (pixelChecksum !== checkPixel(randData.data, j * 4)) { + if (!(pixelAlterations[j])) { + pixelAlterations[j] = new Set(); + } + pixelAlterations[j].add(pixelChecksum); + } + } + } + + // Calculates the number of unique pixel counts + // EG: If pixel at index 2 has 5 unique values accross the 10 canvas slices we would increment pixelChangeCounts[5] += 1 + const pixelChangeCounts = [0]; + for (let i = 1; i < size; i++) { + // If pixel doesn't exist then it's the same as the reference data + if (!(i in pixelAlterations)) { + pixelChangeCounts[0] += 1; + continue; + } + // The number of unique differences to the reference data across the slices + const uniquePixelCount = pixelAlterations[i].size; + if (!(uniquePixelCount in pixelChangeCounts)) { + pixelChangeCounts[uniquePixelCount] = 0; + } + pixelChangeCounts[uniquePixelCount] += 1; + } + return pixelChangeCounts; +} + const tests = [ // Ensures there is a distribution of differences on the canvas { id: 'layering comparison', category: 'resistance', value: async () => { - const size = 2000 * 200; - // Make a unique int per pixel for the channels - function checkPixel (d, i) { - return d[i] + (d[i + 1] * 10 ** 3) + (d[i + 2] * 10 ** 6) + (d[i + 3] * 10 ** 9); - } - - function shouldIgnorePixel (d, i) { - if (d[i + 3] === 0) { - return true; - } - return false; - } - - const pixelData = []; - let randData; - // eslint-disable-next-line new-cap - const rng = new Math.seedrandom('something'); - let eligiblePixels = 0; - // Compare 10 canvases and ensure per pixel there is more than one result - for (let i = 0; i < 10; i++) { - const canvasElement = createCanvas(2000, 200); - const canvasContext = canvasElement.getContext('2d'); - - // Generate rand data for all canvases to use - if (i === 0) { - const canvasDataBlank = canvasContext.getImageData(0, 0, canvasElement.width, canvasElement.height); - for (let i = 0; i < size * 4; i += 4) { - canvasDataBlank.data[i] = Math.floor(rng() * 256); - canvasDataBlank.data[i + 1] = Math.floor(rng() * 256); - canvasDataBlank.data[i + 2] = Math.floor(rng() * 256); - canvasDataBlank.data[i + 3] = 255; - } - randData = canvasDataBlank; - } - canvasContext.putImageData(randData, 0, 0); - - // Draw a unique pixel in top left corner - const ctx = canvasElement.getContext('2d'); - ctx.fillStyle = `#f6${i}`; - ctx.fillRect(0, 0, 1, 1); - - const canvasData = canvasContext.getImageData(0, 0, canvasElement.width, canvasElement.height); - // Start at pixel 1 as pixel 0 was used for making the canvas random - for (let j = 1; j < size; j++) { - if (!shouldIgnorePixel(canvasData.data, j * 4)) { - // Only in layer one check how many eligible pixels we have - if (i === 0) { - eligiblePixels++; - } - if (!(pixelData[j])) { - pixelData[j] = new Set(); - } - pixelData[j].add(checkPixel(canvasData.data, j * 4)); - } - } - } - - // Calculates the number of unique pixel counts - // EG: If pixel at index 2 has 5 unique values accross the 10 canvas slices we would increment pixelChangeCounts[5] += 1 - const pixelChangeCounts = []; - for (let i = 1; i < size; i++) { - // The number of unique pixel data across the slices - const uniquePixelCount = pixelData[i].size; - if (!(uniquePixelCount in pixelChangeCounts)) { - pixelChangeCounts[uniquePixelCount] = 0; - } - pixelChangeCounts[uniquePixelCount] += 1; - } + const width = 2000; + const height = 2000; + const pixelChangeCounts = calculateDifferencesOnRandomCanvas(width, height); + const eligiblePixels = (width * height) - 1; const zeroChange = pixelChangeCounts.shift(); - ok(zeroChange === undefined, 'Sanity check on ignored pixels'); - // Remove single change counts - pixelChangeCounts.shift(); + ok(zeroChange < eligiblePixels, 'Sanity check on pixels that match the reference data'); const changeCount = pixelChangeCounts.reduce((a, i) => a + i, 0); - ok(changeCount > 0, 'Should not only have single perturbed changes'); + ok(changeCount + zeroChange === eligiblePixels, 'Sanity check on counts matching'); // This is set purposefully low to avoid false positives and also our eligible pixel algo is more complex and may change over time. ok(changeCount > (eligiblePixels * 0.2), 'Should have more than 20% of eligiblePixels that are changed across multiple canvases'); return pixelChangeCounts;