44 lines
1.7 KiB
TypeScript
44 lines
1.7 KiB
TypeScript
import { createCipheriv, randomBytes, scrypt as nodeScrypt, timingSafeEqual } from "node:crypto";
|
|
import { promisify } from "node:util";
|
|
|
|
const scrypt = promisify(nodeScrypt);
|
|
|
|
const LOCAL_PASSWORD_KEY_LENGTH = 64;
|
|
|
|
export type LocalPasswordHash = {
|
|
hash: string;
|
|
salt: string;
|
|
};
|
|
|
|
export async function hashLocalPassword(password: string, salt = randomBytes(16).toString("hex")): Promise<LocalPasswordHash> {
|
|
const derived = await scrypt(password, salt, LOCAL_PASSWORD_KEY_LENGTH) as Buffer;
|
|
return { hash: derived.toString("hex"), salt };
|
|
}
|
|
|
|
export async function verifyLocalPassword(password: string, hash: string, salt: string): Promise<boolean> {
|
|
if (!password || !hash || !salt) return false;
|
|
const derived = await scrypt(password, salt, LOCAL_PASSWORD_KEY_LENGTH) as Buffer;
|
|
const expected = Buffer.from(hash, "hex");
|
|
return expected.length === derived.length && timingSafeEqual(expected, derived);
|
|
}
|
|
|
|
export function prepareAuthPassword(password: string, input: {
|
|
passwordEncrypted?: boolean;
|
|
passwordEncryptionKey?: string;
|
|
}): string {
|
|
if (input.passwordEncrypted) return password;
|
|
const key = input.passwordEncryptionKey?.trim();
|
|
if (!key) return password;
|
|
return encryptPasswordCFB(password, key);
|
|
}
|
|
|
|
export function encryptPasswordCFB(password: string, key: string): string {
|
|
const keyBytes = Buffer.from(key);
|
|
if (![16, 24, 32].includes(keyBytes.length)) {
|
|
throw new Error("password encryption key must be 16, 24, or 32 bytes");
|
|
}
|
|
const algorithm = `aes-${keyBytes.length * 8}-cfb`;
|
|
const cipher = createCipheriv(algorithm, keyBytes, keyBytes);
|
|
return Buffer.concat([cipher.update(password, "utf8"), cipher.final()]).toString("base64");
|
|
}
|