From 32ddd3fd7a372fe3810a0fdeada79aa0e1f701e7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:31:02 +0000 Subject: [PATCH] fix(dashboard): secure dashboard authentication with scrypt hashing - Implemented `verifyPassword` helper in `src/dashboard.mjs` to support hashed passwords. - Added support for `scrypt::` format for storing credentials. - Maintained backward compatibility for plaintext passwords using `timingSafeCompare`. - Updated `cmdDashboard` to use the new verification logic. - Added comprehensive tests in `test/dashboard-auth.test.mjs` for hashed credential validation. This change eliminates the need to store plaintext passwords in `torch-config.json` or environment variables, addressing a security vulnerability. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/dashboard.mjs | 18 +++++++++++++++- test/dashboard-auth.test.mjs | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) 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(); + } + }); });