Skip to content

Instantly share code, notes, and snippets.

@neilzheng
Created September 15, 2024 06:29
Show Gist options
  • Save neilzheng/942f0dfef8eb105fd632764994d35da6 to your computer and use it in GitHub Desktop.
Save neilzheng/942f0dfef8eb105fd632764994d35da6 to your computer and use it in GitHub Desktop.
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