Files
BetterDesk/web-nodejs/lib/deviceTokenAuth.js
T
UNITRONIX 648a8d4f8a security(3.4): harden console and Go server for pre-release audit
Commit web-nodejs lockfile with tar override; CI uses npm ci and moderate npm audit.
Add LOG_LEVEL filtering and log redaction in Node console and Go server.
Validate WS tokens on bd-signal; require single-use token for remote-agent relay.
Limit active relay sessions per IP and block open enrollment without TLS.
2026-07-12 20:44:46 +02:00

103 lines
3.2 KiB
JavaScript

'use strict';
/**
* Verify device WebSocket credentials (bd-signal, remote-agent).
* Accepts enrollment device_token (Go device_tokens table) or panel access tokens.
*/
const crypto = require('crypto');
const config = require('../config/config');
const { hashAccessToken } = require('./tokenHash');
function hashDeviceToken(token) {
return crypto.createHash('sha256').update(String(token), 'utf8').digest('hex');
}
function validateEnrollmentRow(row, deviceId) {
if (!row) return false;
if (row.status === 'revoked' || row.status === 'expired') return false;
if (row.max_uses > 0 && row.use_count >= row.max_uses) return false;
if (row.expires_at) {
const exp = new Date(row.expires_at);
if (!Number.isNaN(exp.getTime()) && exp < new Date()) return false;
}
if (row.peer_id && row.peer_id !== deviceId) return false;
return true;
}
async function lookupEnrollmentTokenSqlite(tokenHash) {
const Database = require('better-sqlite3');
const db = new Database(config.dbPath, { readonly: true, fileMustExist: false });
try {
return db.prepare(`
SELECT peer_id, status, max_uses, use_count, expires_at
FROM device_tokens WHERE token_hash = ?
`).get(tokenHash);
} catch (err) {
if (String(err.message || '').includes('no such table')) return null;
throw err;
} finally {
db.close();
}
}
async function lookupEnrollmentTokenPostgres(tokenHash) {
const { Client } = require('pg');
const client = new Client({ connectionString: config.databaseUrl });
await client.connect();
try {
const res = await client.query(
`SELECT peer_id, status, max_uses, use_count, expires_at
FROM device_tokens WHERE token_hash = $1`,
[tokenHash]
);
return res.rows[0] || null;
} finally {
await client.end();
}
}
async function verifyEnrollmentToken(deviceId, plainToken) {
const tokenHash = hashDeviceToken(plainToken);
const row = config.dbType === 'postgres'
? await lookupEnrollmentTokenPostgres(tokenHash)
: await lookupEnrollmentTokenSqlite(tokenHash);
return validateEnrollmentRow(row, deviceId);
}
/**
* @param {string} deviceId
* @param {string} plainToken
* @param {{ getAccessToken?: function }} [db] optional database facade for access-token fallback
*/
async function verifyDeviceWsAuth(deviceId, plainToken, db) {
if (!deviceId || !plainToken || String(plainToken).length < 8) {
return false;
}
if (await verifyEnrollmentToken(deviceId, plainToken)) {
return true;
}
if (db && typeof db.getAccessToken === 'function') {
try {
const row = await db.getAccessToken(plainToken);
if (row && row.client_id === deviceId) {
return true;
}
const hashed = hashAccessToken(plainToken);
const rowHash = await db.getAccessToken(hashed);
if (rowHash && rowHash.client_id === deviceId) {
return true;
}
} catch (_) {
/* ignore */
}
}
return false;
}
module.exports = {
hashDeviceToken,
verifyDeviceWsAuth,
verifyEnrollmentToken,
};