Skip to content

Commit 762cdbe

Browse files
committed
personal access key format
1 parent 98c096f commit 762cdbe

File tree

1 file changed

+96
-0
lines changed

1 file changed

+96
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as Crypto from 'crypto';
2+
import bcrypt from 'bcryptjs';
3+
4+
/**
5+
* @module PersonalAccessKey
6+
* Contains functions for generating an parsonal acces key.
7+
*/
8+
9+
/**
10+
* Payload within the access token.
11+
*/
12+
type DecodedAccessKey = {
13+
/** UUID as stored within the database ("organization_access_tokens"."id") */
14+
id: string;
15+
/** string to compare against the hash within the database ("personal_access_tokens"."hash") */
16+
privateKey: string;
17+
};
18+
19+
/**
20+
* Prefix for the organization access key.
21+
* We use this prefix so we can quickly identify whether an organization access token.
22+
*
23+
* **hv** -> Hive
24+
* **p** -> Personal
25+
* **1** -> Version 1
26+
*/
27+
const keyPrefix = 'hvp1/';
28+
const decodeError = { type: 'error' as const, reason: 'Invalid access token.' };
29+
30+
function encode(recordId: string, secret: string) {
31+
const keyContents = [recordId, secret].join(':');
32+
return keyPrefix + btoa(keyContents);
33+
}
34+
35+
/**
36+
* Attempt to decode a user provided access token string into the embedded id and private key.
37+
*/
38+
export function decode(
39+
accessToken: string,
40+
): { type: 'error'; reason: string } | { type: 'ok'; accessKey: DecodedAccessKey } {
41+
if (!accessToken.startsWith(keyPrefix)) {
42+
return decodeError;
43+
}
44+
45+
accessToken = accessToken.slice(keyPrefix.length);
46+
47+
let str: string;
48+
49+
try {
50+
str = globalThis.atob(accessToken);
51+
} catch (error) {
52+
return decodeError;
53+
}
54+
55+
const parts = str.split(':');
56+
57+
if (parts.length > 2) {
58+
return decodeError;
59+
}
60+
61+
const id = parts.at(0);
62+
const privateKey = parts.at(1);
63+
64+
if (id && privateKey) {
65+
return { type: 'ok', accessKey: { id, privateKey } } as const;
66+
}
67+
68+
return decodeError;
69+
}
70+
71+
/**
72+
* Creates a new organization access key/token for a provided UUID.
73+
*/
74+
export async function create(id: string) {
75+
const secret = Crypto.createHash('sha256')
76+
.update(Crypto.randomBytes(20).toString())
77+
.digest('hex');
78+
79+
const hash = await bcrypt.hash(secret, await bcrypt.genSalt());
80+
const privateAccessToken = encode(id, secret);
81+
const firstCharacters = privateAccessToken.substr(0, 10);
82+
83+
return {
84+
privateAccessToken,
85+
hash,
86+
firstCharacters,
87+
};
88+
}
89+
90+
/**
91+
* Verify whether a organization access key private key matches the
92+
* hash stored within the "organization_access_tokens"."hash" table.
93+
*/
94+
export async function verify(secret: string, hash: string) {
95+
return await bcrypt.compare(secret, hash);
96+
}

0 commit comments

Comments
 (0)