Skip to content

Commit 315addd

Browse files
author
Piyush Bhatt
committed
Create Deno module for crypto-random-string
1 parent 6c2d405 commit 315addd

File tree

7 files changed

+467
-0
lines changed

7 files changed

+467
-0
lines changed

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# crypto-random-string
2+
3+
> Generate a [cryptographically strong](https://en.wikipedia.org/wiki/Strong_cryptography) random string
4+
5+
Deno module based on [crypto-random-string](https://github.com/sindresorhus/crypto-random-string). Useful for creating an identifier, slug, salt, PIN code, fixture, etc.
6+
7+
## Import Module
8+
9+
```typescript
10+
import { cryptoRandomString, cryptoRandomStringAsync } from "https://deno.land/x/[email protected]/mod.ts"
11+
// or
12+
import { cryptoRandomString, cryptoRandomStringAsync } from "https://github.com/piyush-bhatt/crypto-random-string/raw/main/mod.ts"
13+
```
14+
15+
## Usage
16+
17+
```typescript
18+
19+
cryptoRandomString({length: 10});
20+
//=> '2cf05d94db'
21+
22+
cryptoRandomString({length: 10, type: 'base64'});
23+
//=> 'YMiMbaQl6I'
24+
25+
cryptoRandomString({length: 10, type: 'url-safe'});
26+
//=> 'YN-tqc8pOw'
27+
28+
cryptoRandomString({length: 10, type: 'numeric'});
29+
//=> '8314659141'
30+
31+
cryptoRandomString({length: 6, type: 'distinguishable'});
32+
//=> 'CDEHKM'
33+
34+
cryptoRandomString({length: 10, type: 'ascii-printable'});
35+
//=> '`#Rt8$IK>B'
36+
37+
cryptoRandomString({length: 10, type: 'alphanumeric'});
38+
//=> 'DMuKL8YtE7'
39+
40+
cryptoRandomString({length: 10, characters: 'abc'});
41+
//=> 'abaaccabac'
42+
```
43+
44+
## API
45+
46+
### cryptoRandomString(options)
47+
48+
Returns a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.
49+
50+
### cryptoRandomStringAsync(options)
51+
52+
Returns a promise which resolves to a randomized string. [Hex](https://en.wikipedia.org/wiki/Hexadecimal) by default.
53+
54+
#### options
55+
56+
Type: `object`
57+
58+
##### length
59+
60+
*Required*\
61+
Type: `number`
62+
63+
Length of the returned string.
64+
65+
##### type
66+
67+
Type: `string`\
68+
Default: `'hex'`\
69+
Values: `'hex' | 'base64' | 'url-safe' | 'numeric' | 'distinguishable' | 'ascii-printable' | 'alphanumeric'`
70+
71+
Use only characters from a predefined set of allowed characters.
72+
73+
Cannot be set at the same time as the `characters` option.
74+
75+
The `distinguishable` set contains only uppercase characters that are not easily confused: `CDEHKMPRTUWXY012458`. It can be useful if you need to print out a short string that you'd like users to read and type back in with minimal errors. For example, reading a code off of a screen that needs to be typed into a phone to connect two devices.
76+
77+
The `ascii-printable` set contains all [printable ASCII characters](https://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters): ``!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~`` Useful for generating passwords where all possible ASCII characters should be used.
78+
79+
The `alphanumeric` set contains uppercase letters, lowercase letters, and digits: `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`. Useful for generating [nonce](https://developer.mozilla.org/en-US/docs/Web/API/HTMLOrForeignElement/nonce) values.
80+
81+
##### characters
82+
83+
Type: `string`\
84+
Minimum length: `1`\
85+
Maximum length: `65536`
86+
87+
Use only characters from a custom set of allowed characters.
88+
89+
Cannot be set at the same time as the `type` option.
90+
91+
## Licensing
92+
93+
[MIT](https://github.com/piyush-bhatt/crypto-random-string/blob/main/LICENSE) licensed

constants.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const URL_SAFE_CHARACTERS =
2+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~".split(
3+
"",
4+
);
5+
export const NUMERIC_CHARACTERS = "0123456789".split("");
6+
export const DISTINGUISHABLE_CHARACTERS = "CDEHKMPRTUWXY012458".split("");
7+
export const ASCII_PRINTABLE_CHARACTERS =
8+
"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
9+
.split("");
10+
export const ALPHANUMERIC_CHARACTERS =
11+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
12+
export const ALLOWED_TYPES = [
13+
undefined,
14+
"hex",
15+
"base64",
16+
"url-safe",
17+
"numeric",
18+
"distinguishable",
19+
"ascii-printable",
20+
"alphanumeric",
21+
];

cryptoRandomString.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { randomBytes } from "./deps.ts";
2+
import { promisify } from "./deps.ts";
3+
import {
4+
ALLOWED_TYPES,
5+
ALPHANUMERIC_CHARACTERS,
6+
ASCII_PRINTABLE_CHARACTERS,
7+
DISTINGUISHABLE_CHARACTERS,
8+
NUMERIC_CHARACTERS,
9+
URL_SAFE_CHARACTERS,
10+
} from "./constants.ts";
11+
12+
const randomBytesAsync = promisify(randomBytes);
13+
14+
const generateForCustomCharacters = (length: number, characters: string[]) => {
15+
// Generating entropy is faster than complex math operations, so we use the simplest way
16+
const characterCount = characters.length;
17+
const maxValidSelector =
18+
(Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
19+
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
20+
let string = "";
21+
let stringLength = 0;
22+
23+
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
24+
const entropy = randomBytes(entropyLength);
25+
let entropyPosition = 0;
26+
27+
while (entropyPosition < entropyLength && stringLength < length) {
28+
const entropyValue = entropy.readUInt16LE(entropyPosition);
29+
entropyPosition += 2;
30+
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
31+
continue;
32+
}
33+
34+
string += characters[entropyValue % characterCount];
35+
stringLength++;
36+
}
37+
}
38+
39+
return string;
40+
};
41+
42+
const generateForCustomCharactersAsync = async (
43+
length: number,
44+
characters: string[],
45+
) => {
46+
// Generating entropy is faster than complex math operations, so we use the simplest way
47+
const characterCount = characters.length;
48+
const maxValidSelector =
49+
(Math.floor(0x10000 / characterCount) * characterCount) - 1; // Using values above this will ruin distribution when using modular division
50+
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
51+
let string = "";
52+
let stringLength = 0;
53+
54+
while (stringLength < length) { // In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
55+
const entropy = await randomBytesAsync(entropyLength); // eslint-disable-line no-await-in-loop
56+
let entropyPosition = 0;
57+
58+
while (entropyPosition < entropyLength && stringLength < length) {
59+
const entropyValue = entropy.readUInt16LE(entropyPosition);
60+
entropyPosition += 2;
61+
if (entropyValue > maxValidSelector) { // Skip values which will ruin distribution when using modular division
62+
continue;
63+
}
64+
65+
string += characters[entropyValue % characterCount];
66+
stringLength++;
67+
}
68+
}
69+
70+
return string;
71+
};
72+
73+
const generateRandomBytes = (
74+
byteLength: number,
75+
type: string,
76+
length: number,
77+
) => randomBytes(byteLength).toString(type).slice(0, length);
78+
79+
const generateRandomBytesAsync = async (
80+
byteLength: number,
81+
type: string,
82+
length: number,
83+
) => {
84+
const buffer = await randomBytesAsync(byteLength);
85+
return buffer.toString(type).slice(0, length);
86+
};
87+
88+
const createGenerator = (
89+
generateForCustomCharacters: Function,
90+
generateRandomBytes: Function,
91+
) =>
92+
(
93+
{ length, type, characters }: {
94+
length: number;
95+
type?: string;
96+
characters?: string;
97+
},
98+
) => {
99+
if (!(length >= 0 && Number.isFinite(length))) {
100+
throw new TypeError(
101+
"Expected a `length` to be a non-negative finite number",
102+
);
103+
}
104+
105+
if (type !== undefined && characters !== undefined) {
106+
throw new TypeError("Expected either `type` or `characters`");
107+
}
108+
109+
if (characters !== undefined && typeof characters !== "string") {
110+
throw new TypeError("Expected `characters` to be string");
111+
}
112+
113+
if (!ALLOWED_TYPES.includes(type)) {
114+
throw new TypeError(`Unknown type: ${type}`);
115+
}
116+
117+
if (type === undefined && characters === undefined) {
118+
type = "hex";
119+
}
120+
121+
if (type === "hex") {
122+
return generateRandomBytes(Math.ceil(length * 0.5), "hex", length); // Need 0.5 byte entropy per character
123+
}
124+
125+
if (type === "base64") {
126+
return generateRandomBytes(Math.ceil(length * 0.75), "base64", length); // Need 0.75 byte of entropy per character
127+
}
128+
129+
if (type === "url-safe") {
130+
return generateForCustomCharacters(length, URL_SAFE_CHARACTERS);
131+
}
132+
133+
if (type === "numeric") {
134+
return generateForCustomCharacters(length, NUMERIC_CHARACTERS);
135+
}
136+
137+
if (type === "distinguishable") {
138+
return generateForCustomCharacters(length, DISTINGUISHABLE_CHARACTERS);
139+
}
140+
141+
if (type === "ascii-printable") {
142+
return generateForCustomCharacters(length, ASCII_PRINTABLE_CHARACTERS);
143+
}
144+
145+
if (type === "alphanumeric") {
146+
return generateForCustomCharacters(length, ALPHANUMERIC_CHARACTERS);
147+
}
148+
149+
if (characters !== undefined && characters.length === 0) {
150+
throw new TypeError(
151+
"Expected `characters` string length to be greater than or equal to 1",
152+
);
153+
}
154+
155+
if (characters !== undefined && characters.length > 0x10000) {
156+
throw new TypeError(
157+
"Expected `characters` string length to be less or equal to 65536",
158+
);
159+
}
160+
161+
return generateForCustomCharacters(length, characters!.split(""));
162+
};
163+
164+
const cryptoRandomString = createGenerator(
165+
generateForCustomCharacters,
166+
generateRandomBytes,
167+
);
168+
const cryptoRandomStringAsync = createGenerator(
169+
generateForCustomCharactersAsync,
170+
generateRandomBytesAsync,
171+
);
172+
173+
export { cryptoRandomString, cryptoRandomStringAsync };

deps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { randomBytes } from "https://deno.land/[email protected]/node/crypto.ts";
2+
export { promisify } from "https://deno.land/[email protected]/node/util.ts";

mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./cryptoRandomString.ts";

0 commit comments

Comments
 (0)