Created
September 15, 2024 06:29
-
-
Save neilzheng/942f0dfef8eb105fd632764994d35da6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { pbkdf2, randomBytes } from 'crypto'; | |
import { promisify } from 'util'; | |
export interface PBKDF2Options { | |
digest: string; | |
iterations: number; | |
keylen?: number; | |
} | |
export class PBKDF2 { | |
private static hashName = 'pbkdf2'; | |
private static genSalt = promisify(randomBytes); | |
private static genHash = promisify(pbkdf2); | |
private static hashSize = (digest: string) => { | |
switch (digest) { | |
case 'sm3': | |
case 'sha256': | |
return 32; | |
case 'sha512': | |
return 64; | |
} | |
// not supported | |
return 0; | |
}; | |
private static stringfy = ( | |
options: PBKDF2Options, | |
salt: Buffer, | |
hash: Buffer, | |
): string => { | |
const optionArray: string[] = []; | |
for (const keyVal of Object.entries(options)) { | |
optionArray.push(keyVal.join('=')); | |
} | |
return `\$${this.hashName}\$${optionArray.join(',')}\$${salt.toString('base64url')}\$${hash.toString('base64url')}`; | |
}; | |
// $pbkdf2$digest=sm3,iterations=100000,keylen=32$salt$hash | |
static async hash(password: string, options: PBKDF2Options): Promise<string> { | |
const iterations = options.iterations; | |
const digest = options.digest; | |
const digestSize = this.hashSize(digest); | |
if (!options.keylen) options.keylen = digestSize; | |
const keylen = options.keylen; | |
const salt = await this.genSalt(digestSize); | |
const hash = await this.genHash( | |
Buffer.from(password, 'utf-8'), | |
salt, | |
iterations, | |
keylen, | |
digest, | |
); | |
return this.stringfy(options, salt, hash); | |
} | |
static async verify(password: string, hash: string): Promise<boolean> { | |
if (!password || !hash) return false; | |
const hashArr = hash.split('$'); | |
if (hashArr[1] !== this.hashName) return false; | |
const optionStr = hashArr[2]; | |
const optionsObj = Object.fromEntries( | |
optionStr.split(',').map((item) => item.split('=')), | |
); | |
if (!optionsObj.digest || !optionsObj.iterations || !optionsObj.keylen) | |
return false; | |
const { iterations, keylen, ...options } = optionsObj; | |
options.iterations = parseInt(iterations); | |
options.keylen = parseInt(keylen); | |
const salt = Buffer.from(hashArr[3], 'base64url'); | |
const storedHash = Buffer.from(hashArr[4], 'base64url'); | |
const passHash = await this.genHash( | |
Buffer.from(password, 'utf-8'), | |
salt, | |
options.iterations, | |
options.keylen, | |
options.digest, | |
); | |
return storedHash.compare(passHash) == 0; | |
} | |
} | |
/* | |
// simple test | |
async function main() { | |
const options: PBKDF2Options = { | |
iterations: 100000, | |
digest: 'sm3', | |
}; | |
const pass = 'password'; | |
const hash = await PBKDF2.hash(pass, options); | |
console.log(hash); | |
console.log(await PBKDF2.verify(pass, hash)); | |
} | |
main().catch((e) => console.error(e)); | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment