password.ts RawHistoryFolder
1/* Removed Node's crypto import. Using Web Crypto API instead */
2
3/**
4 * Helper function that converts an ArrayBuffer to a hex string.
5 */
6function bufferToHex(buffer: ArrayBuffer): string {
7 return Array.from(new Uint8Array(buffer))
8 .map((b) => b.toString(16).padStart(2, '0'))
9 .join('');
10}
11
12/**
13 * Helper function that converts a hex string to a Uint8Array.
14 */
15function hexToBuffer(hex: string): Uint8Array {
16 if (hex.length % 2 !== 0) {
17 throw new Error('Invalid hex string');
18 }
19 const arr = new Uint8Array(hex.length / 2);
20 for (let i = 0; i < hex.length; i += 2) {
21 arr[i / 2] = parseInt(hex.substring(i, i + 2), 16);
22 }
23 return arr;
24}
25
26/**
27 * Performs a constant-time string comparison to prevent timing attacks.
28 * Returns true if strings are equal, false otherwise.
29 */
30function constantTimeCompare(a: string, b: string): boolean {
31 if (a.length !== b.length) {
32 return false;
33 }
34 let result = 0;
35 for (let i = 0; i < a.length; i++) {
36 result |= a.charCodeAt(i) ^ b.charCodeAt(i);
37 }
38 return result === 0;
39}
40
41/**
42 * Derives a password hash using PBKDF2 with the provided salt.
43 * @param password The plain text password.
44 * @param salt Uint8Array salt.
45 * @returns The derived key in hex format.
46 */
47async function derivePasswordHash(password: string, salt: Uint8Array): Promise<string> {
48 const enc = new TextEncoder();
49 const keyMaterial = enc.encode(password);
50 const key = await crypto.subtle.importKey('raw', keyMaterial, { name: 'PBKDF2' }, false, [
51 'deriveBits'
52 ]);
53 const derivedBits = await crypto.subtle.deriveBits(
54 {
55 name: 'PBKDF2',
56 salt: salt.buffer as ArrayBuffer,
57 iterations: 100000,
58 hash: 'SHA-512'
59 },
60 key,
61 512
62 );
63 return bufferToHex(derivedBits);
64}
65
66/**
67 * Securely hashes a password using PBKDF2 with a random salt.
68 * @param password The plain text password to hash.
69 * @returns A promise that resolves to the hashed password in the format 'salt:hash'.
70 */
71export async function hashPassword(password: string): Promise<string> {
72 const salt = crypto.getRandomValues(new Uint8Array(16));
73 const saltHex = bufferToHex(salt.buffer);
74 const hash = await derivePasswordHash(password, salt);
75 return `${saltHex}:${hash}`;
76}
77
78/**
79 * Verify a password hash created by hashPassword().
80 * @param password The plain text password to verify.
81 * @param hash The hashed password to verify against in format 'salt:hash'.
82 * @returns A promise that resolves to true if the password matches, false otherwise.
83 */
84export async function verifyPassword(password: string, hash: string): Promise<boolean> {
85 const [saltHex, hashValue] = hash.split(':');
86 const salt = hexToBuffer(saltHex);
87 const derivedHash = await derivePasswordHash(password, salt);
88 return constantTimeCompare(derivedHash, hashValue);
89}