fix: replace node.crypto
- commit
- 5d83be3decbb952ce4460caa541a23d872118232
- author
- Kit · · 2025-02-13 07:37 UTC
- parent
- 6fec6e4
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
1 file · +72 -21
M src/lib/utils/password.ts +72 -21
| @@ -1,32 +1,83 @@ | |||
| 1 | - | import crypto from 'crypto'; | |
| 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 | + | */ | |
| 6 | + | function 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 | + | */ | |
| 15 | + | function 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 | + | */ | |
| 32 | + | async 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 | + | } | |
| 2 | 56 | ||
| 3 | 57 | /** | |
| 4 | 58 | * Securely hashes a password using PBKDF2 with a random salt. | |
| 5 | - | * @param password The plain text password to hash | |
| 6 | - | * @returns A promise that resolves to the hashed password in the format 'salt:hash' | |
| 59 | + | * @param password The plain text password to hash. | |
| 60 | + | * @returns A promise that resolves to the hashed password in the format 'salt:hash'. | |
| 7 | 61 | */ | |
| 8 | 62 | export async function hashPassword(password: string): Promise<string> { | |
| 9 | - | return new Promise((resolve, reject) => { | |
| 10 | - | const salt = crypto.randomBytes(16).toString('hex'); | |
| 11 | - | crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => { | |
| 12 | - | if (err) return reject(err); | |
| 13 | - | resolve(`${salt}:${derivedKey.toString('hex')}`); | |
| 14 | - | }); | |
| 15 | - | }); | |
| 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}`; | |
| 16 | 68 | } | |
| 17 | 69 | ||
| 18 | 70 | /** | |
| 19 | - | * Verify a password hash created by hashPassword() | |
| 20 | - | * @param password The plain text password to verify | |
| 21 | - | * @param hash The hashed password to verify against | |
| 22 | - | * @returns A promise that resolves to true if the password matches, false otherwise | |
| 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. | |
| 23 | 75 | */ | |
| 24 | 76 | export async function verifyPassword(password: string, hash: string): Promise<boolean> { | |
| 25 | - | return new Promise((resolve, reject) => { | |
| 26 | - | const [salt, key] = hash.split(':'); | |
| 27 | - | crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => { | |
| 28 | - | if (err) return reject(err); | |
| 29 | - | resolve(key === derivedKey.toString('hex')); | |
| 30 | - | }); | |
| 31 | - | }); | |
| 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; | |
| 32 | 83 | } ⏎̸ | |
| @@ -1,32 +1,83 @@ | |||||
| 1 | - | import crypto from 'crypto'; | 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 | + | */ | |||
| 6 | + | function 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 | + | */ | |||
| 15 | + | function 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 | + | */ | |||
| 32 | + | async 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 | + | } | |||
| 2 | 56 | ||||
| 3 | /** | 57 | /** | ||
| 4 | * Securely hashes a password using PBKDF2 with a random salt. | 58 | * Securely hashes a password using PBKDF2 with a random salt. | ||
| 5 | - | * @param password The plain text password to hash | 59 | + | * @param password The plain text password to hash. |
| 6 | - | * @returns A promise that resolves to the hashed password in the format 'salt:hash' | 60 | + | * @returns A promise that resolves to the hashed password in the format 'salt:hash'. |
| 7 | */ | 61 | */ | ||
| 8 | export async function hashPassword(password: string): Promise<string> { | 62 | export async function hashPassword(password: string): Promise<string> { | ||
| 9 | - | return new Promise((resolve, reject) => { | 63 | + | const salt = crypto.getRandomValues(new Uint8Array(16)); |
| 10 | - | const salt = crypto.randomBytes(16).toString('hex'); | 64 | + | console.debug("$state: Generated salt", salt); |
| 11 | - | crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => { | 65 | + | const saltHex = bufferToHex(salt.buffer); |
| 12 | - | if (err) return reject(err); | 66 | + | const hash = await derivePasswordHash(password, salt); |
| 13 | - | resolve(`${salt}:${derivedKey.toString('hex')}`); | 67 | + | return `${saltHex}:${hash}`; |
| 14 | - | }); | |||
| 15 | - | }); | |||
| 16 | } | 68 | } | ||
| 17 | 69 | ||||
| 18 | /** | 70 | /** | ||
| 19 | - | * Verify a password hash created by hashPassword() | 71 | + | * Verify a password hash created by hashPassword(). |
| 20 | - | * @param password The plain text password to verify | 72 | + | * @param password The plain text password to verify. |
| 21 | - | * @param hash The hashed password to verify against | 73 | + | * @param hash The hashed password to verify against in format 'salt:hash'. |
| 22 | - | * @returns A promise that resolves to true if the password matches, false otherwise | 74 | + | * @returns A promise that resolves to true if the password matches, false otherwise. |
| 23 | */ | 75 | */ | ||
| 24 | export async function verifyPassword(password: string, hash: string): Promise<boolean> { | 76 | export async function verifyPassword(password: string, hash: string): Promise<boolean> { | ||
| 25 | - | return new Promise((resolve, reject) => { | 77 | + | const [saltHex, hashValue] = hash.split(':'); |
| 26 | - | const [salt, key] = hash.split(':'); | 78 | + | const salt = hexToBuffer(saltHex); |
| 27 | - | crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, derivedKey) => { | 79 | + | console.debug("$state: Salt extracted from stored hash", salt); |
| 28 | - | if (err) return reject(err); | 80 | + | const derivedHash = await derivePasswordHash(password, salt); |
| 29 | - | resolve(key === derivedKey.toString('hex')); | 81 | + | console.debug("$state: Derived hash for verification", derivedHash); |
| 30 | - | }); | 82 | + | return derivedHash === hashValue; |
| 31 | - | }); | |||
| 32 | } ⏎̸ | 83 | } ⏎̸ | ||