Repository: antoinevastel/picasso-like-canvas-fingerprinting Branch: master Commit: a39549806072 Files: 8 Total size: 20.4 KB Directory structure: gitextract_wymqla_6/ ├── .github/ │ └── workflows/ │ └── actions.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples/ │ └── demo.html ├── package.json ├── src/ │ └── canvas.js └── test/ └── test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/actions.yml ================================================ name: picasso-canvas-fingerprinting on: [push] jobs: check-picasso-consistency: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '17' - run: npm install - run: npm run test ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 antoine vastel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Picasso based canvas fingerprinting [](https://github.com/antoinevastel/picasso-like-canvas-fingerprinting/actions) [](https://www.npmjs.com/package/picasso-canvas-fingerprinting) ## News: I recently created a [website where you can see your browser fingerprint](https://deviceandbrowserinfo.com/info_device) and different fingerprinting-related signals like your IP address, your canvas fingerprint, your HTTP headers, etc. Some information is accessible both through a webpage and through APIs. Don't hesitate to bookmark it as I will add more signals and more content related to bots. ------------ Implementation of a canvas fingerprinting algorithm inspired by the [Picasso paper](https://ai.google/research/pubs/pub45581) written by Elie Bursztein. An online demo is available on [my blog](https://antoinevastel.com/browser%20fingerprinting/2019/03/21/picasso-canvas-fingerprinting.html). ## Quick start ```html You can host your own version of the Picasso canvas fingerprinting script or include it using Jsdelivr CDN. ``` Once the Picasso script is loaded in your HTML page, you can use it as follows: ```html ``` ================================================ FILE: examples/demo.html ================================================
================================================ FILE: package.json ================================================ { "name": "picasso-canvas-fingerprinting", "version": "1.0.1", "description": "Implementation of a canvas fingerprinting algorithm inspired by the Picasso paper", "main": "src/canvas.js", "scripts": { "test": "node_modules/mocha/bin/mocha.js" }, "repository": { "type": "git", "url": "git+https://github.com/antoinevastel/picasso-like-canvas-fingerprinting.git" }, "author": "Antoine Vastel", "license": "MIT", "bugs": { "url": "https://github.com/antoinevastel/picasso-like-canvas-fingerprinting/issues" }, "homepage": "https://github.com/antoinevastel/picasso-like-canvas-fingerprinting#readme", "devDependencies": { "mocha": "^10.0.0", "puppeteer": "^15.4.0" } } ================================================ FILE: src/canvas.js ================================================ function x64Add(m, n) { m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff]; n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff]; var o = [0, 0, 0, 0]; o[3] += m[3] + n[3]; o[2] += o[3] >>> 16; o[3] &= 0xffff; o[2] += m[2] + n[2]; o[1] += o[2] >>> 16; o[2] &= 0xffff; o[1] += m[1] + n[1]; o[0] += o[1] >>> 16; o[1] &= 0xffff; o[0] += m[0] + n[0]; o[0] &= 0xffff; return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]]; } function x64Multiply(m, n) { m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff]; n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff]; var o = [0, 0, 0, 0]; o[3] += m[3] * n[3]; o[2] += o[3] >>> 16; o[3] &= 0xffff; o[2] += m[2] * n[3]; o[1] += o[2] >>> 16; o[2] &= 0xffff; o[2] += m[3] * n[2]; o[1] += o[2] >>> 16; o[2] &= 0xffff; o[1] += m[1] * n[3]; o[0] += o[1] >>> 16; o[1] &= 0xffff; o[1] += m[2] * n[2]; o[0] += o[1] >>> 16; o[1] &= 0xffff; o[1] += m[3] * n[1]; o[0] += o[1] >>> 16; o[1] &= 0xffff; o[0] += (m[0] * n[3]) + (m[1] * n[2]) + (m[2] * n[1]) + (m[3] * n[0]); o[0] &= 0xffff; return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]]; } function x64Rotl(m, n) { n %= 64; if (n === 32) { return [m[1], m[0]] } else if (n < 32) { return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))] } else { n -= 32; return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))] } } function x64LeftShift(m, n) { n %= 64; if (n === 0) { return m } else if (n < 32) { return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n] } else { return [m[1] << (n - 32), 0] } } function x64Xor(m, n) { return [m[0] ^ n[0], m[1] ^ n[1]]; } function x64Fmix(h) { h = x64Xor(h, [0, h[0] >>> 1]); h = x64Multiply(h, [0xff51afd7, 0xed558ccd]); h = x64Xor(h, [0, h[0] >>> 1]); h = x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53]); h = x64Xor(h, [0, h[0] >>> 1]); return h } function x64hash128(key, seed) { key = key || ''; seed = seed || 0; var remainder = key.length % 16; var bytes = key.length - remainder; var h1 = [0, seed]; var h2 = [0, seed]; var k1 = [0, 0]; var k2 = [0, 0]; var c1 = [0x87c37b91, 0x114253d5]; var c2 = [0x4cf5ad43, 0x2745937f]; for (var i = 0; i < bytes; i = i + 16) { k1 = [((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24), ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24)]; k2 = [((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24), ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24)]; k1 = x64Multiply(k1, c1); k1 = x64Rotl(k1, 31); k1 = x64Multiply(k1, c2); h1 = x64Xor(h1, k1); h1 = x64Rotl(h1, 27); h1 = x64Add(h1, h2); h1 = x64Add(x64Multiply(h1, [0, 5]), [0, 0x52dce729]); k2 = x64Multiply(k2, c2); k2 = x64Rotl(k2, 33); k2 = x64Multiply(k2, c1); h2 = x64Xor(h2, k2); h2 = x64Rotl(h2, 31); h2 = x64Add(h2, h1); h2 = x64Add(x64Multiply(h2, [0, 5]), [0, 0x38495ab5]); } k1 = [0, 0]; k2 = [0, 0]; switch (remainder) { case 15: k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 14)], 48)); case 14: k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 13)], 40)); case 13: k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 12)], 32)); case 12: k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 11)], 24)); case 11: k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 10)], 16)); case 10: k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 9)], 8)); case 9: k2 = x64Xor(k2, [0, key.charCodeAt(i + 8)]); k2 = x64Multiply(k2, c2); k2 = x64Rotl(k2, 33); k2 = x64Multiply(k2, c1); h2 = x64Xor(h2, k2); case 8: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 7)], 56)); case 7: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 6)], 48)); case 6: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 5)], 40)); case 5: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 4)], 32)); case 4: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 3)], 24)); case 3: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 2)], 16)); case 2: k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 1)], 8)); case 1: k1 = x64Xor(k1, [0, key.charCodeAt(i)]); k1 = x64Multiply(k1, c1); k1 = x64Rotl(k1, 31); k1 = x64Multiply(k1, c2); h1 = x64Xor(h1, k1); } h1 = x64Xor(h1, [0, key.length]); h2 = x64Xor(h2, [0, key.length]); h1 = x64Add(h1, h2); h2 = x64Add(h2, h1); h1 = x64Fmix(h1); h2 = x64Fmix(h2); h1 = x64Add(h1, h2); h2 = x64Add(h2, h1); return ('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[1] >>> 0).toString(16)).slice(-8) } function picassoCanvas(roundNumber, seed, params) { const {area, offsetParameter, multiplier, fontSizeFactor, maxShadowBlur} = params; class Prng { constructor(seed) { this.currentNumber = seed % offsetParameter; if (this.currentNumber <= 0) { this.currentNumber += offsetParameter } } getNext() { this.currentNumber = multiplier * this.currentNumber % offsetParameter; return this.currentNumber; } } function adaptRandomNumberToContext(randomNumber, maxBound, floatAllowed) { randomNumber = (randomNumber - 1) / offsetParameter; if (floatAllowed) { return randomNumber * maxBound; } return Math.floor(randomNumber * maxBound); } function addRandomCanvasGradient(prng, context, area) { const canvasGradient = context.createRadialGradient( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), area.width) ); canvasGradient.addColorStop(0, colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]); canvasGradient.addColorStop(1, colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]); context.fillStyle = canvasGradient } function generateRandomWord(prng, wordLength) { const minAscii = 65; const maxAscii = 126; const wordGenerated = []; for (let i = 0; i < wordLength; i++) { const asciiCode = minAscii + (prng.getNext() % (maxAscii - minAscii)); wordGenerated.push(String.fromCharCode(asciiCode)); } return wordGenerated.join(''); } if (!window.CanvasRenderingContext2D) { return 'unknown'; } const colors = ['#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6', '#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D', '#80B300', '#809900', '#E6B3B3', '#6680B3', '#66991A', '#FF99E6', '#CCFF1A', '#FF1A66', '#E6331A', '#33FFCC', '#66994D', '#B366CC', '#4D8000', '#B33300', '#CC80CC', '#66664D', '#991AFF', '#E666FF', '#4DB3FF', '#1AB399', '#E666B3', '#33991A', '#CC9999', '#B3B31A', '#00E680', '#4D8066', '#809980', '#E6FF80', '#1AFF33', '#999933', '#FF3380', '#CCCC00', '#66E64D', '#4D80CC', '#9900B3', '#E64D66', '#4DB380', '#FF4D4D', '#99E6E6', '#6666FF']; const primitives = [ function arc (prng, context, area) { context.beginPath(); context.arc( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), Math.min(area.width, area.height)), adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true), adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true) ); context.stroke() }, function text (prng, context, area) { const wordLength = Math.max(1, adaptRandomNumberToContext(prng.getNext(), 5)); const textToStroke = generateRandomWord(prng, wordLength); context.font = `${area.height / fontSizeFactor}px aafakefontaa`; context.strokeText( textToStroke, adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), area.width) ) }, function bezierCurve (prng, context, area) { context.beginPath(); context.moveTo( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height) ); context.bezierCurveTo( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height) ); context.stroke() }, function quadraticCurve(prng, context, area) { context.beginPath(); context.moveTo( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height) ); context.quadraticCurveTo( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height) ); context.stroke() }, function ellipse(prng, context, area) { context.beginPath(); context.ellipse( adaptRandomNumberToContext(prng.getNext(), area.width), adaptRandomNumberToContext(prng.getNext(), area.height), adaptRandomNumberToContext(prng.getNext(), Math.floor(area.width/2)), adaptRandomNumberToContext(prng.getNext(), Math.floor(area.height/2)), adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true), adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true), adaptRandomNumberToContext(prng.getNext(), 2 * Math.PI, true) ); context.stroke() } ]; try { const prng = new Prng(seed); const canvasElt = document.createElement("canvas"); canvasElt.width = area.width; canvasElt.height = area.height; canvasElt.style.display = "none"; const context = canvasElt.getContext("2d"); for (let i = 0; i < roundNumber; i++) { addRandomCanvasGradient(prng, context, area); context.shadowBlur = adaptRandomNumberToContext(prng.getNext(), maxShadowBlur); context.shadowColor = colors[adaptRandomNumberToContext(prng.getNext(), colors.length)]; const randomPrimitive = primitives[adaptRandomNumberToContext(prng.getNext(), primitives.length)]; randomPrimitive(prng, context, area); context.fill() } return x64hash128(canvasElt.toDataURL(), seed); } catch (e) {} } ================================================ FILE: test/test.js ================================================ const assert = require('assert'); const puppeteer = require('puppeteer'); const fs = require('fs'); describe('Picasso canvas fingerprinting', function() { this.timeout(10000); it('Picasso value should be consistent', async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); const picassoCode = fs.readFileSync('./src/canvas.js', 'utf8'); await page.addScriptTag({content: picassoCode}); const res = await page.evaluate(() => { const params = { area: { width: 300, height: 300, }, offsetParameter: 2001000001, fontSizeFactor: 1.5, multiplier: 15000, maxShadowBlur: 50, }; const numShapes = 5; const initialSeed = Math.floor(100*Math.random()); const canvasValue = picassoCanvas( numShapes, initialSeed, params ); return canvasValue; }) await browser.close(); assert.equal(typeof res, 'string'); assert.equal(res.length > 15, true); }); });