53 lines
1.5 KiB
TypeScript
53 lines
1.5 KiB
TypeScript
import argon2 from "argon2";
|
||
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
||
|
||
const ARGON_OPTIONS = {
|
||
type: argon2.argon2id,
|
||
memoryCost: 65_536,
|
||
timeCost: 3,
|
||
parallelism: 1,
|
||
hashLength: 32,
|
||
} as const;
|
||
|
||
export function normalizeUsername(username: string): string {
|
||
return username.normalize("NFKC").trim().toLocaleLowerCase("und");
|
||
}
|
||
|
||
export function validateNewPassword(password: string): string | null {
|
||
const length = [...password].length;
|
||
if (length < 12 || length > 128 || Buffer.byteLength(password, "utf8") > 512) {
|
||
return "密码长度需要为 12–128 个字符";
|
||
}
|
||
return null;
|
||
}
|
||
|
||
export function hashPassword(password: string): Promise<string> {
|
||
return argon2.hash(password, ARGON_OPTIONS);
|
||
}
|
||
|
||
export async function verifyPassword(hash: string, password: string): Promise<boolean> {
|
||
try {
|
||
return await argon2.verify(hash, password);
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export function randomToken(bytes = 32): string {
|
||
return randomBytes(bytes).toString("base64url");
|
||
}
|
||
|
||
export function sha256(value: string | Buffer): string {
|
||
return createHash("sha256").update(value).digest("hex");
|
||
}
|
||
|
||
export function constantTimeEqual(left: string, right: string): boolean {
|
||
const leftBuffer = Buffer.from(left);
|
||
const rightBuffer = Buffer.from(right);
|
||
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
||
}
|
||
|
||
export function temporaryPassword(): string {
|
||
return `${randomToken(15)}A7!`;
|
||
}
|