ndigittoken
Generate a cryptographically secure pseudorandom token of N digits.
Quick start
gen(n)
where n
is the desired length/number of digits.
import { gen } from 'ndigittoken';
const token: string = gen(6);
// => '076471'
Summary
This tiny module generates an ndigit cryptographically strong pseudorandom token in constant time whilst avoiding modulo bias and with 0 dependencies.
Modulo bias
The ^2.x
version of the ndigittoken
algorithm does avoid modulo bias therefore providing high precision even for larger tokens.
Performance
This algorithm runs in O(1)
constant time for up to a 100
digit long token sizes making it suitable for cryptographic applications (and I'm not sure why you would need longer tokens).
No dependencies
This package has 0 dependencies
🎉
Comparisons
Algorithm  Cryptographically strong?  Avoids modulo bias? 

average RNG  ❌  ❌ 
crypto.randomInt  ❌  ✔️ 
ndigittoken  ✔️  ✔️ 
For more details on how this is achieved, please refer to the the Details section.
Detailed usage
Just give the desired token length to get your random ndigit token.
import { gen } from 'ndigittoken';
const token: string = gen(6);
// => '681485'
const anotherAuthToken: string = gen(6);
// => '090188'
const anEightDigitToken: string = gen(8);
// => '25280789'
JavaScript
Or with plain old JS
:
const { gen } = require('ndigittoken');
const token = gen(6);
// => '029947'
Aliases
gen()
and randomDigits()
are just equivalent aliases of generateSecureToken()
use whichever you prefer:
import { gen, generateSecureToken, randomDigits } from 'ndigittoken';
const alias0: string = generateSecureToken(6);
const alias1: string = gen(6);
const alias2: string = randomDigits(6);
// => '801448'
Advanced options
There are also a few advanced options for customising some parameters of the algorithm and the output, though most users should not need these.
Compatibility
ndigittoken
supports node >= 10.4.0
. There are no additional compatibility requirements.
This package is solely dependent on the builtin nodeJS/crypto
module.
Running in browser
Please note that ndigittoken
is intended to be used serverside and therefore browser support is not actively maintained.
However, as of v2.0.2
you can use ndigittoken
with cryptobrowserify
or other custom byte streams.
Please refer to the customByteStream option for more details.
Details
Background
I was looking for a simple module that generates an ndigit token that could be used for 2FA among others and was surprised that I couldn't find one that uses a cryptographically secure number generator (CSPRNG)
If your application needs cryptographically strong pseudo random values, this uses crypto.randomBytes()
which provides cryptographically strong pseudorandom data.
Algorithmic properties
Performance
The ndigittoken
algorithm executes with O(1)
time complexity, i.e. in constant time when length <= 100
. This makes ndigittoken
suitable for cryptographic use cases.
Normally, you would never need to generate tokens that are above a few digits, such as 6 or 8, so this threshold is already an overkill.
The expected execution time of generating a token where length <= 1000
is still within 1 ms
on a modern CPU.
Entropy
Note that for a cryptographic PRNG the system's entropy is an important factor. The ndigittoken
function will wait
until there is sufficient entropy available as it is uses the crypto.randomBytes()
method.
This should normally not take longer than a few milliseconds unless the system has just booted very recently.
You can read more about this here.
Libuv's threadpool
As ndigittoken
is dependent on crypto.randomBytes()
it uses libuv's threadpool, which can have performance implications for some applications. Please refer to the documentation here for more information.
Memory usage
By default the algorithm ensures modulo precision whilst also balancing performance and memory usage.
In order to achieve O(1)
running time for lengths 1100
the algorithm will attempt to reserve memory linearly scaling with the desired token length.
For token sizes between 132
the maximum used memory will not exceed 128 bytes
.
For insanely large tokens, such as a 1000
digits, the max memory by default is still within 1 kibibyte
.
Options
There are a few supported customisation options for the algorithm for some highly specific use cases.
❗ Most users will NOT need to change any of these options. ❗
optional  default value  

options.returnType  ✔️  'string' 
options.skipPadding  ✔️  false 
options.customMemory  ✔️  undefined 
options.customByteStream  ✔️  undefined 
options.skipPadding
Padding is an important concept regarding this algorithm.
If you aim to change this option, please make sure to read both skipPadding & returnType carefully to avoid unintended consequences.
Generating digits & padding
Generate a singledigit decimal
Since this algorithm aims to generate decimal numbers from a cryptographically strong random byte stream, the distribution of the generated numbers will mostly follow a natural distribution.
This means that if you generate a single digit token, you are mostly equally likely to hit any of the decimal numbers 09
inclusive. Note that, you can therefore get zero as a result (as you should be able to do so).
For example, calling gen(1)
can result in the decimal number 9
and the token '9'
(since the default return type is string):
const token = gen(1);
// internally:
1) length=1 means max=9 (> max=9)
2) roll a number between 09 (> rolls 9)
3) convert it to string (> '9')
4) return
=> '9'
Generate a multidigit decimal
On the other hand, for multidigit tokens, you will be mostly equally likely to hit any of 099
meaning that you can still hit a single digit decimal number.
For example, calling gen(2)
can internally result in the decimal number 9
again, since it is a valid random number on the range 099
. However, since the user wanted to receive a 2digit token, the returned token string will need to be padded by a 0
. Therefore, you will get '09'
as the token.
const token = gen(2);
// internally:
1) length=2 means max=99 (> max=9)
2) roll a number between 099 (> rolls 9)
3) convert it to string (> '9')
4) pad if less than desired length (> '09')
5) return
=> '09'
Equally random
Now you should see why it may be necessary to pad the generated numbers.
Why not just discard numbers that start with 0?
You might be wondering, why can't we just discard numbers that start with zeros rather than to pad them.
Whilst it would be a valid approach to say that we could just discard any numbers that are lower than the desired number of digits, it would defeat the purpose of using a cryptographically strong seed.
In order to provide the closest to a truly random distribution of generated numbers, it is essential that the minimum possible value is 0
as the CSPRNG functions provide a pseudo random stream of binary data.
How much discarded
Furthermore, just think about in how many cases you would need to reroll for larger tokens.
For example for gen(6)
in order to have a 6digit
number any numbers below 100000
would have to be discarded. That's 10000
or 10 ** (length1)
cases (099999
).
const token = gen(6);
=> '009542' // 10% chance to discard
Besides, there are already many average random number generators where you can specify an integer range for both min and max that focuses less on precision.
Onetime tokens often start with zeros
As you may have noticed if you use 2FA, many one time tokens do start with zeros. If they use a bitstream it has a ~10%
chance and this should also explain why ndigittoken
can return a token starting with zero.
Using skipPadding
Setting options.skipPadding=true
will skip padding any tokens that are shorter than the input length.
Therefore, ndigittoken
may return varied token lengths!
Make sure your application is able to handle that the returned token may be of different lengths.
Example
If skipPadding=true
then length
will be the maximum returned token length.
const { gen, generateSecureToken } = require('ndigittoken');
const token = gen(6, { skipPadding: false }); // equivalent to gen(6)
=> '030771'
const token = gen(6, { skipPadding: true });
=> '30771'
options.returnType
By default the algorithm returns the generated token as a string
.
This option allows you to customise the return type of the generated token.
You can choose from:
'string'

'number'
(i.e.'integer'
) 'bigint'
string
guarantees a fixed length output!
If you aim to change this option, please make sure to read both skipPadding & returnType carefully to avoid unintended consequences.
Return type compatibility
Please refer to the below table to see the compatibility of the return types:
return type / token length  115  16+ 

'string' 
✔️  ✔️ 
'number' (integer)

✔️  ❌ 
'bigint' 
✔️  ✔️ 
Examples
const { gen, generateSecureToken } = require('ndigittoken');
const token = gen(6);
=> '440835'
const anotherStringToken = gen(16, { returnType: 'string' });
=> '8384458882874956'
const aNumberToken = gen(6, { returnType: 'number' });
=> 225806
const aBigIntToken = gen(16, { returnType: 'bigint' });
=> 9680644450112709n
Using returnType with skipPadding
Some return types will automatically skip padding.
For example, if the token is returned as a number
there is no way to pad with zeros if shorter.
In other words, some return types require and automatically set skipPadding=true
.
Compatibility table
return type / padding  skipPadding  padWithZeros 

'string' 
optional  default 
'number' 
required  impossible 
'bigint' 
required  impossible 
Examples
const { gen, generateSecureToken } = require('ndigittoken');
// the below is equivalent to gen(6) i.e. default
const token = gen(6, { returnType: 'string', skipPadding: false });
=> '012345'
const token = gen(6, { returnType: 'string', skipPadding: true });
=> '12345'
// the below is equivalent to gen(6, { returnType: 'number' });
const token = gen(6, { returnType: 'number', skipPadding: true });
=> 12345
// the below is equivalent to gen(6, { returnType: 'bigint' });
const token = gen(6, { returnType: 'bigint', skipPadding: true });
=> 12345n
options.customMemory
This is a highly advanced option. Please read memory usage before proceeding.
If you need to limit the used memory, you can do so by specifying the amount of bytes you can allocate via the options.customMemory
option.
For example, if you can only allocate 8 bytes
, you could do the following:
const { gen, generateSecureToken } = require('ndigittoken');
const token = gen(6, { customMemory: 8 });
Please note that both giving too few or too much memory to the algorithm may negatively impact performance by a considerable amount.
If the application detects unsuitable amount of memory, it may warn you in the debug console, but will continue to execute.
options.customByteStream
This is an advanced option. You should only use this if you don't have access to node crypto
.
With this option you can specify a custom synchronous CSPRNG byte stream function that returns a Buffer
that ndigittoken
will use.
You may find use of this option if you need to run ndigittoken
in the browser with e.g. cryptobrowserify
:
const { randomBytes } = require('cryptobrowserify');
const { gen, generateSecureToken } = require('ndigittoken');
const token = gen(6, { customByteStream: randomBytes });
Please note that this is option has only been tested with cryptobrowserify
and inappropriate use may lead to various unintended consequences.
Test
Install the devDependencies
and run npm test
for the module tests.
Scripts

npm test
to see interactive tests and coverage 
npm run build
to compile JavaScript 
npm run lint
to run linting
Support ndigittoken
Financial support
If you like this project, please consider supporting ndigittoken
with a onetime or recurring donation as this project takes considerable amount of time and effort to develop and maintain.
Star this project
If you can't support ndigittoken
financially, but you've found it useful, please consider giving the project a GitHub:star: to help its discoverability. Thank you!
Contributing
Code contributions are also warmly welcomed!