import * as bcrypt from 'bcrypt'; import * as crypto from 'node:crypto'; // https://passlib.readthedocs.io/en/stable/modular_crypt_format.html#mcf-identifiers type OSDefinedHashPrefix = | '$1$' | '$2$' | '$2a$' | '$2x$' | '$2y$' | '$2b$' | '$5$' | '$6$' ; enum NodeCryptoAlgs { md5 = 'md5', bcrypt = 'bcrypt', sha256 = 'sha256', sha512 = 'sha512', } const hashDictionary: Record = { $1$: NodeCryptoAlgs.md5, $2$: NodeCryptoAlgs.bcrypt, $2a$: NodeCryptoAlgs.bcrypt, $2x$: NodeCryptoAlgs.bcrypt, $2y$: NodeCryptoAlgs.bcrypt, $2b$: NodeCryptoAlgs.bcrypt, $5$: NodeCryptoAlgs.sha256, $6$: NodeCryptoAlgs.sha512, } const inversedHashDictionary: Record = { [NodeCryptoAlgs.md5]: '$1$', [NodeCryptoAlgs.bcrypt]: '$2$', [NodeCryptoAlgs.sha256]: '$5$', [NodeCryptoAlgs.sha512]: '$6$', } const saltStringLengthDictionary: Record = { [NodeCryptoAlgs.md5]: 23, [NodeCryptoAlgs.bcrypt]: 23, [NodeCryptoAlgs.sha256]: 23, [NodeCryptoAlgs.sha512]: 23, } interface HashingProvider { hash: (plainText: string, saltRounds: number) => Promise; compare: (plainText: string, hash: string) => Promise; } const algImplementationMap: Record = { [NodeCryptoAlgs.md5]: { hash: (plainText: string, saltRounds: number): Promise => { throw new Error('Not implemented') }, compare: (plainText: string, hash: string): Promise => { throw new Error('Not implemented') }, }, [NodeCryptoAlgs.bcrypt]: bcrypt, [NodeCryptoAlgs.sha256]: { hash: (plainText: string, saltRounds: number): Promise => { throw new Error('Not implemented') }, compare: (plainText: string, hash: string): Promise => { throw new Error('Not implemented') }, }, [NodeCryptoAlgs.sha512]: { hash: (plainText: string, saltRounds: number): Promise => { throw new Error('Not implemented') }, compare: (plainText: string, hash: string): Promise => { throw new Error('Not implemented') }, }, } export enum JWA { HSA256 = 'HSA256', } type SigningFunction = (rawString: string, signingComponent: string) => string; const jwaImplementationMap: Record = { [JWA.HSA256]: function (rawString: string, signingComponent: string) { return crypto.createHmac('sha-256', signingComponent).update(rawString).digest('base64url'); }, } export class SecureStringUtil { private static readonly defaultWorkFactor = 15; static async generateNewHash(rawString: string): Promise { const alg = NodeCryptoAlgs.bcrypt; const saltRounds = this.defaultWorkFactor; return algImplementationMap[alg].hash(rawString, saltRounds); } static async compare(rawString: string, hash: string): Promise { const { alg } = HashMeta.deserialize(hash); return algImplementationMap[alg].compare(rawString, hash); } } export class TokenSignatureUtil { static generateSignature(rawString: string, alg: JWA, signingComponent: string): string { return jwaImplementationMap[alg](rawString, signingComponent); } } class HashMeta { constructor( public readonly alg: NodeCryptoAlgs, public readonly workFactor: number, public readonly salt: string, public readonly hash: string, ) {} static deserialize(hashMeta: string): HashMeta { const parts = hashMeta.split('$'); if (parts.length !== 4) { throw new Error(); } const [_, alg, workFactor, salthash] = parts as [string, string, string, string]; const wrappedAlg = `$${alg}$` as OSDefinedHashPrefix; const nodeCryptoAlg = hashDictionary[wrappedAlg]; const saltStringLength = saltStringLengthDictionary[nodeCryptoAlg]; const salt = salthash.substring(0, saltStringLength); const hash = salthash.substring(saltStringLength); return new HashMeta( nodeCryptoAlg, +workFactor, salt, hash ); } serialize(): string { return [ '', inversedHashDictionary[this.alg], this.workFactor, `${this.salt}${this.hash}` ].join('$'); } }