141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
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<OSDefinedHashPrefix, NodeCryptoAlgs> = {
|
|
$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, OSDefinedHashPrefix> = {
|
|
[NodeCryptoAlgs.md5]: '$1$',
|
|
[NodeCryptoAlgs.bcrypt]: '$2$',
|
|
[NodeCryptoAlgs.sha256]: '$5$',
|
|
[NodeCryptoAlgs.sha512]: '$6$',
|
|
}
|
|
|
|
const saltStringLengthDictionary: Record<NodeCryptoAlgs, number> = {
|
|
[NodeCryptoAlgs.md5]: 23,
|
|
[NodeCryptoAlgs.bcrypt]: 23,
|
|
[NodeCryptoAlgs.sha256]: 23,
|
|
[NodeCryptoAlgs.sha512]: 23,
|
|
}
|
|
|
|
interface HashingProvider {
|
|
hash: (plainText: string, saltRounds: number) => Promise<string>;
|
|
compare: (plainText: string, hash: string) => Promise<boolean>;
|
|
}
|
|
|
|
const algImplementationMap: Record<NodeCryptoAlgs, HashingProvider> = {
|
|
[NodeCryptoAlgs.md5]: {
|
|
hash: (plainText: string, saltRounds: number): Promise<string> => { throw new Error('Not implemented') },
|
|
compare: (plainText: string, hash: string): Promise<boolean> => { throw new Error('Not implemented') },
|
|
},
|
|
[NodeCryptoAlgs.bcrypt]: bcrypt,
|
|
[NodeCryptoAlgs.sha256]: {
|
|
hash: (plainText: string, saltRounds: number): Promise<string> => { throw new Error('Not implemented') },
|
|
compare: (plainText: string, hash: string): Promise<boolean> => { throw new Error('Not implemented') },
|
|
},
|
|
[NodeCryptoAlgs.sha512]: {
|
|
hash: (plainText: string, saltRounds: number): Promise<string> => { throw new Error('Not implemented') },
|
|
compare: (plainText: string, hash: string): Promise<boolean> => { throw new Error('Not implemented') },
|
|
},
|
|
}
|
|
|
|
export enum JWA {
|
|
HSA256 = 'HSA256',
|
|
}
|
|
|
|
type SigningFunction = (rawString: string, signingComponent: string) => string;
|
|
|
|
const jwaImplementationMap: Record<JWA, SigningFunction> = {
|
|
[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<string> {
|
|
const alg = NodeCryptoAlgs.bcrypt;
|
|
const saltRounds = this.defaultWorkFactor;
|
|
return algImplementationMap[alg].hash(rawString, saltRounds);
|
|
}
|
|
|
|
static async compare(rawString: string, hash: string): Promise<boolean> {
|
|
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('$');
|
|
}
|
|
}
|