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 aUint8Arraybyte by byte. - It sees only the string in front of it, not how that string was chosen.
correct horse battery staplescores 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.
/**
* 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.
entropythrows for a value that isn't text or bytes, so the function checks the type before it calls. - Length:
symbolCountcounts 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:
longestRuncounts characters that keep going up or keep going down, such as1357orzyx, not only runs of neighboring code points like1234orabcd. 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 catches12345678and leaves random passwords alone. - Repetition:
bitsdrops when a few characters do most of the work, as inaaaaaaaaaaaabcdef.
#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:
| Situation | What happens |
|---|---|
data is null, undefined or empty | Returns every count as 0 and monotonicDirection: "none". |
data is a number, an object, an array, an ArrayBuffer or a typed array other than Uint8Array | Throws INVALID_TYPE. Check the type of a form field before you call. |
| A predictable secret, such as a dictionary phrase | Scores high. This function can't tell. |
#Fields you will read
entropy(data) hands back one object with these fields:
| Field | Notes |
|---|---|
bits | Shannon entropy of the whole input in bits, order ignored. |
bitsPerSymbol | bits divided by symbolCount. At most 8 for bytes. |
symbolCount | Characters, or bytes, in the input. |
uniqueSymbols | Distinct characters or byte values. |
longestRun | The longest stretch that keeps rising or keeps falling. |
monotonicDirection | "ascending", "descending", or "none" when neither direction has the longer run. |
bigramBits, bigramBitsPerSymbol | The 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.