Skip to content

Commit 315addd

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

7 files changed

+467
-0
lines changed
 

‎README.md

+93
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/crypto_random_string@1.0.0/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

+21
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

+173
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

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { randomBytes } from "https://deno.land/std@0.83.0/node/crypto.ts";
2+
export { promisify } from "https://deno.land/std@0.83.0/node/util.ts";

‎mod.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./cryptoRandomString.ts";

‎mod_test.ts

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { assertEquals, assertMatch, assertThrows } from "./test_deps.ts";
2+
import { cryptoRandomString, cryptoRandomStringAsync } from "./mod.ts";
3+
4+
// Probabilistic, result is always less than or equal to actual set size, chance it is less is below 1e-256 for sizes up to 32656
5+
const generatedCharacterSetSize = (
6+
options: { type?: string; characters?: string },
7+
targetSize: number,
8+
) => {
9+
const set = new Set();
10+
const length = targetSize * 640;
11+
const string = cryptoRandomString({ ...options, length });
12+
13+
for (let i = 0; i < length; i++) {
14+
set.add(string[i]);
15+
}
16+
17+
return set.size;
18+
};
19+
20+
Deno.test("main", () => {
21+
assertEquals(cryptoRandomString({ length: 0 }).length, 0);
22+
assertEquals(cryptoRandomString({ length: 10 }).length, 10);
23+
assertEquals(cryptoRandomString({ length: 100 }).length, 100);
24+
assertMatch(cryptoRandomString({ length: 100 }), /^[a-f\d]*$/); // Sanity check, probabilistic
25+
assertEquals(generatedCharacterSetSize({}, 16), 16);
26+
});
27+
28+
Deno.test("async", async () => {
29+
assertEquals((await cryptoRandomStringAsync({ length: 0 })).length, 0);
30+
assertEquals((await cryptoRandomStringAsync({ length: 10 })).length, 10);
31+
assertEquals((await cryptoRandomStringAsync({ length: 100 })).length, 100);
32+
assertMatch(await cryptoRandomStringAsync({ length: 100 }), /^[a-f\d]*$/);
33+
});
34+
35+
Deno.test("hex", () => {
36+
assertEquals(cryptoRandomString({ length: 0, type: "hex" }).length, 0);
37+
assertEquals(cryptoRandomString({ length: 10, type: "hex" }).length, 10);
38+
assertEquals(cryptoRandomString({ length: 100, type: "hex" }).length, 100);
39+
assertMatch(cryptoRandomString({ length: 100, type: "hex" }), /^[a-f\d]*$/); // Sanity check, probabilistic
40+
assertEquals(generatedCharacterSetSize({ type: "hex" }, 16), 16);
41+
});
42+
43+
Deno.test("base64", () => {
44+
assertEquals(cryptoRandomString({ length: 0, type: "base64" }).length, 0);
45+
assertEquals(cryptoRandomString({ length: 10, type: "base64" }).length, 10);
46+
assertEquals(cryptoRandomString({ length: 100, type: "base64" }).length, 100);
47+
assertMatch(
48+
cryptoRandomString({ length: 100, type: "base64" }),
49+
/^[a-zA-Z\d/+]*$/,
50+
); // Sanity check, probabilistic
51+
assertEquals(generatedCharacterSetSize({ type: "base64" }, 64), 64);
52+
});
53+
54+
Deno.test("url-safe", () => {
55+
assertEquals(cryptoRandomString({ length: 0, type: "url-safe" }).length, 0);
56+
assertEquals(cryptoRandomString({ length: 10, type: "url-safe" }).length, 10);
57+
assertEquals(
58+
cryptoRandomString({ length: 100, type: "url-safe" }).length,
59+
100,
60+
);
61+
assertMatch(
62+
cryptoRandomString({ length: 100, type: "url-safe" }),
63+
/^[a-zA-Z\d._~-]*$/,
64+
); // Sanity check, probabilistic
65+
assertEquals(generatedCharacterSetSize({ type: "url-safe" }, 66), 66);
66+
});
67+
68+
Deno.test("numeric", () => {
69+
assertEquals(cryptoRandomString({ length: 0, type: "numeric" }).length, 0);
70+
assertEquals(cryptoRandomString({ length: 10, type: "numeric" }).length, 10);
71+
assertEquals(
72+
cryptoRandomString({ length: 100, type: "numeric" }).length,
73+
100,
74+
);
75+
assertMatch(cryptoRandomString({ length: 100, type: "numeric" }), /^[\d]*$/); // Sanity check, probabilistic
76+
assertEquals(generatedCharacterSetSize({ type: "numeric" }, 10), 10);
77+
});
78+
79+
Deno.test("distinguishable", () => {
80+
assertEquals(
81+
cryptoRandomString({ length: 0, type: "distinguishable" }).length,
82+
0,
83+
);
84+
assertEquals(
85+
cryptoRandomString({ length: 10, type: "distinguishable" }).length,
86+
10,
87+
);
88+
assertEquals(
89+
cryptoRandomString({ length: 100, type: "distinguishable" }).length,
90+
100,
91+
);
92+
assertMatch(
93+
cryptoRandomString({ length: 100, type: "distinguishable" }),
94+
/^[CDEHKMPRTUWXY012458]*$/,
95+
); // Sanity check, probabilistic
96+
assertEquals(generatedCharacterSetSize({ type: "distinguishable" }, 19), 19);
97+
});
98+
99+
Deno.test("ascii-printable", () => {
100+
assertEquals(
101+
cryptoRandomString({ length: 0, type: "ascii-printable" }).length,
102+
0,
103+
);
104+
assertEquals(
105+
cryptoRandomString({ length: 10, type: "ascii-printable" }).length,
106+
10,
107+
);
108+
assertEquals(
109+
cryptoRandomString({ length: 100, type: "ascii-printable" }).length,
110+
100,
111+
);
112+
assertMatch(
113+
cryptoRandomString({ length: 100, type: "ascii-printable" }),
114+
/^[!"#$%&'()*+,-./\d:;<=>?@A-Z[\\\]^_`a-z{|}~]*$/,
115+
); // Sanity check, probabilistic
116+
});
117+
118+
Deno.test("alphanumeric", () => {
119+
assertEquals(
120+
cryptoRandomString({ length: 0, type: "alphanumeric" }).length,
121+
0,
122+
);
123+
assertEquals(
124+
cryptoRandomString({ length: 10, type: "alphanumeric" }).length,
125+
10,
126+
);
127+
assertEquals(
128+
cryptoRandomString({ length: 100, type: "alphanumeric" }).length,
129+
100,
130+
);
131+
assertMatch(
132+
cryptoRandomString({ length: 100, type: "alphanumeric" }),
133+
/^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789]*$/,
134+
); // Sanity check, probabilistic
135+
assertEquals(generatedCharacterSetSize({ type: "alphanumeric" }, 19), 62);
136+
});
137+
138+
Deno.test("characters", () => {
139+
assertEquals(cryptoRandomString({ length: 0, characters: "1234" }).length, 0);
140+
assertEquals(
141+
cryptoRandomString({ length: 10, characters: "1234" }).length,
142+
10,
143+
);
144+
assertEquals(
145+
cryptoRandomString({ length: 100, characters: "1234" }).length,
146+
100,
147+
);
148+
assertMatch(
149+
cryptoRandomString({ length: 100, characters: "1234" }),
150+
/^[1-4]*$/,
151+
); // Sanity check, probabilistic
152+
assertEquals(generatedCharacterSetSize({ characters: "1234" }, 4), 4);
153+
assertEquals(generatedCharacterSetSize({ characters: "0123456789" }, 10), 10);
154+
});
155+
156+
Deno.test("argument errors", () => {
157+
assertThrows(() => {
158+
cryptoRandomString({ length: Infinity });
159+
});
160+
161+
assertThrows(() => {
162+
cryptoRandomString({ length: -1 });
163+
});
164+
165+
assertThrows(() => {
166+
cryptoRandomString({ length: 0, type: "hex", characters: "1234" });
167+
});
168+
169+
assertThrows(() => {
170+
cryptoRandomString({ length: 0, type: "unknown" });
171+
});
172+
});

‎test_deps.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export {
2+
assertEquals,
3+
assertMatch,
4+
assertThrows,
5+
} from "https://deno.land/std@0.83.0/testing/asserts.ts";

0 commit comments

Comments
 (0)
Please sign in to comment.