@wener/common 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/LICENSE +21 -0
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -0
- package/lib/jsonschema/JsonSchema.js +180 -0
- package/lib/jsonschema/JsonSchema.js.map +1 -0
- package/lib/jsonschema/index.js +2 -0
- package/lib/jsonschema/index.js.map +1 -0
- package/lib/jsonschema/types.d.js +3 -0
- package/lib/jsonschema/types.d.js.map +1 -0
- package/lib/meta/defineInit.js +42 -0
- package/lib/meta/defineInit.js.map +1 -0
- package/lib/meta/defineMetadata.js +30 -0
- package/lib/meta/defineMetadata.js.map +1 -0
- package/lib/meta/index.js +3 -0
- package/lib/meta/index.js.map +1 -0
- package/lib/normalizePagination.js +14 -0
- package/lib/normalizePagination.js.map +1 -0
- package/lib/parseSort.js +91 -0
- package/lib/parseSort.js.map +1 -0
- package/lib/password/PHC.js +200 -0
- package/lib/password/PHC.js.map +1 -0
- package/lib/password/Password.js +83 -0
- package/lib/password/Password.js.map +1 -0
- package/lib/password/createArgon2PasswordAlgorithm.js +53 -0
- package/lib/password/createArgon2PasswordAlgorithm.js.map +1 -0
- package/lib/password/createBase64PasswordAlgorithm.js +14 -0
- package/lib/password/createBase64PasswordAlgorithm.js.map +1 -0
- package/lib/password/createBcryptPasswordAlgorithm.js +20 -0
- package/lib/password/createBcryptPasswordAlgorithm.js.map +1 -0
- package/lib/password/createPBKDF2PasswordAlgorithm.js +54 -0
- package/lib/password/createPBKDF2PasswordAlgorithm.js.map +1 -0
- package/lib/password/createScryptPasswordAlgorithm.js +66 -0
- package/lib/password/createScryptPasswordAlgorithm.js.map +1 -0
- package/lib/password/index.js +6 -0
- package/lib/password/index.js.map +1 -0
- package/lib/password/server/index.js +2 -0
- package/lib/password/server/index.js.map +1 -0
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js +85 -0
- package/lib/tools/renderJsonSchemaToMarkdownDoc.js.map +1 -0
- package/package.json +56 -0
- package/src/index.ts +2 -0
- package/src/jsonschema/JsonSchema.test.ts +27 -0
- package/src/jsonschema/JsonSchema.ts +197 -0
- package/src/jsonschema/index.ts +2 -0
- package/src/jsonschema/types.d.ts +173 -0
- package/src/meta/defineInit.ts +68 -0
- package/src/meta/defineMetadata.test.ts +15 -0
- package/src/meta/defineMetadata.ts +57 -0
- package/src/meta/index.ts +3 -0
- package/src/normalizePagination.ts +25 -0
- package/src/parseSort.test.ts +41 -0
- package/src/parseSort.ts +115 -0
- package/src/password/PHC.test.ts +317 -0
- package/src/password/PHC.ts +247 -0
- package/src/password/Password.test.ts +58 -0
- package/src/password/Password.ts +113 -0
- package/src/password/createArgon2PasswordAlgorithm.ts +80 -0
- package/src/password/createBase64PasswordAlgorithm.ts +14 -0
- package/src/password/createBcryptPasswordAlgorithm.ts +30 -0
- package/src/password/createPBKDF2PasswordAlgorithm.ts +73 -0
- package/src/password/createScryptPasswordAlgorithm.ts +72 -0
- package/src/password/index.ts +5 -0
- package/src/password/server/index.ts +1 -0
- package/src/tools/renderJsonSchemaToMarkdownDoc.ts +93 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { ArrayBuffers } from '@wener/utils';
|
|
2
|
+
|
|
3
|
+
export namespace PHC {
|
|
4
|
+
// https://github.com/simonepri/phc-format/blob/master/index.js
|
|
5
|
+
|
|
6
|
+
const idRegex = /^[a-z0-9-]{1,32}$/;
|
|
7
|
+
const nameRegex = /^[a-z0-9-]{1,32}$/;
|
|
8
|
+
const valueRegex = /^[a-zA-Z0-9/+.-]+$/;
|
|
9
|
+
const b64Regex = /^([a-zA-Z0-9/+.-]+|)$/;
|
|
10
|
+
const decimalRegex = /^((-)?[1-9]\d*|0)$/;
|
|
11
|
+
const versionRegex = /^v=(\d+)$/;
|
|
12
|
+
|
|
13
|
+
const fromBase64 = ArrayBuffers.fromBase64;
|
|
14
|
+
const toBase64 = ArrayBuffers.toBase64;
|
|
15
|
+
const isBuffer = (v: any): v is Uint8Array => {
|
|
16
|
+
return v instanceof Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function objToKeyVal(obj: Record<string, any>): string {
|
|
20
|
+
return objectKeys(obj)
|
|
21
|
+
.map((k) => [k, obj[k]].join('='))
|
|
22
|
+
.join(',');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function keyValtoObj(str: string): Record<string, string> {
|
|
26
|
+
const obj: Record<string, string> = {};
|
|
27
|
+
str.split(',').forEach((ps) => {
|
|
28
|
+
const pss = ps.split('=');
|
|
29
|
+
if (pss.length < 2) {
|
|
30
|
+
throw new TypeError(`params must be in the format name=value`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const key = pss.shift();
|
|
34
|
+
if (key !== undefined) {
|
|
35
|
+
obj[key] = pss.join('=');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return obj;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function objectKeys<T extends object>(object: T): Array<keyof T> {
|
|
42
|
+
return Object.keys(object) as Array<keyof T>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function objectValues<T extends object>(object: T): Array<T[keyof T]> {
|
|
46
|
+
if (typeof Object.values === 'function') return Object.values(object);
|
|
47
|
+
return objectKeys(object).map((k) => object[k]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SerializeOptions {
|
|
51
|
+
id: string;
|
|
52
|
+
version?: number;
|
|
53
|
+
params?: Record<string, string | number | Uint8Array>;
|
|
54
|
+
salt?: Uint8Array;
|
|
55
|
+
hash?: Uint8Array;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generates a PHC string using the data provided.
|
|
60
|
+
* @param {SerializeOptions} opts Object that holds the data needed to generate the PHC string.
|
|
61
|
+
* @return {string} The hash string adhering to the PHC format.
|
|
62
|
+
*/
|
|
63
|
+
export function serialize(opts: SerializeOptions): string {
|
|
64
|
+
const fields: string[] = [''];
|
|
65
|
+
|
|
66
|
+
if (typeof opts !== 'object' || opts === null) {
|
|
67
|
+
throw new TypeError('opts must be an object');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Identifier Validation
|
|
71
|
+
if (typeof opts.id !== 'string') {
|
|
72
|
+
throw new TypeError('id must be a string');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!idRegex.test(opts.id)) {
|
|
76
|
+
throw new TypeError(`id must satisfy ${idRegex}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fields.push(opts.id);
|
|
80
|
+
|
|
81
|
+
if (typeof opts.version !== 'undefined') {
|
|
82
|
+
if (typeof opts.version !== 'number' || opts.version < 0 || !Number.isInteger(opts.version)) {
|
|
83
|
+
throw new TypeError('version must be a positive integer number');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fields.push(`v=${opts.version}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parameters Validation
|
|
90
|
+
if (typeof opts.params !== 'undefined') {
|
|
91
|
+
if (typeof opts.params !== 'object' || opts.params === null) {
|
|
92
|
+
throw new TypeError('params must be an object');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pk = objectKeys(opts.params);
|
|
96
|
+
if (!pk.every((p) => nameRegex.test(p.toString()))) {
|
|
97
|
+
throw new TypeError(`params names must satisfy ${nameRegex}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Convert Numbers into Numeric Strings and Buffers into B64 encoded strings.
|
|
101
|
+
pk.forEach((k) => {
|
|
102
|
+
const value = opts.params![k];
|
|
103
|
+
if (typeof value === 'number') {
|
|
104
|
+
opts.params![k] = value.toString();
|
|
105
|
+
} else if (value instanceof Uint8Array) {
|
|
106
|
+
opts.params![k] = toBase64(value).split('=')[0];
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const pv = objectValues(opts.params);
|
|
110
|
+
if (!pv.every((v) => typeof v === 'string')) {
|
|
111
|
+
throw new TypeError('params values must be strings');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!pv.every((v) => valueRegex.test(v))) {
|
|
115
|
+
throw new TypeError(`params values must satisfy ${valueRegex}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const strpar = objToKeyVal(opts.params as Record<string, string>);
|
|
119
|
+
fields.push(strpar);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof opts.salt !== 'undefined') {
|
|
123
|
+
// Salt Validation
|
|
124
|
+
if (!isBuffer(opts.salt)) {
|
|
125
|
+
throw new TypeError('salt must be a Buffer');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fields.push(toBase64(opts.salt).split('=')[0]);
|
|
129
|
+
|
|
130
|
+
if (typeof opts.hash !== 'undefined') {
|
|
131
|
+
// Hash Validation
|
|
132
|
+
if (!isBuffer(opts.hash)) {
|
|
133
|
+
throw new TypeError('hash must be a Buffer');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fields.push(toBase64(opts.hash).split('=')[0]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create the PHC formatted string
|
|
141
|
+
const phcstr = fields.join('$');
|
|
142
|
+
|
|
143
|
+
return phcstr;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface DeserializeResult {
|
|
147
|
+
id: string;
|
|
148
|
+
version?: number;
|
|
149
|
+
params?: Record<string, string | number>;
|
|
150
|
+
salt?: Uint8Array;
|
|
151
|
+
hash?: Uint8Array;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parses data from a PHC string.
|
|
156
|
+
* @param {string} phcstr A PHC string to parse.
|
|
157
|
+
* @return {DeserializeResult} The object containing the data parsed from the PHC string.
|
|
158
|
+
*/
|
|
159
|
+
export function deserialize(phcstr: string): DeserializeResult {
|
|
160
|
+
if (typeof phcstr !== 'string' || phcstr === '') {
|
|
161
|
+
throw new TypeError('pchstr must be a non-empty string');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (phcstr[0] !== '$') {
|
|
165
|
+
throw new TypeError('pchstr must contain a $ as first char');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fields = phcstr.split('$');
|
|
169
|
+
// Remove first empty $
|
|
170
|
+
fields.shift();
|
|
171
|
+
|
|
172
|
+
// Parse Fields
|
|
173
|
+
let maxf = 5;
|
|
174
|
+
if (!versionRegex.test(fields[1])) maxf--;
|
|
175
|
+
if (fields.length > maxf) {
|
|
176
|
+
throw new TypeError(`pchstr contains too many fileds: ${fields.length}/${maxf}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Parse Identifier
|
|
180
|
+
const id = fields.shift();
|
|
181
|
+
if (!id || !idRegex.test(id)) {
|
|
182
|
+
throw new TypeError(`id must satisfy ${idRegex}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let version: number | undefined;
|
|
186
|
+
// Parse Version
|
|
187
|
+
if (fields[0] && versionRegex.test(fields[0])) {
|
|
188
|
+
const versionMatch = fields.shift()?.match(versionRegex);
|
|
189
|
+
version = versionMatch ? parseInt(versionMatch[1], 10) : undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let hash: Uint8Array | undefined;
|
|
193
|
+
let salt: Uint8Array | undefined;
|
|
194
|
+
if (fields[fields.length - 1] && b64Regex.test(fields[fields.length - 1])) {
|
|
195
|
+
if (fields.length > 1 && b64Regex.test(fields[fields.length - 2])) {
|
|
196
|
+
// Parse Hash
|
|
197
|
+
const hashStr = fields.pop();
|
|
198
|
+
if (hashStr) hash = fromBase64(hashStr);
|
|
199
|
+
// Parse Salt
|
|
200
|
+
const saltStr = fields.pop();
|
|
201
|
+
if (saltStr !== undefined) salt = fromBase64(saltStr);
|
|
202
|
+
} else {
|
|
203
|
+
// Parse Salt
|
|
204
|
+
const saltStr = fields.pop();
|
|
205
|
+
if (saltStr !== undefined) salt = fromBase64(saltStr);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Parse Parameters
|
|
210
|
+
let params: Record<string, string | number> | undefined;
|
|
211
|
+
if (fields.length > 0) {
|
|
212
|
+
const parstr = fields.pop();
|
|
213
|
+
if (parstr) {
|
|
214
|
+
params = keyValtoObj(parstr);
|
|
215
|
+
if (!Object.keys(params).every((p) => nameRegex.test(p))) {
|
|
216
|
+
throw new TypeError(`params names must satisfy ${nameRegex}}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const pv = Object.values(params);
|
|
220
|
+
if (!pv.every((v) => valueRegex.test(String(v)))) {
|
|
221
|
+
throw new TypeError(`params values must satisfy ${valueRegex}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Convert Decimal Strings into Numbers
|
|
225
|
+
Object.keys(params).forEach((k) => {
|
|
226
|
+
const value = params![k];
|
|
227
|
+
if (typeof value === 'string' && decimalRegex.test(value)) {
|
|
228
|
+
params![k] = parseInt(value, 10);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (fields.length > 0) {
|
|
235
|
+
throw new TypeError(`pchstr contains unrecognized fileds: ${fields}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build the output object
|
|
239
|
+
const phcobj: DeserializeResult = { id };
|
|
240
|
+
if (version !== undefined) phcobj.version = version;
|
|
241
|
+
if (params) phcobj.params = params;
|
|
242
|
+
if (salt) phcobj.salt = salt;
|
|
243
|
+
if (hash) phcobj.hash = hash;
|
|
244
|
+
|
|
245
|
+
return phcobj;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { createArgon2PasswordAlgorithm } from './createArgon2PasswordAlgorithm';
|
|
3
|
+
import { createBase64PasswordAlgorithm } from './createBase64PasswordAlgorithm';
|
|
4
|
+
import { createBcryptPasswordAlgorithm } from './createBcryptPasswordAlgorithm';
|
|
5
|
+
import { Password } from './Password';
|
|
6
|
+
|
|
7
|
+
describe('Password', () => {
|
|
8
|
+
const check = async ({
|
|
9
|
+
password = '1',
|
|
10
|
+
...rest
|
|
11
|
+
}: Password.PasswordHashOptions & {
|
|
12
|
+
password?: string;
|
|
13
|
+
}) => {
|
|
14
|
+
let out = await Password.hash(password, rest);
|
|
15
|
+
console.log(`${rest.algorithm || 'default'}: hash ${out}`);
|
|
16
|
+
let result = await Password.verify(password, out);
|
|
17
|
+
expect(result).toBe(true);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
Password.addAlgorithm(createBcryptPasswordAlgorithm());
|
|
21
|
+
Password.addAlgorithm(createBase64PasswordAlgorithm());
|
|
22
|
+
Password.addAlgorithm(
|
|
23
|
+
createArgon2PasswordAlgorithm({
|
|
24
|
+
provide: import('argon2'),
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
// Password.addAlgorithm(createScryptPasswordAlgorithm());
|
|
28
|
+
|
|
29
|
+
test('base', async () => {
|
|
30
|
+
await check({});
|
|
31
|
+
|
|
32
|
+
await check({ algorithm: 'bcrypt' });
|
|
33
|
+
|
|
34
|
+
await check({ algorithm: 'base64' });
|
|
35
|
+
// invalid base 64
|
|
36
|
+
await check({ algorithm: 'base64', password: '你好' });
|
|
37
|
+
|
|
38
|
+
await check({ algorithm: '5' });
|
|
39
|
+
await check({ algorithm: '6' });
|
|
40
|
+
await check({ algorithm: '7' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should verify manual created hash', async () => {
|
|
44
|
+
const tests: Array<{
|
|
45
|
+
password?: string;
|
|
46
|
+
hash: string;
|
|
47
|
+
}> = [
|
|
48
|
+
{ hash: '$2y$10$MQ057tMbDG6/lVkGFWrNwOR9kh/5rzbkhBPrwNPTPuZ5wBpGNbWLa' },
|
|
49
|
+
{ hash: '$argon2i$v=19$m=16,t=2,p=1$SDZBU29LRUp0eTJyRDJqZg$76L95nAjG4SjjdoR0YZyFw' },
|
|
50
|
+
{ hash: '$argon2d$v=19$m=16,t=2,p=1$SDZBU29LRUp0eTJyRDJqZg$+cB2R45sauVlfxbGslAmOw' },
|
|
51
|
+
{ hash: '$argon2id$v=19$m=16,t=2,p=1$SDZBU29LRUp0eTJyRDJqZg$iP9HYuSDXgG2lW7KARBuQQ' },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const { password = '1', hash } of tests) {
|
|
55
|
+
expect(await Password.verify(password, hash)).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Errors } from '@wener/utils';
|
|
2
|
+
import { createPBKDF2PasswordAlgorithm } from './createPBKDF2PasswordAlgorithm';
|
|
3
|
+
import { PHC } from './PHC';
|
|
4
|
+
|
|
5
|
+
export namespace Password {
|
|
6
|
+
export interface ParsedPassword {
|
|
7
|
+
id: string;
|
|
8
|
+
version?: number;
|
|
9
|
+
params?: Record<string, string | number>;
|
|
10
|
+
salt?: Uint8Array;
|
|
11
|
+
hash?: Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type PasswordAlgorithmHashOptions = {
|
|
15
|
+
rounds?: number;
|
|
16
|
+
salt?: Uint8Array;
|
|
17
|
+
id?: string;
|
|
18
|
+
};
|
|
19
|
+
type PasswordAlgorithmVerifyOptions = ParsedPassword;
|
|
20
|
+
export type PasswordAlgorithm = {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly ids?: string[];
|
|
23
|
+
hash(password: string, opts?: PasswordAlgorithmHashOptions): Promise<string>;
|
|
24
|
+
verify(password: string, hash: string, opts: PasswordAlgorithmVerifyOptions): Promise<boolean>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const Algorithms: Record<string, string | PasswordAlgorithm> = {
|
|
28
|
+
1: 'md5',
|
|
29
|
+
'2a': 'bcrypt', // original
|
|
30
|
+
'2b': 'bcrypt', // February 2014
|
|
31
|
+
'2x': 'bcrypt', // June 2011
|
|
32
|
+
'2y': 'bcrypt', // June 2011
|
|
33
|
+
5: 'sha256',
|
|
34
|
+
6: 'sha512',
|
|
35
|
+
7: 'scrypt',
|
|
36
|
+
};
|
|
37
|
+
let DefaultAlgorithm: string = '6';
|
|
38
|
+
|
|
39
|
+
export function setDefaultAlgorithm(algorithm: string) {
|
|
40
|
+
Errors.BadRequest.check(Algorithms[algorithm], `Unknown algorithm ${algorithm}`);
|
|
41
|
+
DefaultAlgorithm = algorithm;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getDefaultAlgorithm() {
|
|
45
|
+
return DefaultAlgorithm;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function addAlgorithm(algorithm: PasswordAlgorithm) {
|
|
49
|
+
Algorithms[algorithm.name] = algorithm;
|
|
50
|
+
if (algorithm.ids) {
|
|
51
|
+
for (const id of algorithm.ids) {
|
|
52
|
+
Algorithms[id] = algorithm;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
addAlgorithm(
|
|
58
|
+
createPBKDF2PasswordAlgorithm({
|
|
59
|
+
id: 'sha256',
|
|
60
|
+
digest: 'SHA-256',
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
addAlgorithm(
|
|
64
|
+
createPBKDF2PasswordAlgorithm({
|
|
65
|
+
id: 'sha512',
|
|
66
|
+
digest: 'SHA-512',
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export async function parse(hash: string) {
|
|
71
|
+
return PHC.deserialize(hash);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveAlgorithm(id: string | PasswordAlgorithm): PasswordAlgorithm {
|
|
75
|
+
let f = id;
|
|
76
|
+
while (typeof f === 'string') {
|
|
77
|
+
f = Algorithms[f];
|
|
78
|
+
}
|
|
79
|
+
if (!f) {
|
|
80
|
+
throw new Error(`Unknown algorithm ${id}`);
|
|
81
|
+
}
|
|
82
|
+
return f;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function check(password: string, hash: string) {
|
|
86
|
+
let res = await parse(hash);
|
|
87
|
+
let f = resolveAlgorithm(res.id);
|
|
88
|
+
return {
|
|
89
|
+
result: f.verify(password, hash, res),
|
|
90
|
+
parsed: res,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function verify(password: string, hash: string) {
|
|
95
|
+
let res = await parse(hash);
|
|
96
|
+
let f = resolveAlgorithm(res.id);
|
|
97
|
+
return f.verify(password, hash, res);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type PasswordHashOptions = PasswordAlgorithmHashOptions & {
|
|
101
|
+
algorithm?: string | PasswordAlgorithm;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export async function hash(password: string, { algorithm, ...opts }: PasswordHashOptions = {}) {
|
|
105
|
+
let f = resolveAlgorithm(algorithm ?? DefaultAlgorithm);
|
|
106
|
+
let id = algorithm ?? DefaultAlgorithm;
|
|
107
|
+
typeof id !== 'string' && (id = f.name);
|
|
108
|
+
return f.hash(password, {
|
|
109
|
+
id,
|
|
110
|
+
...opts,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { maybeFunction, type MaybeFunction, type MaybePromise } from '@wener/utils';
|
|
2
|
+
import { Password } from './Password';
|
|
3
|
+
|
|
4
|
+
type Provide = {
|
|
5
|
+
hash: (password: string, options: { salt?: Buffer; raw?: boolean; type?: 0 | 1 | 2 }) => Promise<string>;
|
|
6
|
+
verify: (hash: string, password: string) => Promise<boolean>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function createArgon2PasswordAlgorithm({
|
|
10
|
+
type,
|
|
11
|
+
provide = async () => {
|
|
12
|
+
throw new Error('Please provide argon2');
|
|
13
|
+
// const { default: wasm } = await import('argon2-browser/dist/argon2.wasm');
|
|
14
|
+
// const argon2 = await WebAssembly.instantiateStreaming(fetch(wasm), {
|
|
15
|
+
// env: {
|
|
16
|
+
// memoryBase: 0,
|
|
17
|
+
// tableBase: 0,
|
|
18
|
+
// memory: new WebAssembly.Memory({ initial: 256 }),
|
|
19
|
+
// table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
|
|
20
|
+
// __memory_base: 0,
|
|
21
|
+
// __table_base: 0,
|
|
22
|
+
// },
|
|
23
|
+
// });
|
|
24
|
+
// console.log(argon2.instance.exports);
|
|
25
|
+
// const { hash } = argon2.instance.exports as any as typeof import('argon2-browser');
|
|
26
|
+
},
|
|
27
|
+
// argon2-browser/dist/argon2-bundled.min.js
|
|
28
|
+
// import('argon2-browser').then(({ default: { hash, verify } }) => {
|
|
29
|
+
// return {
|
|
30
|
+
// hash(password, options) {
|
|
31
|
+
// return hash({
|
|
32
|
+
// pass: password,
|
|
33
|
+
// });
|
|
34
|
+
// },
|
|
35
|
+
// verify(hash, password) {
|
|
36
|
+
// return verify({
|
|
37
|
+
// pass: password,
|
|
38
|
+
// hash: hash,
|
|
39
|
+
// })
|
|
40
|
+
// .then(() => true)
|
|
41
|
+
// .catch(() => false);
|
|
42
|
+
// },
|
|
43
|
+
// };
|
|
44
|
+
// }),
|
|
45
|
+
// provide = () => import('argon2'),
|
|
46
|
+
}: {
|
|
47
|
+
type?: 'argon2d' | 'argon2i' | 'argon2id';
|
|
48
|
+
provide?: MaybeFunction<MaybePromise<Provide>>;
|
|
49
|
+
} = {}): Password.PasswordAlgorithm {
|
|
50
|
+
// 0=Argon2d, 1=Argon2i, 2=Argon2id
|
|
51
|
+
const toType: Record<string, 0 | 1 | 2 | undefined> = {
|
|
52
|
+
argon2d: 0,
|
|
53
|
+
argon2i: 1,
|
|
54
|
+
argon2id: 2,
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
let mod: Provide;
|
|
58
|
+
const resolve = () => {
|
|
59
|
+
if (mod) return mod;
|
|
60
|
+
return Promise.resolve(maybeFunction(provide)).then((v) => (mod = v));
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
name: 'argon2',
|
|
64
|
+
ids: ['argon2i', 'argon2d', 'argon2id'],
|
|
65
|
+
async hash(password: string, opts) {
|
|
66
|
+
// const { hash } = await import('argon2');
|
|
67
|
+
const { hash } = await resolve();
|
|
68
|
+
const id = opts?.id;
|
|
69
|
+
return hash(password, {
|
|
70
|
+
salt: opts?.salt ? Buffer.from(opts.salt) : undefined,
|
|
71
|
+
raw: false,
|
|
72
|
+
type: toType[id || ''] ?? toType[type || ''],
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
async verify(password: string, hash: string) {
|
|
76
|
+
const { verify } = await resolve();
|
|
77
|
+
return verify(hash, password);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ArrayBuffers } from '@wener/utils';
|
|
2
|
+
import { Password } from './Password';
|
|
3
|
+
|
|
4
|
+
export function createBase64PasswordAlgorithm({ id = 'base64' }: { id?: string } = {}): Password.PasswordAlgorithm {
|
|
5
|
+
return {
|
|
6
|
+
name: id,
|
|
7
|
+
async hash(password: string) {
|
|
8
|
+
return `$${id}$$${ArrayBuffers.toBase64(password).replace(/=/g, '')}`;
|
|
9
|
+
},
|
|
10
|
+
async verify(password: string, hash: string, opts) {
|
|
11
|
+
return Boolean(opts.hash) && ArrayBuffers.toString(opts.hash!) === password;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MaybePromise } from '@wener/utils';
|
|
2
|
+
import { Password } from './Password';
|
|
3
|
+
|
|
4
|
+
type ProviderType = () => MaybePromise<{
|
|
5
|
+
hash: (password: string, rounds: number | string) => Promise<string>;
|
|
6
|
+
compare: (password: string, hash: string) => Promise<boolean>;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
export function createBcryptPasswordAlgorithm({
|
|
10
|
+
// provider = () => import('bcrypt').then((v) => v.default),
|
|
11
|
+
provider = () => import('bcryptjs').then((v) => v.default),
|
|
12
|
+
}: {
|
|
13
|
+
provider?: ProviderType;
|
|
14
|
+
} = {}): Password.PasswordAlgorithm {
|
|
15
|
+
// bcrypt or bcryptjs
|
|
16
|
+
return {
|
|
17
|
+
name: 'bcrypt',
|
|
18
|
+
async hash(password: string, opts) {
|
|
19
|
+
const { hash } = await provider();
|
|
20
|
+
return hash(password, opts?.rounds ?? 10);
|
|
21
|
+
},
|
|
22
|
+
async verify(password: string, hash: string) {
|
|
23
|
+
const { compare } = await provider();
|
|
24
|
+
if (hash.startsWith('$2y$')) {
|
|
25
|
+
hash = hash.replace(/^\$2y\$/, '$2a$');
|
|
26
|
+
}
|
|
27
|
+
return compare(password, hash);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Errors } from '@wener/utils';
|
|
2
|
+
import { Password } from './Password';
|
|
3
|
+
import { PHC } from './PHC';
|
|
4
|
+
|
|
5
|
+
export function createPBKDF2PasswordAlgorithm({
|
|
6
|
+
id,
|
|
7
|
+
digest,
|
|
8
|
+
iterations = 100000,
|
|
9
|
+
saltlen = 16,
|
|
10
|
+
keylen = digest === 'SHA-256' ? 32 : 64,
|
|
11
|
+
}: {
|
|
12
|
+
id: string;
|
|
13
|
+
digest: 'SHA-256' | 'SHA-512';
|
|
14
|
+
iterations?: number;
|
|
15
|
+
keylen?: number;
|
|
16
|
+
saltlen?: number;
|
|
17
|
+
}): Password.PasswordAlgorithm {
|
|
18
|
+
return {
|
|
19
|
+
name: id,
|
|
20
|
+
async hash(password: string, opts) {
|
|
21
|
+
let salt: Uint8Array;
|
|
22
|
+
|
|
23
|
+
if (opts?.salt) {
|
|
24
|
+
salt = opts.salt;
|
|
25
|
+
} else {
|
|
26
|
+
salt = new Uint8Array(saltlen);
|
|
27
|
+
crypto.getRandomValues(salt);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rounds = opts?.rounds ?? iterations;
|
|
31
|
+
|
|
32
|
+
let key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [
|
|
33
|
+
'deriveBits',
|
|
34
|
+
]);
|
|
35
|
+
let hash = await crypto.subtle.deriveBits(
|
|
36
|
+
{ name: 'PBKDF2', iterations: rounds, salt, hash: digest },
|
|
37
|
+
key,
|
|
38
|
+
keylen * 8,
|
|
39
|
+
);
|
|
40
|
+
return PHC.serialize({
|
|
41
|
+
id: opts?.id ?? id,
|
|
42
|
+
params: {
|
|
43
|
+
rounds,
|
|
44
|
+
},
|
|
45
|
+
salt,
|
|
46
|
+
hash: new Uint8Array(hash),
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
async verify(password: string, _: string, opts) {
|
|
50
|
+
const rounds = opts?.params?.rounds ?? iterations;
|
|
51
|
+
const salt = opts.salt;
|
|
52
|
+
const storedHash = opts.hash;
|
|
53
|
+
Errors.BadRequest.check(typeof rounds === 'number', 'Invalid rounds');
|
|
54
|
+
Errors.BadRequest.check(salt instanceof Uint8Array, 'Invalid salt');
|
|
55
|
+
Errors.BadRequest.check(storedHash instanceof Uint8Array, 'Invalid hash');
|
|
56
|
+
|
|
57
|
+
let key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [
|
|
58
|
+
'deriveBits',
|
|
59
|
+
]);
|
|
60
|
+
let hash = await crypto.subtle.deriveBits(
|
|
61
|
+
{
|
|
62
|
+
name: 'PBKDF2',
|
|
63
|
+
iterations: rounds,
|
|
64
|
+
salt,
|
|
65
|
+
hash: digest,
|
|
66
|
+
},
|
|
67
|
+
key,
|
|
68
|
+
storedHash.length * 8,
|
|
69
|
+
);
|
|
70
|
+
return new Uint8Array(hash).every((v, i) => v === storedHash![i]);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { Errors } from '@wener/utils';
|
|
3
|
+
import { Password } from './Password';
|
|
4
|
+
import { PHC } from './PHC';
|
|
5
|
+
|
|
6
|
+
export function createScryptPasswordAlgorithm(
|
|
7
|
+
options: {
|
|
8
|
+
id?: string;
|
|
9
|
+
cost?: number;
|
|
10
|
+
blocksize?: number;
|
|
11
|
+
parallelism?: number;
|
|
12
|
+
saltlen?: number;
|
|
13
|
+
keylen?: number;
|
|
14
|
+
} = {},
|
|
15
|
+
): Password.PasswordAlgorithm {
|
|
16
|
+
let id = options.id || 'scrypt';
|
|
17
|
+
options.cost ||= Math.pow(2, 14);
|
|
18
|
+
options.blocksize ||= 8;
|
|
19
|
+
options.parallelism ||= 1;
|
|
20
|
+
options.saltlen ||= 16;
|
|
21
|
+
options.keylen ||= 32;
|
|
22
|
+
return {
|
|
23
|
+
name: id,
|
|
24
|
+
async hash(password: string, opts): Promise<string> {
|
|
25
|
+
const salt = opts?.salt || randomBytes(options.saltlen!);
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let N = options.cost!;
|
|
28
|
+
let r = options.blocksize!;
|
|
29
|
+
let p = options.parallelism!;
|
|
30
|
+
scrypt(password, salt, options.keylen!, { N, r, p }, (err, derivedKey) => {
|
|
31
|
+
if (err) return reject(err);
|
|
32
|
+
|
|
33
|
+
resolve(
|
|
34
|
+
PHC.serialize({
|
|
35
|
+
id: opts?.id ?? id,
|
|
36
|
+
params: {
|
|
37
|
+
ln: N!,
|
|
38
|
+
r: r!,
|
|
39
|
+
p: p!,
|
|
40
|
+
},
|
|
41
|
+
salt,
|
|
42
|
+
hash: derivedKey,
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async verify(password: string, hash: string, opts): Promise<boolean> {
|
|
50
|
+
try {
|
|
51
|
+
const salt = Errors.BadRequest.require(opts.salt);
|
|
52
|
+
const storedHash = Errors.BadRequest.require(opts.hash);
|
|
53
|
+
|
|
54
|
+
const N = parseInt(opts.params?.ln as string, 10);
|
|
55
|
+
const r = parseInt(opts.params?.r as string, 10);
|
|
56
|
+
const p = parseInt(opts.params?.p as string, 10);
|
|
57
|
+
const keylen = storedHash.length;
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
scrypt(password, salt, keylen, { N, r, p }, (err, derivedKey) => {
|
|
61
|
+
if (err) return reject(err);
|
|
62
|
+
|
|
63
|
+
const isMatch = timingSafeEqual(derivedKey, storedHash);
|
|
64
|
+
resolve(isMatch);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return Promise.resolve(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { PHC } from './PHC';
|
|
2
|
+
export { Password } from './Password';
|
|
3
|
+
export { createArgon2PasswordAlgorithm } from './createArgon2PasswordAlgorithm';
|
|
4
|
+
export { createBase64PasswordAlgorithm } from './createBase64PasswordAlgorithm';
|
|
5
|
+
export { createBcryptPasswordAlgorithm } from './createBcryptPasswordAlgorithm';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createScryptPasswordAlgorithm } from '../createScryptPasswordAlgorithm';
|