Check a secret's entropy

Turn away passwords and keys that are plainly weak, such as aaaaaaaaaaaaaaaa or 12345678abcdefgh, before you store them.

entropy looks at a string or byte array and tells you how varied it is: how evenly its characters are spread, how predictable each pair of neighbors is, and how long its longest sorted stretch runs. That's enough to catch a password at sign-up, or a key in a config file, that plainly isn't random. It can't vouch for anything, though. A high score proves nothing about strength, so this check only ever gets to say no.

#Before you start

  • It's synchronous and runs on every runtime listed in Installation. It reads a string by Unicode code point, so é counts once, and a Uint8Array byte by byte.
  • It sees only the string in front of it, not how that string was chosen. correct horse battery staple scores 98 bits and is in every password-cracking dictionary.
  • Never accept a secret because it scored well. Pair this check with a length minimum and a blocklist of breached passwords, as NIST SP 800-63B-4 §3.1.1.2 requires.
  • For a secret you make yourself, skip the check and use secureGenerate.

#Reject a weak password at sign-up

This function checks a new password against a few rules. When one catches it, you get back a message to show on the sign-up form.

password-policy.ts
/**
 * Why a new password is too weak, or `undefined` when no rule caught it.
 * `undefined` is not a verdict: a phrase from a cracking dictionary can
 * pass every rule here, so check it against a breached-password list too.
 */
export function weakPasswordReason(password: unknown): string | undefined {
  // `entropy` throws INVALID_TYPE for anything but text or bytes.
  if (typeof password !== "string") return "Enter a password.";

  const { symbolCount, uniqueSymbols, longestRun, bits } = entropy(password);
  if (symbolCount < 15) return "Use at least 15 characters.";
  if (uniqueSymbols < 6) return "Use more different characters.";
  if (longestRun >= 8) return "Avoid sequences such as 12345678 or abcdefgh.";
  if (bits < 45) return "Repeat characters less often.";
  return undefined;
}

Each rule catches one kind of weakness, and each threshold sits where a randomly made password almost never trips it:

  • Type first: a form field can arrive as anything at all. entropy throws for a value that isn't text or bytes, so the function checks the type before it calls.
  • Length: symbolCount counts code points, the numbers Unicode gives each character, not UTF-16 units. 15 is the minimum that NIST SP 800-63B-4 §3.1.1.2, the US digital identity guideline, sets for a password used without a second factor.
  • Runs: longestRun counts characters that keep going up or keep going down, such as 1357 or zyx, not only runs of neighboring code points like 1234 or abcd. Random text has short runs too: about one 16-character random password in seven holds a run of 5, and one in 3,400 a run of 8. A threshold of 8 catches 12345678 and leaves random passwords alone.
  • Repetition: bits drops when a few characters do most of the work, as in aaaaaaaaaaaabcdef.

#Why one number is not enough

The bits field is a guess made from the characters in front of it, not the strength of whatever made them (Concepts explains the difference). It counts how often each character shows up and pays no attention to their order. So the alphabet in order scores as well as noise over the same 16 letters, and longestRun is what gives it away:

entropy("abcdefghijklmnop");
// { bits: 64, uniqueSymbols: 16, longestRun: 16, monotonicDirection: "ascending", … }

#When it fails

Only a value of the wrong type throws. A weak secret is a low score, never an error:

SituationWhat happens
data is null, undefined or emptyReturns every count as 0 and monotonicDirection: "none".
data is a number, an object, an array, an ArrayBuffer or a typed array other than Uint8ArrayThrows INVALID_TYPE. Check the type of a form field before you call.
A predictable secret, such as a dictionary phraseScores high. This function can't tell.

#Fields you will read

entropy(data) hands back one object with these fields:

FieldNotes
bitsShannon entropy of the whole input in bits, order ignored.
bitsPerSymbolbits divided by symbolCount. At most 8 for bytes.
symbolCountCharacters, or bytes, in the input.
uniqueSymbolsDistinct characters or byte values.
longestRunThe longest stretch that keeps rising or keeps falling.
monotonicDirection"ascending", "descending", or "none" when neither direction has the longer run.
bigramBits, bigramBitsPerSymbolThe same measure over neighboring pairs, which catches ababab. Its ceiling depends on input length, so compare it against a random input of the same length rather than a fixed number.

#Going further

A few notes on random bytes, and on the checks that belong next to this one:

  • Checking random bytes: 4096 random bytes score about 7.96 bits per byte, but 256 random bytes only about 7.2. That's because 256 draws over 256 values leave about a third of the values unseen. So compare against a known random sample of the same length, not a fixed number.
  • Better password estimators: an estimator that knows dictionaries, such as zxcvbn, catches names, dates and keyboard walks like qwertyuiopasdfghjk, which sails through every rule on this page.
  • Breached passwords: the Have I Been Pwned range API tells you whether a password shows up in known breaches, without the password ever leaving your server.