cybertoken 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +1 -0
- package/.github/dependabot.yaml +11 -0
- package/.github/workflows/CI.yaml +66 -0
- package/.prettierrc.json +3 -0
- package/README.md +94 -0
- package/a.mjs +8 -0
- package/built/cli.d.ts +1 -0
- package/built/cli.js +20 -0
- package/built/constants.d.ts +4 -0
- package/built/constants.js +4 -0
- package/built/crc32.d.ts +1 -0
- package/built/crc32.js +56 -0
- package/built/index.d.ts +8 -0
- package/built/index.js +47 -0
- package/built/index.test.d.ts +1 -0
- package/built/index.test.js +64 -0
- package/built/parse.d.ts +10 -0
- package/built/parse.js +52 -0
- package/built/parse.test.d.ts +1 -0
- package/built/parse.test.js +35 -0
- package/package.json +35 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: nikeee
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- master
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [16.x, 18.x, 19.x]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v3
|
|
19
|
+
- uses: actions/setup-node@v3
|
|
20
|
+
with:
|
|
21
|
+
node-version: ${{ matrix.node-version }}
|
|
22
|
+
cache: npm
|
|
23
|
+
|
|
24
|
+
- run: npm ci
|
|
25
|
+
- run: npm run test:coverage
|
|
26
|
+
|
|
27
|
+
docs-build:
|
|
28
|
+
name: Build Docs
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
needs:
|
|
31
|
+
- test
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v3
|
|
35
|
+
- uses: actions/setup-node@v3
|
|
36
|
+
with:
|
|
37
|
+
node-version: 19.x
|
|
38
|
+
cache: npm
|
|
39
|
+
|
|
40
|
+
- run: npm ci
|
|
41
|
+
- run: npm run docs
|
|
42
|
+
|
|
43
|
+
- uses: actions/upload-pages-artifact@v1
|
|
44
|
+
with:
|
|
45
|
+
path: ./docs
|
|
46
|
+
|
|
47
|
+
docs-deploy:
|
|
48
|
+
name: Deploy Docs
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
if: ${{ github.ref == 'refs/heads/master' }}
|
|
51
|
+
|
|
52
|
+
permissions:
|
|
53
|
+
pages: write
|
|
54
|
+
id-token: write
|
|
55
|
+
|
|
56
|
+
needs:
|
|
57
|
+
- docs-build
|
|
58
|
+
|
|
59
|
+
environment:
|
|
60
|
+
name: github-pages
|
|
61
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
62
|
+
|
|
63
|
+
steps:
|
|
64
|
+
- name: Deploy to GitHub Pages
|
|
65
|
+
id: deployment
|
|
66
|
+
uses: actions/deploy-pages@v1
|
package/.prettierrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# cybertoken [](https://github.com/nikeee/cybertoken/actions/workflows/CI.yaml) [](https://www.npmjs.com/package/cybertoken)
|
|
2
|
+
|
|
3
|
+
A token format for APIs inspired by the GitHub's API token format. Think of it as a standardized password format with some handy properties. Intended for API token and password generation.
|
|
4
|
+
|
|
5
|
+
## What is this?
|
|
6
|
+
|
|
7
|
+
GitHub changed their token format a while back and explained the reasons in an insightful blog post[^1].
|
|
8
|
+
|
|
9
|
+
tl;dr:
|
|
10
|
+
|
|
11
|
+
1. Having a defined format for secrets is good for **automated secret scanning**.
|
|
12
|
+
2. Putting some prefix to a secret makes **debugging easier** when looking at some random token.
|
|
13
|
+
3. Using `_` instead of `-` is better for double-click **text selection**: `abc_def` vs `abc-def`.
|
|
14
|
+
4. Using Base62[^2] instead of Base64 results in **less encoding errors** when passing the token somewhere.
|
|
15
|
+
5. **CRC32 checksum** at the end of the token for faster offline syntactic check of a token (used as a heuristic, not for security).
|
|
16
|
+
|
|
17
|
+
These properties make this token format suitable for things like API tokens, which is what cybertoken is.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Install:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install cybertoken
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { createTokenGenerator } from "cybertoken";
|
|
29
|
+
|
|
30
|
+
const apiTokenGenerator = createTokenGenerator({
|
|
31
|
+
prefixWithoutUnderscore: "contoso",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const token = apiTokenGenerator.generateToken();
|
|
35
|
+
// Securely hash `token` and save it in your database. Return `token` to the user once.
|
|
36
|
+
|
|
37
|
+
console.log(token);
|
|
38
|
+
// contoso_IvN2xEuHUDjQ3PNQgZkILWc7GUxpmThDD410NQxJalD5GjY
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Bonus
|
|
42
|
+
|
|
43
|
+
You can also use cybertokens as passwords. If you use GitHub, you can set up a [custom secret scanning pattern](https://docs.github.com/en/enterprise-cloud@latest/code-security/secret-scanning/defining-custom-patterns-for-secret-scanning) that will prevent anyone from pushing anything that looks like a cybertoken that your org uses.
|
|
44
|
+
|
|
45
|
+
For example, use this pattern for Cybertokens with the prefix `contosopass`:
|
|
46
|
+
|
|
47
|
+
```regex
|
|
48
|
+
contosopass_[0-9A-Za-z]{10,}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### CLI Usage
|
|
52
|
+
The npm package also offers a command-line utility to generate tokens.
|
|
53
|
+
```sh
|
|
54
|
+
npm install -g cybertoken
|
|
55
|
+
```
|
|
56
|
+
Usage:
|
|
57
|
+
```
|
|
58
|
+
cybertoken <prefix>
|
|
59
|
+
|
|
60
|
+
# example:
|
|
61
|
+
cybertoken test
|
|
62
|
+
test_KOK5QxQr4GBGk97X8Ij5ZnyZO24kMIEiW1LdUYIqEj7A5Nv
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also provide the token prefix via an env variable:
|
|
66
|
+
```sh
|
|
67
|
+
CYBERTOKEN_PREFIX=foo cybertoken
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To make thing easier, you can put this in your `.bashrc`:
|
|
71
|
+
```sh
|
|
72
|
+
echo "export CYBERTOKEN_PREFIX=foo" >> ~/.bashrc
|
|
73
|
+
```
|
|
74
|
+
...and now you can just use `cybertoken`!
|
|
75
|
+
|
|
76
|
+
## Anatomy
|
|
77
|
+
|
|
78
|
+
This is what a cybertoken consists of:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
prefix base62(n-bytes + v + crc32(n-bytes + v))
|
|
82
|
+
┌────────┐ ┌──────────────────────────────────────────────┐
|
|
83
|
+
myapitoken_161YNJQOaoFoWLaoXeMVHYyrSLhGPOjSBYBtxY6V3GqFFZKV
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Explanation:
|
|
87
|
+
|
|
88
|
+
- `n-bytes` are `n` random bytes, but at least 21 (default: 30), generated by a cryptographically secure random source
|
|
89
|
+
- `v` is a single byte containing the version of the cybertoken format used (currently, only `0x00`)
|
|
90
|
+
- The bytes are concatenated with the version and their CRC32 sum before base62 encoding
|
|
91
|
+
- base62 alphabet used: `0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`
|
|
92
|
+
|
|
93
|
+
[^1]: GitHub's blog post covering this: https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats
|
|
94
|
+
[^2]: More info in Base62: https://en.wikipedia.org/wiki/Base62
|
package/a.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as b from "./built/index.js";
|
|
2
|
+
|
|
3
|
+
const a = b.createTokenGenerator({ prefixWithoutUnderscore: "test" });
|
|
4
|
+
console.log(a.generateToken());
|
|
5
|
+
console.log(a.generateToken());
|
|
6
|
+
console.log(a.generateToken());
|
|
7
|
+
console.log(a.generateToken());
|
|
8
|
+
console.log(a.generateToken());
|
package/built/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/built/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { createTokenGenerator } from "./index.js";
|
|
3
|
+
const prefixWithoutUnderscore = (_a = process.argv[2]) !== null && _a !== void 0 ? _a : process.env.CYBERTOKEN_PREFIX;
|
|
4
|
+
if (!prefixWithoutUnderscore) {
|
|
5
|
+
console.error("Please specify a prefix for the token.");
|
|
6
|
+
console.error("A prefix must be a non-empty string.");
|
|
7
|
+
console.error();
|
|
8
|
+
console.error("Usage:");
|
|
9
|
+
console.error(" cybertoken <prefix>");
|
|
10
|
+
console.error();
|
|
11
|
+
console.error("You can also provide the prefix via the CYBERTOKEN_PREFIX environment variable.");
|
|
12
|
+
console.error("For example:");
|
|
13
|
+
console.error(" CYBERTOKEN_PREFIX=foo cybertoken");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const tokenGenerator = createTokenGenerator({
|
|
17
|
+
prefixWithoutUnderscore,
|
|
18
|
+
});
|
|
19
|
+
const token = tokenGenerator.generateToken();
|
|
20
|
+
console.log(token);
|
package/built/crc32.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function crc32(inputBuffer: Uint8Array): Uint8Array;
|
package/built/crc32.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const table = new Uint32Array([
|
|
2
|
+
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
|
|
3
|
+
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
|
4
|
+
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
|
|
5
|
+
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
|
6
|
+
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
|
|
7
|
+
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
|
8
|
+
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
|
9
|
+
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
|
10
|
+
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
|
|
11
|
+
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
|
12
|
+
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
|
|
13
|
+
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
|
14
|
+
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
|
|
15
|
+
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
|
16
|
+
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
|
|
17
|
+
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
|
18
|
+
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
|
|
19
|
+
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
|
20
|
+
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
|
|
21
|
+
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
|
22
|
+
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
|
23
|
+
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
|
24
|
+
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
|
|
25
|
+
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
|
26
|
+
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
|
|
27
|
+
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
|
28
|
+
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
|
|
29
|
+
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
|
30
|
+
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
|
|
31
|
+
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
|
32
|
+
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
|
|
33
|
+
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
|
34
|
+
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
|
|
35
|
+
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
|
36
|
+
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
|
37
|
+
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
|
38
|
+
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
|
|
39
|
+
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
|
40
|
+
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
|
|
41
|
+
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
|
42
|
+
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
|
|
43
|
+
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
|
44
|
+
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
|
|
45
|
+
]);
|
|
46
|
+
export default function crc32(inputBuffer) {
|
|
47
|
+
const ds = new Uint8Array(inputBuffer);
|
|
48
|
+
let crc = 0 ^ -1;
|
|
49
|
+
for (var i = 0; i < ds.length; i++) {
|
|
50
|
+
crc = (crc >>> 8) ^ table[(crc ^ ds[i]) & 0xff];
|
|
51
|
+
}
|
|
52
|
+
const result = new Uint8Array(4);
|
|
53
|
+
const dv = new DataView(result.buffer);
|
|
54
|
+
dv.setUint32(0, (crc ^ -1) >>> 0);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
package/built/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface TokenGeneratorOptions {
|
|
2
|
+
prefixWithoutUnderscore: string;
|
|
3
|
+
entropyBytes?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function createTokenGenerator(options: TokenGeneratorOptions): {
|
|
6
|
+
generateToken: () => string;
|
|
7
|
+
isTokenString: (value: unknown) => boolean;
|
|
8
|
+
};
|
package/built/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import crc32 from "./crc32.js";
|
|
3
|
+
import { base62, version } from "./constants.js";
|
|
4
|
+
import { getTokenPattern, parseTokenData } from "./parse.js";
|
|
5
|
+
const cryptoServices = (_a = globalThis.crypto) !== null && _a !== void 0 ? _a : require("node:crypto").webcrypto;
|
|
6
|
+
export function createTokenGenerator(options) {
|
|
7
|
+
var _a;
|
|
8
|
+
if (!options.prefixWithoutUnderscore) {
|
|
9
|
+
throw new Error("The `prefixWithoutUnderscore` option is required and must not be an empty string.");
|
|
10
|
+
}
|
|
11
|
+
const prefixWithUnderscore = options.prefixWithoutUnderscore + "_";
|
|
12
|
+
const tokenPattern = getTokenPattern(prefixWithUnderscore);
|
|
13
|
+
const tokenSecretByteCount = (_a = options.entropyBytes) !== null && _a !== void 0 ? _a : 30;
|
|
14
|
+
if (tokenSecretByteCount <= 20) {
|
|
15
|
+
throw new Error("The token secret byte count (`entropyBytes`) must be greater than 20.");
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
generateToken,
|
|
19
|
+
isTokenString,
|
|
20
|
+
};
|
|
21
|
+
function generateToken() {
|
|
22
|
+
const tokenData = generateTokenData();
|
|
23
|
+
const encodedData = base62.encode(tokenData);
|
|
24
|
+
return prefixWithUnderscore + encodedData;
|
|
25
|
+
}
|
|
26
|
+
function generateTokenData() {
|
|
27
|
+
const entropyWithVersion = cryptoServices.getRandomValues(new Uint8Array(tokenSecretByteCount + 1));
|
|
28
|
+
entropyWithVersion[entropyWithVersion.length - 1] = version;
|
|
29
|
+
const checksum = crc32(entropyWithVersion);
|
|
30
|
+
console.assert(checksum.byteLength === 4);
|
|
31
|
+
const payloadWithChecksum = new Uint8Array(entropyWithVersion.byteLength + checksum.byteLength);
|
|
32
|
+
payloadWithChecksum.set(entropyWithVersion, 0);
|
|
33
|
+
payloadWithChecksum.set(checksum, entropyWithVersion.byteLength);
|
|
34
|
+
console.assert(payloadWithChecksum.length === tokenSecretByteCount + 4 + 1);
|
|
35
|
+
return payloadWithChecksum;
|
|
36
|
+
}
|
|
37
|
+
function isTokenString(value) {
|
|
38
|
+
if (!value ||
|
|
39
|
+
typeof value !== "string" ||
|
|
40
|
+
!value.startsWith(prefixWithUnderscore) ||
|
|
41
|
+
!tokenPattern.test(value)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const tokenData = parseTokenData(value);
|
|
45
|
+
return !!tokenData && tokenData.isSyntacticallyValid;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { test, expect } from "vitest";
|
|
11
|
+
import { createTokenGenerator } from "./index.js";
|
|
12
|
+
test("Instance creation smoke test", () => {
|
|
13
|
+
createTokenGenerator({
|
|
14
|
+
prefixWithoutUnderscore: "test",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
test("Instance creation expect prefix", () => {
|
|
18
|
+
expect(() => createTokenGenerator({})).toThrowError("The `prefixWithoutUnderscore` option is required and must not be an empty string.");
|
|
19
|
+
});
|
|
20
|
+
test("Instance creation expect proper byte count", () => {
|
|
21
|
+
expect(() => createTokenGenerator({
|
|
22
|
+
prefixWithoutUnderscore: "a",
|
|
23
|
+
entropyBytes: -1,
|
|
24
|
+
})).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
|
|
25
|
+
expect(() => createTokenGenerator({
|
|
26
|
+
prefixWithoutUnderscore: "a",
|
|
27
|
+
entropyBytes: 0,
|
|
28
|
+
})).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
|
|
29
|
+
expect(() => createTokenGenerator({
|
|
30
|
+
prefixWithoutUnderscore: "a",
|
|
31
|
+
entropyBytes: 19,
|
|
32
|
+
})).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
|
|
33
|
+
expect(() => createTokenGenerator({
|
|
34
|
+
prefixWithoutUnderscore: "a",
|
|
35
|
+
entropyBytes: 20,
|
|
36
|
+
})).toThrowError("The token secret byte count (`entropyBytes`) must be greater than 20.");
|
|
37
|
+
createTokenGenerator({
|
|
38
|
+
prefixWithoutUnderscore: "a",
|
|
39
|
+
entropyBytes: 21,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
test("Roundtrip syntax check", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
const g = createTokenGenerator({ prefixWithoutUnderscore: "test" });
|
|
44
|
+
const token = yield g.generateToken();
|
|
45
|
+
expect(g.isTokenString(token)).toBe(true);
|
|
46
|
+
}));
|
|
47
|
+
test("Non-happy paths in isTokenString", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
48
|
+
const g = createTokenGenerator({ prefixWithoutUnderscore: "test" });
|
|
49
|
+
expect(g.isTokenString()).toBe(false);
|
|
50
|
+
expect(g.isTokenString(null)).toBe(false);
|
|
51
|
+
expect(g.isTokenString(undefined)).toBe(false);
|
|
52
|
+
expect(g.isTokenString("")).toBe(false);
|
|
53
|
+
expect(g.isTokenString("a")).toBe(false);
|
|
54
|
+
expect(g.isTokenString("a_")).toBe(false);
|
|
55
|
+
expect(g.isTokenString("a_1234")).toBe(false);
|
|
56
|
+
expect(g.isTokenString("test_")).toBe(false);
|
|
57
|
+
expect(g.isTokenString("test_1234")).toBe(false);
|
|
58
|
+
expect(g.isTokenString("test_")).toBe(false);
|
|
59
|
+
expect(g.isTokenString("test_AAAABBBB")).toBe(false);
|
|
60
|
+
expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbN")).toBe(false);
|
|
61
|
+
expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHd")).toBe(false);
|
|
62
|
+
expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyW")).toBe(false);
|
|
63
|
+
expect(g.isTokenString("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyw")).toBe(true);
|
|
64
|
+
}));
|
package/built/parse.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getTokenPattern(prefixWithUnderscore: string): RegExp;
|
|
2
|
+
export interface TokenContents {
|
|
3
|
+
prefixWithoutUnderscore: string;
|
|
4
|
+
secret: Uint8Array;
|
|
5
|
+
version: number;
|
|
6
|
+
suppliedChecksum: Uint8Array;
|
|
7
|
+
actualChecksum: Uint8Array;
|
|
8
|
+
isSyntacticallyValid: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseTokenData(token: string): TokenContents | undefined;
|
package/built/parse.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import crc32 from "./crc32.js";
|
|
2
|
+
import { alphabet, base62, version } from "./constants.js";
|
|
3
|
+
export function getTokenPattern(prefixWithUnderscore) {
|
|
4
|
+
return new RegExp(`^${prefixWithUnderscore}[${alphabet}]+$`);
|
|
5
|
+
}
|
|
6
|
+
export function parseTokenData(token) {
|
|
7
|
+
if (!token.includes("_")) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const splitData = token.split("_");
|
|
11
|
+
if (splitData.length !== 2) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const [prefix, encodedTokenData] = splitData;
|
|
15
|
+
const secretWithVersionAndChecksum = base62.decode(encodedTokenData);
|
|
16
|
+
if (!secretWithVersionAndChecksum ||
|
|
17
|
+
secretWithVersionAndChecksum.length <= 4) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const suppliedChecksum = secretWithVersionAndChecksum.slice(secretWithVersionAndChecksum.length - 4);
|
|
21
|
+
if (suppliedChecksum.length !== 4) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const secretAndVersionBuffer = secretWithVersionAndChecksum.slice(0, secretWithVersionAndChecksum.length - 4);
|
|
25
|
+
const actualChecksum = crc32(secretAndVersionBuffer);
|
|
26
|
+
const isSyntacticallyValid = secretAndVersionBuffer.length > 0 &&
|
|
27
|
+
buffersEqual(suppliedChecksum, actualChecksum);
|
|
28
|
+
const suppliedVersion = secretAndVersionBuffer[secretAndVersionBuffer.length - 1];
|
|
29
|
+
if (suppliedVersion !== version) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const secretPayload = secretAndVersionBuffer.slice(0, secretAndVersionBuffer.length - 1);
|
|
33
|
+
return {
|
|
34
|
+
version: suppliedVersion,
|
|
35
|
+
prefixWithoutUnderscore: prefix,
|
|
36
|
+
secret: secretPayload,
|
|
37
|
+
suppliedChecksum,
|
|
38
|
+
actualChecksum,
|
|
39
|
+
isSyntacticallyValid,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function buffersEqual(a, b) {
|
|
43
|
+
if (a.byteLength !== b.byteLength) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
for (let i = 0; i < a.byteLength; ++i) {
|
|
47
|
+
if (a[i] !== b[i]) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { test, expect } from "vitest";
|
|
2
|
+
import { parseTokenData } from "./parse.js";
|
|
3
|
+
test("Parse token contents", () => {
|
|
4
|
+
let contents;
|
|
5
|
+
contents = parseTokenData("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHd");
|
|
6
|
+
expect(contents).toBeUndefined();
|
|
7
|
+
contents = parseTokenData("test_");
|
|
8
|
+
expect(contents).toBeUndefined();
|
|
9
|
+
contents = parseTokenData("test_abc_def");
|
|
10
|
+
expect(contents).toBeUndefined();
|
|
11
|
+
contents = parseTokenData("test_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyw");
|
|
12
|
+
expect(contents).toEqual({
|
|
13
|
+
prefixWithoutUnderscore: "test",
|
|
14
|
+
actualChecksum: new Uint8Array([94, 199, 163, 74]),
|
|
15
|
+
isSyntacticallyValid: true,
|
|
16
|
+
secret: new Uint8Array([
|
|
17
|
+
100, 166, 3, 29, 89, 224, 104, 216, 82, 241, 34, 139, 193, 245, 62, 254,
|
|
18
|
+
71, 254, 26, 57, 131, 99, 11, 222, 170, 63, 150, 82, 53, 165,
|
|
19
|
+
]),
|
|
20
|
+
suppliedChecksum: new Uint8Array([94, 199, 163, 74]),
|
|
21
|
+
version: 0,
|
|
22
|
+
});
|
|
23
|
+
contents = parseTokenData("randomToken_R67NJs98Lvg5o42CanYRTirswpki3SAsJYbNiDwHdKNhiyw");
|
|
24
|
+
expect(contents).toEqual({
|
|
25
|
+
prefixWithoutUnderscore: "randomToken",
|
|
26
|
+
actualChecksum: new Uint8Array([94, 199, 163, 74]),
|
|
27
|
+
isSyntacticallyValid: true,
|
|
28
|
+
secret: new Uint8Array([
|
|
29
|
+
100, 166, 3, 29, 89, 224, 104, 216, 82, 241, 34, 139, 193, 245, 62, 254,
|
|
30
|
+
71, 254, 26, 57, 131, 99, 11, 222, 170, 63, 150, 82, 53, 165,
|
|
31
|
+
]),
|
|
32
|
+
suppliedChecksum: new Uint8Array([94, 199, 163, 74]),
|
|
33
|
+
version: 0,
|
|
34
|
+
});
|
|
35
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cybertoken",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A token format for APIs inspired by the GitHub's API token format.",
|
|
5
|
+
"author": "Niklas Mollenhauer",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "built/index.js",
|
|
9
|
+
"bin": "built/cli.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"compile": "tsc",
|
|
12
|
+
"clean": "rimraf built",
|
|
13
|
+
"docs": "typedoc",
|
|
14
|
+
"test": "tsc --noEmit && vitest run",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"test:watch": "vitest watch --coverage",
|
|
17
|
+
"prepare": "npm run clean && npm run compile"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"token",
|
|
21
|
+
"api",
|
|
22
|
+
"format",
|
|
23
|
+
"secret",
|
|
24
|
+
"generator"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"base-x": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@vitest/coverage-c8": "^0.26.3",
|
|
31
|
+
"rimraf": "^3.0.2",
|
|
32
|
+
"typedoc": "^0.23.23",
|
|
33
|
+
"vitest": "^0.26.3"
|
|
34
|
+
}
|
|
35
|
+
}
|