DEV Community

Password Generator
Password Generator

Posted on

Building a Secure Password Generator with Web Crypto API: No Servers, Pure Browser Power

Hey fellow devs! In a world where data breaches make headlines weekly, weak passwords are like leaving your front door unlocked in a sketchy neighborhood. We've all been there—scrambling to create a "strong" password on the fly, only to end up with something forgettable and crackable. What if I told you you could build a lightning-fast, ultra-secure password generator that runs entirely in the browser, without sending a single byte to a server?

That's exactly what I did with PasswordGenerator.cam, a free tool powered by the Web Crypto API. It's customizable, truly random, and respects user privacy from the get-go. In this article, I'll walk you through how I built it, why it's secure, and how you can fork or extend it for your own projects. Let's dive in—no fluff, just code and crypto.

The Problem: Why Most Password Generators Fall Short

Before we code, let's set the stage. Traditional password generators often:

  • Rely on server-side randomness, which can introduce biases or logging risks.
  • Use pseudo-random number generators (PRNGs) like Math.random(), which aren't cryptographically secure and can be predicted under certain attacks.
  • Lack customization without bloating the UI.

According to NIST guidelines (SP 800-63B), passwords should be at least 8 characters but ideally longer, with a mix of character sets for entropy. But entropy is king—true randomness ensures a password like X7!pQ2#kL9$mV4 isn't guessable in a million years.

Enter the Web Crypto API: A browser-native interface for cryptographic operations, including crypto.getRandomValues(). It's hardware-backed where possible (e.g., via TPM on desktops) and uniform across modern browsers. No npm packages needed—just vanilla JS.

Step 1: Setting Up the Basics (HTML + CSS)

We start simple. A static HTML page with inputs for customization and a generate button. Here's the skeleton:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Secure Password Generator</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
    input[type="range"] { width: 100%; }
    button { background: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; }
    #password { font-family: monospace; font-size: 1.2em; word-break: break-all; }
  </style>
</head>
<body>
  <h1>Generate a Bulletproof Password</h1>

  <label>Length: <span id="lengthValue">16</span></label>
  <input type="range" id="length" min="8" max="200" value="16">

  <label><input type="checkbox" id="uppercase" checked> Uppercase Letters</label>
  <label><input type="checkbox" id="lowercase" checked> Lowercase Letters</label>
  <label><input type="checkbox" id="numbers" checked> Numbers</label>
  <label><input type="checkbox" id="symbols" checked> Symbols</label>

  <button onclick="generatePassword()">Generate</button>

  <p>Generated: <span id="password"></span></p>

  <script src="script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This gives us a clean UI. The range slider for length (8-200 chars—overkill? Maybe, but why not?) and checkboxes for charset selection. Pro tip: Default to "on" for all to encourage strong defaults.

Step 2: The Magic—Generating Secure Randomness in JS

Now, the core logic in script.js. We use crypto.getRandomValues() to fill a Uint8Array, then map bytes to characters. This ensures uniform distribution—no server pings, no data leaks.

function generatePassword() {
  const length = parseInt(document.getElementById('length').value);
  const uppercase = document.getElementById('uppercase').checked;
  const lowercase = document.getElementById('lowercase').checked;
  const numbers = document.getElementById('numbers').checked;
  const symbols = document.getElementById('symbols').checked;

  // Build the charset based on selections
  let charset = '';
  if (uppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  if (lowercase) charset += 'abcdefghijklmnopqrstuvwxyz';
  if (numbers) charset += '0123456789';
  if (symbols) charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';

  if (charset.length === 0) {
    alert('Select at least one character type!');
    return;
  }

  // Generate secure random bytes
  const randomBytes = new Uint8Array(length);
  crypto.getRandomValues(randomBytes);

  // Map bytes to charset indices
  let password = '';
  for (let i = 0; i < length; i++) {
    const randomIndex = randomBytes[i] % charset.length;
    password += charset[randomIndex];
  }

  document.getElementById('password').textContent = password;
  document.getElementById('lengthValue').textContent = length; // Update slider display
}

// Update length display on slider change
document.getElementById('length').addEventListener('input', function() {
  document.getElementById('lengthValue').textContent = this.value;
});
Enter fullscreen mode Exit fullscreen mode

Key Security Notes:

  • crypto.getRandomValues() is the gold standard for CSPRNG in browsers. It throws an error in insecure contexts (e.g., non-HTTPS), which is a feature, not a bug.
  • We modulo the byte value against charset length for even distribution. For extra pedantry, you could use rejection sampling to avoid bias, but for passwords, this is plenty secure (entropy ~ log2(charset^length)).
  • No clipboard API here to avoid auto-copy pitfalls—users copy manually for awareness.

Test it: Open in Chrome DevTools, generate a few, and log password.length. Boom—randomness in <1ms.

Step 3: Enhancements for Dev Delight

To make it production-ready (like on PasswordGenerator.cam), I added:

  • Strength Meter: Use zxcvbn (a lightweight library) to score entropy. Include via CDN: <script src="https://cdnjs.cloudflare.com/ajax/libs/zxcvbn/4.4.2/zxcvbn.min.js"></script>.
  // After generating...
  const strength = zxcvbn(password).score;
  const meter = document.createElement('div');
  meter.textContent = `Strength: ${['Weak', 'Fair', 'Good', 'Strong', 'Unbreakable'][strength]}`;
  meter.style.color = strength > 2 ? 'green' : 'red';
  document.body.appendChild(meter); // Or integrate into UI
Enter fullscreen mode Exit fullscreen mode
  • Export/QR Code: For mobile, generate a QR of the password using a lib like qrcode.js.
  • PWA Support: Add a manifest.json for offline use—perfect for "generate on the go."

Edge cases? Handle short charsets gracefully and warn if entropy dips below 80 bits (NIST rec).

Why Browser-Only Wins for Privacy & Performance

Servers add latency and trust issues—why log user prefs? With static hosting (GitHub Pages, Netlify), your site loads in milliseconds. Web Crypto is audited, cross-browser (Chrome 37+, Firefox 21+), and future-proof.

On PasswordGenerator.cam, we've served thousands without a single backend call. Users love the speed; devs appreciate the open-source vibe (fork it on GitHub!).

Wrapping Up: Secure Your Side Projects (and the World)

Building this took an afternoon, but the impact? Lifelong better habits for users. Next time you're auditing auth flows, drop in a custom generator—it's a quick win.

Try it live at passwordgenerator.cam and let me know: What's your go-to charset combo? Drop a comment below, share your forks, or tweet me @pgdotcam with improvements.

Top comments (0)