passgen: Cryptographically Random Passwords Without crypto.randomUUID Hacks
Team shared passwords in Slack. Built a generator using crypto.getRandomValues with zxcvbn-style strength scoring — no npm.
Rule: Never use Math.random() for passwords. This tool uses crypto.getRandomValues only.
Full generator
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Password Generator</title>
<style>
body { font-family: system-ui; max-width: 520px; margin: 2rem auto; padding: 0 1rem; }
output { display: block; font-family: monospace; font-size: 18px; padding: 1rem; background: #f8fafc; border-radius: 8px; word-break: break-all; }
.bar { height: 8px; background: #e2e8f0; border-radius: 4px; margin: .5rem 0; }
.bar span { display: block; height: 100%; border-radius: 4px; transition: width .2s; }
label { display: block; margin: .5rem 0; }
</style>
</head>
<body>
<h1>Password Generator</h1>
<output id="pw"></output>
<div class="bar"><span id="strength" style="width:0;background:#22c55e"></span></div>
<p id="label"></p>
<label>Length <input type="range" id="len" min="8" max="64" value="20"> <span id="lenVal">20</span></label>
<label><input type="checkbox" id="upper" checked> A-Z</label>
<label><input type="checkbox" id="lower" checked> a-z</label>
<label><input type="checkbox" id="nums" checked> 0-9</label>
<label><input type="checkbox" id="sym" checked> Symbols</label>
<button id="gen">Generate</button>
<button id="copy">Copy</button>
<script>
const CHARSET = {
upper: 'ABCDEFGHJKLMNPQRSTUVWXYZ',
lower: 'abcdefghijkmnopqrstuvwxyz',
nums: '23456789',
sym: '!@#$%^&*-_+=?',
};
function secureRandomInt(max) {
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
return buf[0] % max;
}
function buildPool() {
let pool = '';
if (document.getElementById('upper').checked) pool += CHARSET.upper;
if (document.getElementById('lower').checked) pool += CHARSET.lower;
if (document.getElementById('nums').checked) pool += CHARSET.nums;
if (document.getElementById('sym').checked) pool += CHARSET.sym;
return pool;
}
function score(pw) {
let s = 0;
if (pw.length >= 12) s += 25;
if (pw.length >= 20) s += 25;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) s += 15;
if (/[0-9]/.test(pw)) s += 15;
if (/[^a-zA-Z0-9]/.test(pw)) s += 20;
return Math.min(100, s);
}
function generate() {
const pool = buildPool();
if (!pool) { alert('Select at least one charset'); return; }
const len = Number(document.getElementById('len').value);
let pw = '';
for (let i = 0; i < len; i++) pw += pool[secureRandomInt(pool.length)];
document.getElementById('pw').textContent = pw;
const sc = score(pw);
const bar = document.getElementById('strength');
bar.style.width = sc + '%';
bar.style.background = sc < 50 ? '#ef4444' : sc < 75 ? '#f59e0b' : '#22c55e';
document.getElementById('label').textContent = sc < 50 ? 'Weak' : sc < 75 ? 'Fair' : 'Strong';
}
document.getElementById('len').oninput = (e) => {
document.getElementById('lenVal').textContent = e.target.value;
};
document.getElementById('gen').onclick = generate;
document.getElementById('copy').onclick = () => {
navigator.clipboard.writeText(document.getElementById('pw').textContent);
};
generate();
</script>
</body>
</html>
Ambiguous characters removed
No 0/O or 1/l in charset — fewer support tickets.
Clipboard on HTTP
navigator.clipboard needs HTTPS or localhost. Fallback:
try { await navigator.clipboard.writeText(pw); }
catch { prompt('Copy:', pw); }
Verify
Generate 20 × 20-char passwords — all unique Uncheck all boxes → alert Strength bar turns green at length 20 with all charsets