Passphrase Generator — JavaScript source
Generate memorable, high-entropy passphrases from the EFF diceware wordlist using cryptographic randomness. Each word is drawn with crypto.getRandomValues and rejection sampling (no modulo bias), and every passphrase shows its exact entropy in bits plus the estimated crack time at 1 trillion guesses/sec. Everything runs locally - nothing is sent anywhere.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* Passphrase Generator — diceware generation + entropy scoring.
*
* Language: JavaScript (ES2022, Web Crypto for the CSPRNG)
* Source: CosmoDev polyglot showcase port of the Passphrase Generator tool,
* ported from src/lib/passphrase-generator.ts (the canonical
* TypeScript implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Word selection uses crypto.getRandomValues with rejection sampling, so
* every word index is uniform over the 7,776-word EFF long wordlist — no
* modulo bias. The wordlist is too large to inline; pass it in as an array
* of strings (one word per line of the EFF list).
*/
const EFF_WORDLIST_LENGTH = 7776;
const MIN_WORDS = 3;
const MAX_WORDS = 10;
const DEFAULT_WORDS = 6;
const DEFAULT_GUESSES_PER_SECOND = 1e12;
const SEPARATORS = { space: ' ', dash: '-', dot: '.', underscore: '_', none: '' };
// Largest multiple of 7,776 that fits in [0, 65536): 7776 * 8 = 62208.
// Draws at or above the limit are rejected so v % 7776 is uniform.
const WORD_LIMIT = EFF_WORDLIST_LENGTH * Math.floor(65536 / EFF_WORDLIST_LENGTH);
const YEAR = 31_557_600; // seconds (Julian year)
const SCALES = [
[1e12, 'trillion'],
[1e9, 'billion'],
[1e6, 'million'],
[1e3, 'thousand'],
];
/** One uniformly-random word from the EFF long wordlist (CSPRNG). */
function randomWord(wordlist) {
const buf = new Uint16Array(1);
let v;
do {
crypto.getRandomValues(buf);
v = buf[0];
} while (v >= WORD_LIMIT);
return wordlist[v % EFF_WORDLIST_LENGTH];
}
/** One uniformly-random digit 0-9 (CSPRNG, rejection-sampled on uint8). */
function randomDigit() {
const buf = new Uint8Array(1);
let v;
do {
crypto.getRandomValues(buf);
v = buf[0];
} while (v >= 250); // largest multiple of 10 in [0, 256)
return String(v % 10);
}
/** Theoretical word entropy in bits: wordCount * log2(7776). */
function entropyBits(wordCount) {
return wordCount <= 0 ? 0 : wordCount * Math.log2(EFF_WORDLIST_LENGTH);
}
/** Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond. */
function crackTimeSeconds(bits, guessesPerSecond = DEFAULT_GUESSES_PER_SECOND) {
return Math.pow(2, bits - 1) / guessesPerSecond;
}
function span(v, singular, plural = `${singular}s`) {
const n = round(v);
return `${locale(n)} ${n === 1 ? singular : plural}`;
}
function round(v) {
return v >= 10 ? Math.round(v) : Math.round(v * 10) / 10;
}
function locale(n) {
return n.toLocaleString('en-US'); // 12,345 — floats below 1000 keep 1 decimal
}
/** Human-readable crack-time span; collapses to scaled words at diceware
* entropies so the string stays readable ("2.4 billion centuries"). */
function formatCrackTime(seconds) {
if (!Number.isFinite(seconds) || seconds < 0) return '-';
if (seconds < 1) return '< 1 second';
if (seconds < 60) return span(seconds, 'second');
if (seconds < 3600) return span(seconds / 60, 'minute');
if (seconds < 86_400) return span(seconds / 3600, 'hour');
if (seconds < YEAR) return span(seconds / 86_400, 'day');
const years = seconds / YEAR;
if (years < 100) return span(years, 'year');
if (years < 1000) return span(years / 100, 'century', 'centuries');
if (years < 1e6) return span(years / 1000, 'millennium', 'millennia');
const centuries = years / 100; // >= 10,000 here: collapse to a scaled plural
const [factor, name] = SCALES.find(([f]) => centuries >= f);
return `${locale(round(centuries / factor))} ${name} centuries`;
}
/**
* Generate a diceware passphrase from the EFF long wordlist. Options (all
* optional): wordCount, separator ('space'|'dash'|'dot'|'underscore'|'none'),
* capitalize, appendDigit, guessesPerSecond (attack speed; 1e12 default).
*/
function generatePassphrase(wordlist, options = {}) {
const raw = options.wordCount ?? DEFAULT_WORDS;
let wordCount = Number.isFinite(raw) ? Math.round(raw) : DEFAULT_WORDS;
wordCount = Math.min(MAX_WORDS, Math.max(MIN_WORDS, wordCount));
const separator = SEPARATORS[options.separator ?? 'space'] ?? ' ';
const capitalize = options.capitalize ?? false;
const appendDigit = options.appendDigit ?? false;
const gps = options.guessesPerSecond ?? DEFAULT_GUESSES_PER_SECOND;
const words = Array.from({ length: wordCount }, () => randomWord(wordlist));
const shown = capitalize ? words.map((w) => w[0].toUpperCase() + w.slice(1)) : words;
let passphrase = shown.join(separator);
if (appendDigit) passphrase += randomDigit();
const entropy = entropyBits(wordCount);
const seconds = crackTimeSeconds(entropy, gps);
return {
words,
passphrase,
entropy,
crackTime: { seconds, human: formatCrackTime(seconds) },
};
}
// Example (deterministic parts only):
// entropyBits(6); // 77.548875... bits
// formatCrackTime(crackTimeSeconds(entropyBits(6))); // "3.5 millennia"
// formatCrackTime(crackTimeSeconds(entropyBits(8))); // "2.1 billion centuries"
// generatePassphrase(effWordlist, { separator: 'dash', capitalize: true,
// appendDigit: true });
// // -> { passphrase: "Oatmeal-Marker-Ranger-...-7", entropy: 77.55, ... }
Also available in 12 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →