Unreleased changes
These pages document main, which has changes 3.0.2 does not. npm install @evanion/luhn gives you 3.0.2, documented here. Values on these pages are produced by running the current source on every build.
Standards and modulo bias
@evanion/luhn provides a faithful implementation of the Luhn algorithm. Depending on the dictionary you choose, it either matches a global industry standard or implements a general-purpose “mod-N” checksum.
Luhn mod-10: The Industry Standard
When you use the dictionary '0123456789', @evanion/luhn implements Luhn mod-10. This is the standardized check digit used worldwide for credit card numbers, payment cards, and various government identifiers.
createLuhn({ dictionary: '0123456789' }) reproduces the published mod-10 test vectors:
const = ({ : '0123456789' });
.('7992739871').; // -> '3'
.('4539578763621486').; // -> true
.('79927398710').; // -> false
.('20260042').; // -> '5'This implementation is compliant with:
- ISO/IEC 7812-1 Annex B: The specification for issuer identification numbers on payment cards.
- 3GPP TS 23.003 Annex B.2: The standard for mobile network identifiers.
- CMS National Provider Identifier (NPI): The check-digit specification for US healthcare providers.
Luhn mod-N: General Purpose Checksums
When you use any other dictionary, @evanion/luhn implements Luhn mod-N.
It is important to note that there is no formal RFC or ISO standard for mod-N Luhn. The la-generalization used by this library (and many others) traces back to common community implementations and Wikipedia.
If you are integrating with another system that claims to use “Luhn mod-N,” do not assume their implementation matches. Always verify their output against your own test vectors, as the dictionary order and the doubling formula may differ.
The “Blind Spot” of the Luhn Algorithm
As discussed in the Dictionaries guide, the Luhn algorithm has one specific weakness: it cannot detect a swap of the dictionary’s first and last characters.
For any even-sized dictionary, swapping the characters at index 0 and index n - 1 (e.g., 0 and z in the default dictionary) leaves the Luhn sum unchanged. This is a mathematical property of the algorithm’s doubling step and cannot be “fixed” without changing the algorithm itself.
Modulo Bias in Random Generation
If you are generating random codes based on a Luhn dictionary, you must be aware of modulo bias.
If you draw a random byte (0-255) and use byte % n to pick a character, every character is equally likely only if n divides 256.
The default 36-character dictionary is not uniform: . This means the first four characters in the dictionary will appear roughly 14.3% more often than the others.
uniformOverBytes allows you to check if a dictionary is safe for simple modulo drawing:
.; // -> false
.; // -> trueHow to avoid bias:
- Use a dictionary that divides 256: This is why
@evanion/tokenconstrains its alphabet to 32 characters. - Rejection Sampling: Draw a byte, discard it if it falls in the “biased tail” (e.g., values 252-255 for ), and draw again.
@evanion/luhn provides the diagnostic tool (uniformOverBytes), but the responsibility for generating high-quality random bytes lies with your random number generator (e.g., crypto.getRandomValues).