DEV Community

Cover image for Rolling Your Own Password Generator in Code: When It's the Right Call and How to Stop Getting It Wrong
Tea-sip for Lizely

Posted on

Rolling Your Own Password Generator in Code: When It's the Right Call and How to Stop Getting It Wrong

Most password "guides" skip past the engineering side. They tell you to use a manager, click a button, and move on. But the moment you sit down to wire authentication into a script, a CLI, a CI job, or a small internal tool, the question gets specific fast: do I shell out to an existing generator, call an API, or generate the password inside the program I am already writing?

This piece is for engineers making that call. It covers the cases where generating the password in code is the right answer, the constraints you actually hit, the patterns that hold up under review, and the patterns that get rejected. The in-depth walkthrough for the on-the-spot case — humans typing into a browser — lives in the Lizely guide on generating a secure password using Google and local tools, which is worth bookmarking. What follows is a different problem: programmatic generation, inside your own stack.

When Programmatic Generation Is the Right Choice

There are four situations where generating a password inside your code, rather than asking a human to paste one in, is the correct engineering decision.

First, seeding service accounts at deploy time. A deploy script that creates a database user, an S3 IAM user, or a service principal needs a credential to put somewhere — typically a secret manager. Nobody should be copy-pasting it from a browser. The generator should run inline, write the secret, and never echo it.

Second, generating one-time passwords for invitations. When you invite a contractor, issue a vendor API key, or open a temporary portal account, the password is a payload, not a workflow. It is generated, emailed or displayed once, and rotated on first login.

Third, test fixtures. Reproducible CI runs often need deterministic credentials — same length, same charset — so that downstream assertions about format, not entropy, are stable.

Fourth, breaking glass. The "emergency admin" credential that lives in a vault for incident response. Generating it once at provisioning time, storing it sealed, and never displaying it again.

If your case is not one of those four, you almost certainly want a manager and a human in the loop. The rest of this article assumes you are in one of them.

The Three Constraints That Actually Matter

When reviewers push back on homegrown generators, it is almost never about length. It is about three things that experienced security engineers have seen go wrong repeatedly.

Entropy Source

A Math.random() call in JavaScript, a random.randint() in Python, or a rand() in C is not suitable. These are deterministic PRNGs seeded from low-entropy state. A 32-character password built from Math.random() collapses to a search space much smaller than 32 characters, because the underlying generator state is recoverable from a small number of outputs.

You need a CSPRNG. The acronym stands for cryptographically secure pseudorandom number generator, and the property you care about is that even an attacker who has seen every previous output cannot predict the next one with better than 50/50 odds. Most languages expose this under a different name:

  • Python: secrets module — built specifically for tokens and passwords.
  • Node.js: crypto.randomBytes() or crypto.randomInt().
  • Go: crypto/rand.
  • Java: java.security.SecureRandom.
  • Rust: rand crate with the OsRng adapter.
  • C/C++: read from /dev/urandom or call BCryptGenRandom on Windows.

The Python secrets documentation is a particularly clear reference: it explicitly contrasts itself with random and lists the exact failure modes. Worth reading once even if you do not write Python.

Output Bias

Once you have a CSPRNG, the next trap is mapping uniform bytes onto a restricted character set. If your charset is 62 characters (a–z, A–Z, 0–9) and you take a random byte mod 62, the first six residues are slightly more likely than the rest. For 16-character passwords this is negligible. For API keys that get generated millions of times and then brute-forced, it is measurable.

The fix is rejection sampling: read bytes, accept only those below the largest multiple of your charset size that does not exceed 256, and resample the rest. Or use a base-N encoding trick. secrets.choice in Python and crypto.randomInt in Node both handle this for you when you ask for a range, but if you are building the alphabet yourself, audit the mapping.

For the deepest treatment, the NIST SP 800-90A recommendation series describes the entropy-extraction patterns that standardized generators use. You do not need to implement it from scratch — the point is to recognize the failure mode when reviewing someone else's code.

Logging and Side Channels

The third constraint is the one that ships the bug. A password gets generated, then a stack trace gets printed, then someone greps logs six months later and finds the credential. The patterns I have seen:

  • An exception handler that logs its arguments, including a generated password that was passed in.
  • A debug print of "request body" that serializes the whole payload.
  • A test harness that captures stdout and stores it.

The mitigation is mechanical: generate the password as late as possible, hand it directly to the secret manager or the response object, and never let it touch a variable that has a debug representation. In Python this means returning from a function whose return value is wrapped immediately; in Go it means passing a []byte rather than a string so that %s formatting does not reveal it.

A Concrete Pattern That Survives Review

Here is a minimal pattern in Python that hits the three constraints above and is short enough to read in one screen. It uses the standard secrets module, applies rejection sampling implicitly through secrets.choice, and returns the result without printing anything.

import secrets
import string

ALPHABET = string.ascii_letters + string.digits
LENGTH = 24

def generate_password(length: int = LENGTH) -> str:
    return ''.join(secrets.choice(ALPHABET) for _ in range(length))
Enter fullscreen mode Exit fullscreen mode

For Node, the equivalent is roughly:

const crypto = require('node:crypto');

const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generatePassword(length = 24) {
  const bytes = crypto.randomBytes(length * 2);
  let out = '';
  for (let i = 0; i < length; i++) {
    out += ALPHABET[bytes[i] % ALPHABET.length];
  }
  return out;
}
Enter fullscreen mode Exit fullscreen mode

Note the length * 2 on the randomBytes call — that is a cheap defense against output bias by oversampling. For high-volume key generation, switch to explicit rejection sampling:

function unbiased(length = 24) {
  const max = Math.floor(256 / ALPHABET.length) * ALPHABET.length;
  const out = [];
  while (out.length < length) {
    const buf = crypto.randomBytes(64);
    for (const b of buf) {
      if (b < max && out.length < length) {
        out.push(ALPHABET[b]);
      }
    }
  }
  return out.join('');
}
Enter fullscreen mode Exit fullscreen mode

Both versions are short, but only the second one is appropriate for keys that get rotated through an API and could be observed by an attacker.

When to Reach for a Library Instead

Hand-rolling is fine for the four cases above. It is not fine when:

  • You are generating passwords for end users at scale. The threat model changes from "credential is leaked through logs" to "credential is guessed online." You want a library that is audited for that case, plus rate limiting at the auth layer.
  • You need passphrases instead of passwords. Four random dictionary words from a 7,776-word list is approximately 51 bits of entropy, which is excellent for human memorability but requires word-list hygiene you do not want to maintain yourself.
  • You are signing or encrypting. A password generator is not a key derivation function. If your goal is to derive a key from a password, you want PBKDF2, scrypt, or Argon2 — three algorithms with very different performance and memory profiles, covered in their respective Wikipedia entries.

The dividing line I use in code review: if the generated string is going to be hashed with bcrypt or argon2 on the server side, the in-code generator is fine. If the generated string is going to be used as a key directly, or as input to encryption, stop and reach for a KDF.

Common Mistakes in Review

When I review a PR that contains a password generator, I look for exactly four things. They are a usable checklist.

  1. Source of randomness. Is it a CSPRNG? Grep for Math.random, random.randint, rand(, and mt_rand. Any hit is a reject.
  2. Alphabet size vs. length. A 6-character password from a 62-character alphabet is 35 bits. That is brute-forceable on a GPU farm in hours. Length matters more than character variety past a certain threshold.
  3. Logging surface. Does the variable holding the password ever appear in a format string, a JSON serializer, or an exception payload? If yes, rename it to something explicit and add a lint rule.
  4. Post-generation handling. Is the password written to a secret manager, or is it echoed? The former is correct. The latter is correct only for invitation flows where the consumer needs to see it once.

If those four are clean, the PR is approvable. If any fails, the PR is bounced with a comment naming the failure.

A Note on "No Characters That Look Alike"

You will be asked, at some point, to exclude characters that look alike — usually 0, O, 1, l, I. Resist this. It shrinks the alphabet, reduces entropy, and the actual usability benefit is small for a password that is copy-pasted once. The NIST SP 800-63B Digital Identity Guidelines explicitly recommend against composition rules, including this one, in favor of length. If a stakeholder insists, document the entropy loss so the decision is made consciously rather than by reflex.

Frequently Asked Questions

How long should a programmatically generated password be?

For service accounts and CI tokens, 24 characters from a 62-character alphabet is a sensible default — roughly 143 bits of entropy, well past any brute-force horizon. For invitation passwords that humans will type once before rotating, 16 is acceptable. For end-user passwords, length matters less than your rate limiting and storage hashing strategy.

Is crypto.randomUUID() good enough?

For tokens that get used as identifiers, yes. For passwords, no — UUIDs are drawn from a restricted bit pattern and produce strings with hyphens in fixed positions, which weakens them as passwords and makes them recognizable as UUIDs in logs. Use a CSPRNG directly.

Should I generate the password on the client?

Almost never. If the client generates it, the client has the plaintext, and you have lost control of where it lives. Generate server-side, transmit over TLS, and never store the plaintext past the initial issuance.

What about using openssl rand -base64 from a shell script?

It works for one-off provisioning, and the underlying CSPRNG is correct. The risks are the shell-script ones: the password lands in process listings, in shell history, and in any error output. Prefer a small language program that hands the value to your secret manager in the same process.


This article was drafted with AI assistance and reviewed for technical accuracy before publishing.

Top comments (0)