NNicheStackTutorials
← All tutorials
Frontend Tools

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.

·10 min read·Frontend Tools
passgen: Cryptographically Random Passwords Without crypto.randomUUID Hacks cover

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

Tags: SecurityPasswordcryptoVanilla JS