Skip to content

Commit e1bc56a

Browse files
feat(tools): add scrypt password hash methods
1 parent 94f3528 commit e1bc56a

3 files changed

Lines changed: 90 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vitrify/tools': minor
3+
---
4+
5+
feat(tools): add scrypt password hash methods

packages/tools/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"./env": {
2424
"types": "./dist/types/env/index.d.ts",
2525
"import": "./dist/env/index.js"
26+
},
27+
"./scrypt": {
28+
"types": "./dist/types/scrypt/index.d.ts",
29+
"import": "./dist/scrypt/index.js"
2630
}
2731
},
2832
"scripts": {

packages/tools/src/scrypt/index.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { scrypt, randomBytes } from 'node:crypto'
2+
import { promisify } from 'node:util'
3+
4+
const RECOMMENDED_SALT_SIZE = 16
5+
const RECOMMENDED_COST = Math.pow(2, 17)
6+
const RECOMMENDED_BLOCK_SIZE = 8
7+
const RECOMMENDED_PARALLELIZATION = 1
8+
9+
export const genSalt = ({ size = RECOMMENDED_SALT_SIZE }) => {
10+
return promisify(randomBytes)(size).then((buf) => buf.toString('hex'))
11+
}
12+
13+
export const hash = (
14+
data: string,
15+
salt: string,
16+
{
17+
cost = RECOMMENDED_COST,
18+
blockSize = RECOMMENDED_BLOCK_SIZE,
19+
parallelization = RECOMMENDED_PARALLELIZATION
20+
}: { cost: number; blockSize: number; parallelization: number }
21+
): Promise<string> => {
22+
return new Promise((resolve, reject) => {
23+
scrypt(
24+
data,
25+
salt,
26+
64,
27+
{
28+
cost: cost,
29+
blockSize: blockSize,
30+
parallelization: parallelization,
31+
maxmem: 256 * cost * blockSize
32+
},
33+
(err, derivedKey) => {
34+
if (err) {
35+
reject(err)
36+
} else {
37+
resolve(derivedKey.toString('base64'))
38+
}
39+
}
40+
)
41+
})
42+
}
43+
44+
export const hashPassword = async (
45+
data: string,
46+
options = {
47+
salt: { size: RECOMMENDED_SALT_SIZE },
48+
hash: {
49+
cost: RECOMMENDED_COST,
50+
blockSize: RECOMMENDED_BLOCK_SIZE,
51+
parallelization: RECOMMENDED_PARALLELIZATION
52+
}
53+
}
54+
) => {
55+
const salt = await genSalt(options.salt)
56+
const hashedData = await hash(data, salt, options.hash)
57+
return [
58+
'scrypt',
59+
salt,
60+
options.hash.cost,
61+
options.hash.blockSize,
62+
options.hash.parallelization,
63+
hashedData
64+
].join('$')
65+
}
66+
67+
export const compare = async (data: string, hashed: string) => {
68+
const [algorithm, salt, cost, blockSize, parallelization, hashedData] =
69+
hashed.split('$')
70+
71+
if (algorithm !== 'scrypt') throw new Error('Scrypt was not used for hash')
72+
73+
const hashedDataToCompare = await hash(data, salt, {
74+
cost: Number(cost),
75+
blockSize: Number(blockSize),
76+
parallelization: Number(parallelization)
77+
})
78+
79+
if (hashedDataToCompare === hashedData) return true
80+
return false
81+
}

0 commit comments

Comments
 (0)