fix(dashboard): secure dashboard authentication with scrypt hashing

- Implemented `verifyPassword` helper in `src/dashboard.mjs` to support hashed passwords.
- Added support for `scrypt:<salt>:<hash>` 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 <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-21 18:31:02 +00:00
1 parent 79e261d7b2
commit 32ddd3fd7a
2 files changed
+57 -1

No files matched your search

+17 -1
View File
@@ -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 <root>/src/dashboard.mjs, so '..' goes to <root>
@@ -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;
}
+40
View File
@@ -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();
}
});
});