Last active
January 28, 2021 21:43
-
-
Save atoponce/d7e8f7b378f80875f6c61120737de1b0 to your computer and use it in GitHub Desktop.
JavaScript entropy proof-of-concept
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' /> | |
<title>JavaScript Entropy Proof-of-Concept</title> | |
<script language='javascript'> | |
function draw_disco(s) { | |
const canvas = document.getElementById('canvas') | |
const context = canvas.getContext('2d') | |
const radius = 30 | |
for (let i = 0; i < 8; i++) { | |
for (let j = 0; j < 8; j++) { | |
const centerX = 60 * j + 30 | |
const centerY = 60 * i + 30 | |
if (s[ i * 8 + j] === '0') context.fillStyle = '#801515' | |
if (s[ i * 8 + j] === '1') context.fillStyle = '#D4916A' | |
if (s[ i * 8 + j] === '2') context.fillStyle = '#805215' | |
if (s[ i * 8 + j] === '3') context.fillStyle = '#D4B56A' | |
if (s[ i * 8 + j] === '4') context.fillStyle = '#806D15' | |
if (s[ i * 8 + j] === '5') context.fillStyle = '#D4CF6A' | |
if (s[ i * 8 + j] === '6') context.fillStyle = '#697B15' | |
if (s[ i * 8 + j] === '7') context.fillStyle = '#9AC361' | |
if (s[ i * 8 + j] === '8') context.fillStyle = '#116611' | |
if (s[ i * 8 + j] === '9') context.fillStyle = '#458A79' | |
if (s[ i * 8 + j] === 'a') context.fillStyle = '#123652' | |
if (s[ i * 8 + j] === 'b') context.fillStyle = '#515E91' | |
if (s[ i * 8 + j] === 'c') context.fillStyle = '#261758' | |
if (s[ i * 8 + j] === 'd') context.fillStyle = '#6F4D8F' | |
if (s[ i * 8 + j] === 'e') context.fillStyle = '#530E53' | |
if (s[ i * 8 + j] === 'f') context.fillStyle = '#B45A81' | |
context.beginPath() | |
context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false) | |
context.fill() | |
} | |
} | |
} | |
function csprng_entropy(buf, crypto) { | |
// collect cryptographic quality randomness | |
const csprng = new Uint32Array(8) | |
crypto.getRandomValues(csprng) | |
console.log('System entropy: ', csprng) | |
for (let i = 0; i < 8; i++) { | |
buf[i] = csprng[i] | |
} | |
return buf | |
} | |
function timing_entropy(buf, crypto) { | |
// if the CSPRNG is not trustworthy, mix some timing entropy | |
const timings = new Uint32Array(256) | |
const isPrime = num => { | |
for(let i = 2, s = Math.sqrt(num); i <= s; i++) | |
if(num % i === 0) return false; | |
return num > 1; | |
} | |
for (let i = 0; i < 256; i++) { | |
const start_a = performance.now() | |
let j = 0, a = 0 | |
while (performance.now() === start_a) { | |
if (isPrime(a * i * j)) { | |
a = a * i * j | |
} | |
j++ | |
} | |
timings[i] = j | |
} | |
console.log('Raw timing data: ', timings) | |
crypto.subtle.digest('SHA-256', timings) | |
.then(function(hash) { | |
results = new Uint32Array(hash) | |
console.log('Timing entropy: ', results) | |
for (let i = 0; i < results.length; i++) { | |
buf[i] ^= results[i] | |
} | |
}) | |
.catch(function(e){ | |
console.error(e) | |
}) | |
return buf | |
} | |
function mouse_entropy(buf, crypto) { | |
// if the CSPRNG is not trustworthy, mix some mouse entropy | |
const coords = new Uint32Array(256) | |
const mouseEntropy = function(e) { | |
this.ctr = this.ctr || 0 | |
if (this.ctr > 255) { | |
this.ctr = 0 | |
window.removeEventListener('mousemove', mouseEntropy) | |
console.log('Raw mouse data: ', coords) | |
crypto.subtle.digest('SHA-256', coords) | |
.then(function(hash) { | |
results = new Uint32Array(hash) | |
console.log('Mouse entropy: ', results) | |
for (let i = 0; i < results.length; i++) { | |
buf[i] ^= results[i] | |
} | |
console.log('Final entropy: ', buf) | |
}) | |
.catch(function(e){ | |
console.error(e) | |
}) | |
const mouse_id = document.getElementById('mouse') | |
const entropy_results = document.getElementById('entropy_results') | |
const artwork = document.getElementById('artwork') | |
let hex = '' | |
for (let i = 0; i < buf.length; i++) { | |
hex += ('00000000'+(Number(buf[i]).toString(16))).slice(-8) | |
} | |
mouse_id.innerText = 'Stop!'; | |
artwork.innerHTML = 'You could take a screenshot of this to use as a <a href="https://keepass.info/help/base/keys.html#keyfiles">KeePass key file</a>:' | |
draw_disco(hex) | |
return | |
} | |
else { | |
coords[this.ctr] = window.screen.width * e.y + e.x | |
this.ctr++ | |
} | |
} | |
window.addEventListener('mousemove', mouseEntropy) | |
return buf | |
} | |
function reset_entropies() { | |
console.clear() | |
// the main crytpographic object | |
const crypto = window.crypto || window.mscrypto | |
// the final collected entropy after all the mixings | |
let finals = new Uint32Array(8) | |
finals = csprng_entropy(finals, crypto) | |
finals = timing_entropy(finals, crypto) | |
// wait 1 second to get the mouse away from the reset button to reduce predicting the starting event | |
setTimeout(function() { | |
finals = mouse_entropy(finals, crypto) | |
}, 1000) | |
const mouse_id = document.getElementById('mouse') | |
const entropy_results = document.getElementById('entropy_results') | |
const artwork = document.getElementById('artwork') | |
const canvas = document.getElementById('canvas') | |
const context = canvas.getContext('2d') | |
mouse_id.innerText = 'Keep moving your mouse...' | |
entropy_results.innerText = '' | |
artwork.innerText = '' | |
context.clearRect(0, 0, canvas.width, canvas.height) | |
} | |
</script> | |
</head> | |
<body onload='reset_entropies()')> | |
<h1>JavaScript Entropy Collector Proof-of-Concept</h1> | |
<p>Just a JavaScript entropy proof-of-concept (<a href='https://gist.github.com/atoponce/d7e8f7b378f80875f6c61120737de1b0'>source code</a>). In practice, whenever you need safe, secure, cryptographic-quality randomness, you should always use the Web Crypto API CSPRNG.</p> | |
<p>However, if you suspect that your CSPRNG is not trustworthy, this page demonstrates a possible improvement by mixing in additional entropy from external sources.</p> | |
<p>This was inspired by how <a href='https://www.veracrypt.fr/en/Random%20Number%20Generator.html'>VeraCrypt</a> creates volumes.</p> | |
<p>The procedure is as follows: | |
<ol> | |
<li>Generate 256-bits of entropy from the <tt>window.crypto.getRandomValues()</tt> CSPRNG.</li> | |
<li>Generate 256-bits of entropy from 256 high-precision timing events (assumes 1-bit per event).</li> | |
<li>Generate 256-bits of entropy from 256 collected mouse movement events (assumes 1-bit per event).</li> | |
<li>Calculate <tt>final = csprng xor sha-256(timings) xor sha-256(mouse)</tt>, convert to hexadecimal, and print.</li> | |
</ol> | |
</p> | |
<p style='font-weight: bold;'>This implementation is not yet mobile device friendly.</p> | |
<button onclick='reset_entropies()'>Reset</button> | |
<p id='mouse'></p> | |
<p id='entropy_results' style='color: green; font-size: 20px;'></p> | |
<p id='artwork'></p> | |
<canvas id='canvas' width='480' height='480'></canvas> | |
</body> | |
<html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment