Skip to content

Commit

Permalink
Add type and characters options (#4)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Yanis Benson and sindresorhus committed May 8, 2019
1 parent d605421 commit 51402ba
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 7 deletions.
52 changes: 49 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
import {MergeExclusive} from 'type-fest';

declare namespace cryptoRandomString {
interface TypeOptions {
/**
Use only characters from a predefined set of allowed characters.
Cannot be set at the same time as the `characters` option.
@default 'hex'
@example
```
cryptoRandomString(10, {type:'hex'});
//=> '87fc70e2b9'
cryptoRandomString(10, {type:'base64'});
//=> 'mhsX7xmIv/'
cryptoRandomString(10, {type:'url-safe'});
//=> 'VEjfNW3Yej'
```
*/
type?: 'hex' | 'base64' | 'url-safe';
}

interface CharactersOptions {
/**
Use only characters from a custom set of allowed characters.
Cannot be set at the same time as the `type` option.
Minimum length: `1`
Maximum length: `65536`
@example
```
cryptoRandomString(10, {characters:'0123456789'});
//=> '8796225811'
```
*/
characters: string;
}
type Options = MergeExclusive<TypeOptions, CharactersOptions>;
}

/**
Generate a [cryptographically strong](https://en.m.wikipedia.org/wiki/Strong_cryptography) random string.
Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string.
@param length - Length of the returned string.
@returns A [`hex`](https://en.wikipedia.org/wiki/Hexadecimal) string.
@returns Returns a randomized string.
@example
```
Expand All @@ -12,6 +58,6 @@ cryptoRandomString(10);
//=> '2cf05d94db'
```
*/
declare function cryptoRandomString(length: number): string;
declare function cryptoRandomString(length: number, options?: cryptoRandomString.Options): string;

export = cryptoRandomString;
74 changes: 72 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,80 @@
'use strict';
const crypto = require('crypto');

module.exports = length => {
const urlSafeChars = 'abcdefjhijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'.split('');

const generateForCustomCharacters = (length, chars) => {
// Generating entropy is faster than complex math operations, so we use the simplest way
const characterCount = chars.length;
const maxValidSelector = (Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
const entropyLength = 2 * Math.ceil(1.1 * length); // Generating a bit more than required so chances we need more than one pass will be really low
let string = '';
let stringLength = 0;

while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
const entropy = crypto.randomBytes(entropyLength);
let entropyPosition = 0;

while (entropyPosition < entropyLength && stringLength < length) {
const entropyValue = entropy.readUInt16LE(entropyPosition);
entropyPosition += 2;
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
continue;
}

string += chars[entropyValue % characterCount];
stringLength++;
}
}

return string;
};

const allowedTypes = [undefined, 'hex', 'base64', 'url-safe'];

module.exports = (length, opts) => {
if (!Number.isFinite(length)) {
throw new TypeError('Expected a finite number');
}

return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
let type = opts === undefined ? undefined : opts.type;
const characters = opts === undefined ? undefined : opts.characters;

if (type !== undefined && characters !== undefined) {
throw new TypeError('Expected either type or characters');
}

if (characters !== undefined && typeof characters !== 'string') {
throw new TypeError('Expected characters to be string');
}

if (!allowedTypes.includes(type)) {
throw new TypeError(`Unknown type: ${type}`);
}

if (type === undefined && characters === undefined) {
type = 'hex';
}

if (type === 'hex' || (type === undefined && characters === undefined)) {
return crypto.randomBytes(Math.ceil(length * 0.5)).toString('hex').slice(0, length); // Need 0.5 byte entropy per character
}

if (type === 'base64') {
return crypto.randomBytes(Math.ceil(length * 0.75)).toString('base64').slice(0, length); // Need 0.75 byte of entropy per character
}

if (type === 'url-safe') {
return generateForCustomCharacters(length, urlSafeChars);
}

if (characters.length === 0) {
throw new TypeError('Expected `characters` string length to be greater than or equal to 1');
}

if (characters.length > 0x10000) {
throw new TypeError('Expected `characters` string length to be less or equal to 65536');
}

return generateForCustomCharacters(length, characters.split(''));
};
2 changes: 2 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ import {expectType} from 'tsd';
import cryptoRandomString = require('.');

expectType<string>(cryptoRandomString(10));
expectType<string>(cryptoRandomString(10, {type: 'url-safe'}));
expectType<string>(cryptoRandomString(10, {characters: '1234'}));
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"secure",
"hex"
],
"dependencies": {
"type-fest": "^0.4.1"
},
"devDependencies": {
"ava": "^1.4.1",
"tsd": "^0.7.2",
Expand Down
40 changes: 38 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,57 @@ const cryptoRandomString = require('crypto-random-string');

cryptoRandomString(10);
//=> '2cf05d94db'

cryptoRandomString(10, {type: 'hex'});
//=> 'c00f094c79'

cryptoRandomString(10, {type: 'base64'});
//=> 'YMiMbaQl6I'

cryptoRandomString(10, {type: 'url-safe'});
//=> 'YN-tqc8pOw'

cryptoRandomString(10, {characters: '1234567890'});
//=> '1791935639'
```


## API

### cryptoRandomString(length)
### cryptoRandomString(length, [options])

Returns a [`hex`](https://en.wikipedia.org/wiki/Hexadecimal) string.
Returns a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.

#### length

Type: `number`

Length of the returned string.

#### options

Type: `object`

##### type

Type: `string`<br>
Default: `hex`<br>
Values: `hex` `base64` `url-safe`

Use only characters from a predefined set of allowed characters.

Cannot be set at the same time as the `characters` option.

##### characters

Type: `string`<br>
Minimum length: `1`<br>
Maximum length: `65536`

Use only characters from a custom set of allowed characters.

Cannot be set at the same time as the `type` option.


## Related

Expand Down
47 changes: 47 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,51 @@ test('main', t => {
t.is(cryptoRandomString(0).length, 0);
t.is(cryptoRandomString(10).length, 10);
t.is(cryptoRandomString(100).length, 100);
t.regex(cryptoRandomString(100), /^[a-f\d]*$/); // Sanity check, probabilistic
});

test('hex', t => {
t.is(cryptoRandomString(0, {type: 'hex'}).length, 0);
t.is(cryptoRandomString(10, {type: 'hex'}).length, 10);
t.is(cryptoRandomString(100, {type: 'hex'}).length, 100);
t.regex(cryptoRandomString(100, {type: 'hex'}), /^[a-f\d]*$/); // Sanity check, probabilistic
});

test('base64', t => {
t.is(cryptoRandomString(0, {type: 'base64'}).length, 0);
t.is(cryptoRandomString(10, {type: 'base64'}).length, 10);
t.is(cryptoRandomString(100, {type: 'base64'}).length, 100);
t.regex(cryptoRandomString(100, {type: 'base64'}), /^[a-zA-Z\d/+]*$/); // Sanity check, probabilistic
});

test('url-safe', t => {
t.is(cryptoRandomString(0, {type: 'url-safe'}).length, 0);
t.is(cryptoRandomString(10, {type: 'url-safe'}).length, 10);
t.is(cryptoRandomString(100, {type: 'url-safe'}).length, 100);
t.regex(cryptoRandomString(100, {type: 'url-safe'}), /^[a-zA-Z\d._~-]*$/); // Sanity check, probabilistic
});

test('characters', t => {
t.is(cryptoRandomString(0, {characters: '1234'}).length, 0);
t.is(cryptoRandomString(10, {characters: '1234'}).length, 10);
t.is(cryptoRandomString(100, {characters: '1234'}).length, 100);
t.regex(cryptoRandomString(100, {characters: '1234'}), /^[1-4]*$/); // Sanity check, probabilistic
});

test('argument errors', t => {
t.throws(() => {
cryptoRandomString(Infinity);
});

t.throws(() => {
cryptoRandomString(0, {type: 'hex', characters: '1234'});
});

t.throws(() => {
cryptoRandomString(0, {characters: 42});
});

t.throws(() => {
cryptoRandomString(0, {type: 'unknown'});
});
});

0 comments on commit 51402ba

Please sign in to comment.