import logger from 'log' import * as misc from 'misc' const log = logger('crypto-server:pwd') type Salt = Uint8Array type Hash = ArrayBuffer type Iterations = number export enum Strength { Easy = 100_000, Medium = 300_000, Hard = 750_000, } export async function hash(password: string, strength: Strength = Strength.Medium) : Promise { log.debug('hash password') const iterations = strength as number const salt = crypto.getRandomValues(new Uint8Array(16)) const h = await internal_hash(password, iterations, salt) return encode(salt, iterations, h) } export async function verify(password: string, hash: string) : Promise { log.debug("verify password's hash") const params = decode(hash) if (params === null) { log.warn('Invalid hash') return false } const h = await internal_hash(password, params[1], params[0]) return compare(h, params[2]) } export function encode(salt: Salt, iterations: Iterations, hash: Hash) : string { const s = salt_encode(salt) const i = iterations_encode(iterations) const h = hash_encode(hash) // version.iterations$salt.hash return `1.${i}$${s}.${h}` } export function decode(hash: string) : [Salt, Iterations, Hash] | null { // TODO : Log const parts = hash.split('$') if (parts.length !== 2) return null const p1 = parts[0].split('.') if (p1.length !== 2) return null const p2 = parts[1].split('.') if (p2.length !== 2) return null // Version if (p1[0] !== '1') return null const iterations = iterations_decode(p1[1]) if (iterations === null) return null const salt = salt_decode(p2[0]) if (salt === null) return null const h = hash_decode(p2[1]) if (h === null) return null return [salt, iterations, h] } export function iterations_encode(iterations: Iterations) : string { return iterations.toString(16) } export function iterations_decode(iterations: string) : Iterations | null { try { return parseInt(iterations, 16) } catch(e) { log.warn('Invalid iterations count') log.debug('Error', e) return null } } export function salt_encode(salt: Salt) : string { return misc.a2b64(salt) } export function salt_decode(salt: string) : Salt | null { const res = misc.b642a(salt) if (res.is_err()) { log.warn('Invalid salt') return null } else { return res.unwrap() } } export function hash_encode(hash: Hash) : string { return misc.a2b64(new Uint8Array(hash)) } export function hash_decode(hash: string) : Hash | null { const res = misc.b642a(hash) if (res.is_err()) { log.warn('Invalid hash') return null } else { return res.unwrap().buffer as ArrayBuffer } } export async function internal_hash(password: string, iterations: Iterations, salt: Salt): Promise { const pwd = await encode_password(password) return crypto.subtle.deriveBits( { name: 'PBKDF2', hash: 'SHA-512', iterations, salt, }, pwd, 256 ) } export function compare(a: Hash, b: Hash) : boolean { const a1 = new Uint8Array(a) const b1 = new Uint8Array(b) let result = a1.length === b1.length for (let i = 0;i < Math.max(a1.length, b1.length); ++i) { const a2 = a1[i] ?? 'a' const b2 = b1[i] ?? 'b' const test = a2 === b2 result = result && test } return result } export async function encode_password(password: string) : Promise { return crypto.subtle.importKey( 'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits'] ) }