Tier 1 โ€” Beginner

Password Generator

Generate secure passwords with Fisher-Yates shuffle and guaranteed character types.

๐Ÿ Python ๐ŸŸจ JavaScript

1. The Problem

Build a secure password generator that creates passwords of a specified length using a configurable mix of uppercase, lowercase, digits, and symbols. Guarantee at least one of each required character type. Rate the strength of each password.

Why this project? String manipulation, configurable options objects, the Fisher-Yates shuffle (avoiding biased randomness), and security thinking.

2. How to Think About This

The naive approach (pick randomly from the full pool) can produce a "strong" password that accidentally has no digits or no symbols โ€” failing its own requirements. The fix: first pick one character from each required set, then fill the rest from the combined pool, then shuffle so the guaranteed characters aren't always first.

Shuffle must be Fisher-Yates (not .sort(() => Math.random() - 0.5) which is statistically biased).

3. The Build

pwgen.mjs
import { argv } from "process";

const CHARS = {
  upper:   "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
  lower:   "abcdefghijklmnopqrstuvwxyz",
  digits:  "0123456789",
  symbols: "!@#$%^&*()-_=+[]{}|;:,<>?",
};

// Pick one random character from a string
function pick(str) {
  return str[Math.floor(Math.random() * str.length)];
}

// Fisher-Yates shuffle โ€” unbiased, in-place
function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];   // swap
  }
  return arr;
}

// Generate a password with given options
export function generatePassword({
  length  = 16,
  upper   = true,
  lower   = true,
  digits  = true,
  symbols = true,
} = {}) {
  if (length < 4 || length > 128) throw new RangeError("Length must be 4โ€“128");

  const sets = [
    upper   ? CHARS.upper   : "",
    lower   ? CHARS.lower   : "",
    digits  ? CHARS.digits  : "",
    symbols ? CHARS.symbols : "",
  ].filter(Boolean);

  if (!sets.length) throw new Error("Select at least one character type");
  const pool = sets.join("");

  // Guarantee one from each active set:
  const guaranteed = sets.map(set => pick(set));

  // Fill the rest from the full pool:
  const rest = Array.from({ length: length - guaranteed.length }, () => pick(pool));

  // Shuffle so guaranteed chars aren't always at the start:
  return shuffle([...guaranteed, ...rest]).join("");
}

// Strength rating (0โ€“5)
export function strength(pw) {
  let score = 0;
  if (pw.length >= 12)           score++;
  if (pw.length >= 16)           score++;
  if (/[A-Z]/.test(pw))          score++;
  if (/[a-z]/.test(pw))          score++;
  if (/\d/.test(pw))             score++;
  if (/[^A-Za-z0-9]/.test(pw))  score++;
  const labels = ["Very Weak","Weak","Fair","Good","Strong","Very Strong"];
  return { score, label: labels[Math.min(score, 5)] };
}

// CLI: node pwgen.mjs [length] [count]
const length = parseInt(argv[2] ?? "16", 10) || 16;
const count  = parseInt(argv[3] ?? "5",  10) || 5;

if (isNaN(length) || length < 4 || length > 128) {
  console.error("Length must be 4โ€“128"); process.exit(1);
}

console.log(`Generating ${count} password${count===1?"":"s"} (length ${length}):
`);
for (let i = 0; i < count; i++) {
  const pw = generatePassword({ length });
  const { label } = strength(pw);
  console.log(`  ${pw}  [${label}]`);
}

4. Test & Prove

pwgen.test.mjs
import { generatePassword, strength } from "./pwgen.mjs";

let pass=0,fail=0;
function test(desc,fn){try{fn();console.log("  โœ“",desc);pass++;}catch(e){console.log("  โœ—",desc,"โ€”",e.message);fail++;}}

// Statistical: run many times to check invariants
const N = 500;
test("always correct length", () => {
  for(let i=0;i<N;i++){const pw=generatePassword({length:12});if(pw.length!==12)throw new Error(pw.length);}
});
test("always has uppercase (when enabled)", () => {
  for(let i=0;i<N;i++){const pw=generatePassword({upper:true,lower:true,digits:true,symbols:false});
    if(!/[A-Z]/.test(pw))throw new Error("missing upper: "+pw);}
});
test("always has digit (when enabled)", () => {
  for(let i=0;i<N;i++){const pw=generatePassword();if(!/\d/.test(pw))throw new Error("missing digit: "+pw);}
});
test("digits-only contains no letters", () => {
  for(let i=0;i<50;i++){const pw=generatePassword({upper:false,lower:false,digits:true,symbols:false});
    if(/[a-zA-Z!@#]/.test(pw))throw new Error("found non-digit: "+pw);}
});
test("throws on length < 4",  () => { try{generatePassword({length:2});throw new Error("no throw");}catch(e){if(e.message==="no throw")throw e;} });
test("throws on no char sets", () => { try{generatePassword({upper:false,lower:false,digits:false,symbols:false});throw new Error("no throw");}catch(e){if(e.message==="no throw")throw e;} });
test("16-char password rates Strong or better", () => {
  const pw=generatePassword({length:16}); const {score}=strength(pw); if(score<4)throw new Error(score);
});
console.log(\`
\${pass} passed, \${fail} failed\`);

5. The Interface

Input: optional CLI args โ€” node pwgen.mjs [length] [count]. Defaults: length 16, count 5.

Output: one password per line with its strength rating.

$ node pwgen.mjs 20 3
Generating 3 passwords (length 20):

  Kx#2mPqL9!vRt$nYeJ@8  [Very Strong]
  7bN*cWd4^sFhZo!rPuQ1  [Very Strong]
  Gm$8kVn#2LpRt!xYeJ9@  [Very Strong]

6. Run It

node pwgen.mjs          # 5 passwords, length 16
node pwgen.mjs 24       # 5 passwords, length 24
node pwgen.mjs 8 10     # 10 passwords, length 8
node pwgen.test.mjs     # run tests (500 iterations)

๐ŸŽฏ Try this next

  1. Use crypto.getRandomValues() (browser) or crypto.randomInt() (Node) for cryptographically secure randomness
  2. Add a --no-symbols flag using process.argv parsing
  3. Add a "pronounceable" mode โ€” alternating consonant/vowel for easier memorisation
  4. Build a browser version with a copy-to-clipboard button using navigator.clipboard.writeText()

What you practised

Default parameter objects, guaranteed character inclusion via seeded arrays, Fisher-Yates shuffle (the correct shuffle algorithm), statistical testing (run 500 times), and the difference between biased .sort(Math.random) and unbiased Fisher-Yates.

Reference: Arrays ยท Functions ยท Regular Expressions

Try It Yourself