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:

  • longestRun and monotonicDirection catch sorted and reverse-sorted stretches directly, at any input length. Here the whole string is one ascending run.
  • bigramBits and bigramBitsPerSymbol measure 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: 3

Same 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 0

Nullish and empty inputs return zeros rather than throwing.

#Bytes

entropy(crypto.getRandomValues(new Uint8Array(4096))).bitsPerSymbol;
// ≈ 7.96

The 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 approaches log2(symbolCount - 1); structure shows up as a value noticeably below that.
  • Very long inputs (symbolCount - 1 far above ) are alphabet-limited. Random input approaches 2 * bitsPerSymbol; structure such as "abcabc…" collapses toward log2(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.