Skip to content

Commit

Permalink
Simplify canvas layering checks (#55)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan Kingston <jkingston@duckduckgo.com>
  • Loading branch information
jonathanKingston and jonathanKingston authored Sep 16, 2021
1 parent 9a2f84d commit 4589cf8
Showing 1 changed file with 72 additions and 72 deletions.
144 changes: 72 additions & 72 deletions privacy-protections/fingerprinting/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 4589cf8

Please sign in to comment.