143 lines
3.4 KiB
TypeScript
143 lines
3.4 KiB
TypeScript
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<string> {
|
|
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<boolean> {
|
|
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<Hash> {
|
|
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<CryptoKey> {
|
|
return crypto.subtle.importKey(
|
|
'raw',
|
|
new TextEncoder().encode(password),
|
|
'PBKDF2',
|
|
false,
|
|
['deriveBits']
|
|
)
|
|
}
|