diff --git a/src/dashboard.mjs b/src/dashboard.mjs index 1e939cf..a3f38e3 100644 --- a/src/dashboard.mjs +++ b/src/dashboard.mjs @@ -31,6 +31,22 @@ function timingSafeCompare(a, b) { return crypto.timingSafeEqual(bufA, bufB); } +function verifyPassword(input, stored) { + if (stored.startsWith('scrypt:')) { + const parts = stored.split(':'); + if (parts.length !== 3) return false; + try { + const salt = Buffer.from(parts[1], 'hex'); + const hash = Buffer.from(parts[2], 'hex'); + const derived = crypto.scryptSync(input, salt, 64); + return crypto.timingSafeEqual(derived, hash); + } catch { + return false; + } + } + return timingSafeCompare(input, stored); +} + export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.0.1') { // Resolve package root relative to this file (src/dashboard.mjs) // this file is in /src/dashboard.mjs, so '..' goes to @@ -54,7 +70,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0. if (type === 'Basic' && credentials) { try { const decoded = Buffer.from(credentials, 'base64').toString(); - isValid = timingSafeCompare(decoded, auth); + isValid = verifyPassword(decoded, auth); } catch { isValid = false; } diff --git a/test/dashboard-auth.test.mjs b/test/dashboard-auth.test.mjs index 7cedac0..54e6910 100644 --- a/test/dashboard-auth.test.mjs +++ b/test/dashboard-auth.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert'; import http from 'node:http'; +import crypto from 'node:crypto'; import { cmdDashboard } from '../src/dashboard.mjs'; import { _resetTorchConfigCache } from '../src/torch-config.mjs'; @@ -147,4 +148,43 @@ test('Dashboard Authentication', async (t) => { _resetTorchConfigCache(); } }); + + await t.test('validates correctly using scrypt hashed credentials', async () => { + // Generate a valid scrypt hash + const password = 'admin:hashedpassword'; + const salt = crypto.randomBytes(16); + const hash = crypto.scryptSync(password, salt, 64); + const storedAuth = `scrypt:${salt.toString('hex')}:${hash.toString('hex')}`; + + process.env.TORCH_DASHBOARD_AUTH = storedAuth; + _resetTorchConfigCache(); + + server = await cmdDashboard(0); + testPort = server.address().port; + + try { + const makeRequest = (credentials) => { + const auth = Buffer.from(credentials).toString('base64'); + return new Promise((resolve) => { + http.get({ + host: '127.0.0.1', + port: testPort, + path: '/dashboard/', + headers: { 'Authorization': `Basic ${auth}` } + }, resolve); + }); + }; + + // Correct password + const resCorrect = await makeRequest(password); + assert.notStrictEqual(resCorrect.statusCode, 401, 'correct hashed credentials must not be rejected'); + + // Incorrect password + const resIncorrect = await makeRequest('admin:wrong'); + assert.strictEqual(resIncorrect.statusCode, 401, 'incorrect hashed credentials must be rejected'); + } finally { + delete process.env.TORCH_DASHBOARD_AUTH; + _resetTorchConfigCache(); + } + }); });