Bulk password generator

Generate a batch of independent passwords for account provisioning, in your browser, with nothing sent anywhere.

G
More options
up to 10,000

Constraints

What a batch is actually for

The usual reason to want fifty passwords at once is provisioning: onboarding a cohort of users, seeding accounts in a new system, creating service credentials for a fleet, or building a fixture set for a test environment. Each value here is drawn independently from the same source as a single password, so a batch of fifty 16-character passwords is fifty separate 99.11-bit secrets rather than fifty variations on one.

Duplicates within a batch are not something you need to think about at this size. With 99 bits per value, the chance that any two of fifty collide is smaller than any risk you are currently managing. The uniqueness that matters here is a different one: never issue the same password to two accounts deliberately, which happens more often than accidental collision ever will, usually because someone generated one value and pasted it into every row.

Generation happens in this page. The batch never reaches a server, appears in a request body, or lands in a server log. That property matters more for a list of fifty live credentials than it does for a single value, because a list is exactly the artefact that gets forwarded.

Distribution is the hard part

A batch of passwords is at its most exposed between the moment it exists and the moment each recipient has changed theirs. Almost every incident involving provisioned credentials happens in that window, and none of them involve the randomness being weak.

Do not email the list. Mail sits in two mailboxes, in server storage, in backups, and often in a search index, and it is forwarded without thought. The same objection applies to pasting a batch into a ticket, a wiki page, a shared document or a chat channel, all of which are searchable by people who were never meant to see it and retained long after the passwords stopped being needed.

What works is a per-recipient channel with a short life. A one-time secret link that expires on first view or after a set period delivers a single password to a single person and leaves nothing behind. Out-of-band delivery, where the account name travels one way and the password another, limits what any single compromised channel yields. For a genuinely sensitive credential, hand delivery in a sealed envelope is not old fashioned, it is the option with the smallest attack surface. Whatever you choose, the file holding the batch should exist for minutes and then be deleted, and it should never be written to a synced or backed-up folder.

  • One password per recipient, over a channel only that recipient can read.
  • Never in email, chat, a ticket, a wiki or a shared spreadsheet.
  • Delete the batch file as soon as distribution is done, and keep it out of synced folders.
  • Keep a record of which account was provisioned, never of what the password was.

Expire every password at first use

A provisioned password is a bootstrap credential, not a password. Its only job is to let the right person authenticate once so they can set a real one, and it should be configured to expire at that point. Every serious directory and identity system supports a change-at-next-logon flag, and using it converts the distribution window from an ongoing exposure into a single brief one.

Give the credential a short absolute expiry as well, so an unclaimed account does not sit with a known password for months. Accounts that are never claimed should be disabled rather than left waiting. Where multi-factor authentication is required, enrol at the same first login, because a bootstrap password and no second factor is the weakest state the account will ever be in.

One thing not to do is force periodic rotation afterwards. NIST SP 800-63B-4 requires a change on evidence of compromise, not on a schedule, and scheduled expiry pushes people toward predictable increments of a password they already chose. Forced change at first use is a different mechanism serving a different purpose, and it is the one that is justified.

If you cannot force a change at first use, treat every password in the batch as compromised from the moment it is sent, and plan the account permissions accordingly.

Export formats, and the traps in them

A batch is usually going somewhere structured, so the export options matter. Plain lines suit a paste into a provisioning script. JSON suits an API call. CSV suits a mail merge or an import into a directory tool. Each has a hazard worth knowing before you use it.

CSV is the one that bites. A password beginning with an equals sign, a plus, a hyphen or an at sign is interpreted as a formula when the file is opened in Excel or Google Sheets. The visible value changes, so the password you distribute is not the password you generated, and a crafted value in that position is a genuine injection route. Every field in our CSV export is quoted, but quoting is a convention the reading application chooses to honour, so verify a sample after import rather than assuming.

The URL case is quieter. A plus sign in an unencoded query string decodes to a space, so a password embedded in a link can arrive silently altered. Percent-encode anything you put in a URL, or use a delivery mechanism that carries the password in a body rather than a path. And when writing to an environment file, remember that shell-significant characters in an unquoted value will be interpreted by whatever reads it.

Choosing a format
FormatGood forWatch for
Plain linesPiping into a scriptNo association between password and account
CSVDirectory import, mail mergeLeading = + - or @ read as a formula by spreadsheets
JSON arrayAPI-driven provisioningEnds up in request logs if the endpoint logs bodies
.env linesService credentialsUnquoted shell-significant characters

Doing this in your own code

JavaScript
// 74-character alphabet: A-Z a-z 0-9 and the 12 safe symbols.
const ALPHABET =
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_";

// Rejection sampling. v % n on a raw uint32 is biased whenever n does not
// divide 2^32, and 74 does not.
function pick(n) {
  const limit = Math.floor(2 ** 32 / n) * n;
  const buf = new Uint32Array(1);
  let v;
  do {
    crypto.getRandomValues(buf);
    v = buf[0];
  } while (v >= limit);
  return v % n;
}

function password(length = 20) {
  let out = "";
  for (let i = 0; i < length; i++) out += ALPHABET[pick(ALPHABET.length)];
  return out;
}

Math.random() is never acceptable here. V8 implements it with xorshift128+, whose internal state is recoverable from a handful of outputs.

Python 3.6+
import secrets
import string

ALPHABET = string.ascii_letters + string.digits + "!#$%*+-=?@^_"

def password(length: int = 20) -> str:
    return "".join(secrets.choice(ALPHABET) for _ in range(length))

# With a required-class check, resample the WHOLE string on failure.
def password_all_classes(length: int = 20) -> str:
    while True:
        p = password(length)
        if (any(c.isupper() for c in p) and any(c.islower() for c in p)
                and any(c.isdigit() for c in p)
                and any(c in "!#$%*+-=?@^_" for c in p)):
            return p

secrets.choice draws through randbelow, which rejection samples. random.choice rejection samples too, but it runs on the Mersenne Twister and must not be used for a password.

Go
import (
    "crypto/rand"
    "math/big"
)

const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_"

func Password(n int) (string, error) {
    b := make([]byte, n)
    max := big.NewInt(int64(len(alphabet)))
    for i := range b {
        v, err := rand.Int(rand.Reader, max)
        if err != nil {
            return "", err
        }
        b[i] = alphabet[v.Int64()]
    }
    return string(b), nil
}

crypto/rand.Int rejection samples internally. math/rand is seeded and predictable, and importing the wrong one is a common review miss.

PHP 7+
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_';
$n = strlen($alphabet);

$password = '';
for ($i = 0; $i < 20; $i++) {
    $password .= $alphabet[random_int(0, $n - 1)];
}

random_int throws rather than falling back to a weak source. rand() and mt_rand() are not suitable for credentials.

Java
import java.security.SecureRandom;

static final String ALPHABET =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_";

static String password(int length) {
    SecureRandom rnd = new SecureRandom();
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < length; i++) {
        sb.append(ALPHABET.charAt(rnd.nextInt(ALPHABET.length())));
    }
    return sb.toString();
}

nextInt(bound) rejection samples. Do not build one from nextDouble or from nextInt() % n.

C# / .NET 8+
using System.Security.Cryptography;

const string Alphabet =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%*+-=?@^_";

string Password(int length) =>
    new string(RandomNumberGenerator.GetItems<char>(Alphabet, length));

RandomNumberGenerator.GetItems arrived in .NET 8 and is unbiased. On older targets, loop over GetInt32(0, Alphabet.Length).

Shell
# tr discards every byte outside the set, which is rejection sampling by
# construction. The hyphen must stay last inside the bracket.
LC_ALL=C tr -dc 'A-Za-z0-9!#$%*+=?@^_-' < /dev/urandom | head -c 20; echo

# A 192-bit token, if you do not need a typeable password. 24 bytes is
# 32 base64 characters with no padding, in the standard +/ alphabet.
openssl rand -base64 24

Without LC_ALL=C, tr in a UTF-8 locale can behave differently on high bytes.

Questions

How many passwords can I generate at once?

The page defaults to 50, which suits most provisioning runs, and the count control accepts much larger batches. Generation is chunked so the browser stays responsive and the run can be cancelled, rather than freezing the tab while it works.

Could two passwords in a batch be identical?

In principle, in the same way any two random values could match. At 99 bits each, the probability across a batch of fifty is far below anything worth mitigating. The uniqueness risk in practice is administrative: issuing one password to several accounts because someone copied a single row.

Is it safe to generate credentials for other people on a website?

It is safe on a page that generates locally, which you can verify by opening the Network tab and watching nothing happen while you generate. It is not safe on a page that generates on a server, because the credentials then exist in a response body and in the logs behind it. Seven of the fifteen generators we audited generate server-side.

Should the batch have the same length for every password?

Yes, unless different systems impose different limits. A uniform length is easier to audit, and varying it per row buys nothing. Where a target system caps below your chosen length, generate a separate batch at that length rather than letting the field truncate.

What do I do with the file afterwards?

Delete it once every recipient has authenticated and changed their password. Keep the record of which accounts were provisioned and when, and do not keep the passwords themselves. If the file was written to a synced folder, remember that deleting the local copy may not remove the versions the sync service retained.

Related generators