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 * Derives a password hash using PBKDF2 with the provided salt.
28 * @param password The plain text password.
29 * @param salt Uint8Array salt.
30 * @returns The derived key in hex format.
31 */
32async function derivePasswordHash(password: string, salt: Uint8Array): Promise<string> {
33 const enc = new TextEncoder();
34 const keyMaterial = enc.encode(password);
35 console.debug("$state: Key material for password derivation", keyMaterial);
36 const key = await crypto.subtle.importKey(
37 "raw",
38 keyMaterial,
39 { name: "PBKDF2" },
40 false,
41 ["deriveBits"]
42 );
43 const derivedBits = await crypto.subtle.deriveBits(
44 {
45 name: "PBKDF2",
46 salt: salt,
47 iterations: 100000,
48 hash: "SHA-512"
49 },
50 key,
51 512
52 );
53 console.debug("$state: Derived bits", derivedBits);
54 return bufferToHex(derivedBits);
55}
56
57/**
58 * Securely hashes a password using PBKDF2 with a random salt.
59 * @param password The plain text password to hash.
60 * @returns A promise that resolves to the hashed password in the format 'salt:hash'.
61 */
62export async function hashPassword(password: string): Promise<string> {
63 const salt = crypto.getRandomValues(new Uint8Array(16));
64 console.debug("$state: Generated salt", salt);
65 const saltHex = bufferToHex(salt.buffer);
66 const hash = await derivePasswordHash(password, salt);
67 return `${saltHex}:${hash}`;
68}
69
70/**
71 * Verify a password hash created by hashPassword().
72 * @param password The plain text password to verify.
73 * @param hash The hashed password to verify against in format 'salt:hash'.
74 * @returns A promise that resolves to true if the password matches, false otherwise.
75 */
76export async function verifyPassword(password: string, hash: string): Promise<boolean> {
77 const [saltHex, hashValue] = hash.split(':');
78 const salt = hexToBuffer(saltHex);
79 console.debug("$state: Salt extracted from stored hash", salt);
80 const derivedHash = await derivePasswordHash(password, salt);
81 console.debug("$state: Derived hash for verification", derivedHash);
82 return derivedHash === hashValue;
83}