entropy()
Shannon entropy over symbols, plus two structural signals that catch the inputs Shannon entropy is blind to.
import { entropy } from "unsecure/entropy";
entropy("Zk(p4@L!v9{g~8sB");#Signature
function entropy(data: string | Uint8Array | null | undefined): EntropyResult;
interface EntropyResult {
// Unigram — standard Shannon, blind to order
bits: number;
bitsPerSymbol: number;
symbolCount: number;
uniqueSymbols: number;
maxBitsPerSymbol: number;
// Bigram — picks up local structure
bigramBits: number;
bigramBitsPerSymbol: number;
// Monotonic runs — catches sorted and reverse-sorted input
longestRun: number;
monotonicDirection: "ascending" | "descending" | "none";
}Strings are analysed per Unicode code point, so emoji and CJK count as one symbol each. A Uint8Array is analysed per byte value.
#Why three metrics
Shannon entropy counts how often each symbol appears and ignores the order they appear in. That makes it easy to fool:
entropy("abcdefghijklmnop");
// {
// bits: 64, bitsPerSymbol: 4, symbolCount: 16, uniqueSymbols: 16,
// maxBitsPerSymbol: 4,
// bigramBits: 58.603…, bigramBitsPerSymbol: 3.662…,
// longestRun: 16, monotonicDirection: "ascending"
// }Four bits per symbol is the maximum for sixteen distinct characters. By the unigram numbers alone, the alphabet in order looks as strong as a random string over the same characters. The other two fields are what give it away:
longestRunandmonotonicDirectioncatch sorted and reverse-sorted stretches directly, at any input length. Here the whole string is one ascending run.bigramBitsandbigramBitsPerSymbolmeasure the distribution of adjacent pairs, which catches sequential, alternating (ababab) and blocked (aaabbbccc) structure. More powerful than the run detector on long inputs, less useful on short ones.
For comparison, a generated token of the same length:
entropy("Zk(p4@L!v9{g~8sB");
// bits: 64, bitsPerSymbol: 4, longestRun: 3Same unigram numbers, a much shorter longest run.
#Degenerate and empty input
entropy("aaaaaaa");
// bits: 0, bitsPerSymbol: 0, uniqueSymbols: 1, longestRun: 1, monotonicDirection: "none"
entropy(""); // every field 0
entropy(null); // every field 0Nullish and empty inputs return zeros rather than throwing.
#Bytes
entropy(crypto.getRandomValues(new Uint8Array(4096))).bitsPerSymbol;
// ≈ 7.96The ceiling is 8 bits per byte, and how close a random draw gets depends on the sample size, not the source. 256 random bytes score around 7.2, because with 256 draws over 256 values many values never come up at all. Compare against a known-random control of the same length rather than a fixed number.
#Reading bigramBitsPerSymbol
The absolute value is length-sensitive. Per-bigram entropy is capped by log2(min(symbolCount - 1, k²)), where k is uniqueSymbols:
- Short and moderate inputs (
symbolCount - 1 < k²) are sample-limited. Random input approacheslog2(symbolCount - 1); structure shows up as a value noticeably below that. - Very long inputs (
symbolCount - 1far abovek²) are alphabet-limited. Random input approaches2 * bitsPerSymbol; structure such as"abcabc…"collapses towardlog2(k).
Do not compare it against a fixed threshold in isolation. For short inputs, lean on longestRun instead.
#Recipe: a password quality gate
import { entropy } from "unsecure/entropy";
function passesQualityGate(secret: string, minBits = 60): boolean {
const r = entropy(secret);
if (r.bits < minBits) return false;
if (r.longestRun >= 5) return false; // "12345", "abcde"
return true;
}#Recipe: gate an incoming secret
import { entropy } from "unsecure/entropy";
function validateSecretStrength(secret: string): boolean {
const { bitsPerSymbol, uniqueSymbols, longestRun } = entropy(secret);
return bitsPerSymbol >= 3 && uniqueSymbols >= 10 && longestRun < 4;
}#Pitfall: reading this as cryptographic strength
Shannon entropy measures the information density of the data in front of it, not the security of whatever produced it. "Qwerty123!@#" scores well on the unigram fields and is in every cracking dictionary. A run of 16 characters copied out of a random token scores exactly as well as the token.
Use entropy() as a heuristic that rejects obvious junk. Do not use it to accept anything, and never use it in place of secureGenerate when you are the one producing the value.