mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
fix(console): validate deploy-string public key and fall back to Go (#340)
Reject placeholder/invalid id_ed25519.pub values in Dashboard client config, prefer live Go /api/server-key, and set PUB_KEY_PATH in Windows NSSM console env.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Dashboard Copy deploy string with invalid/placeholder public key (#340):** client config / deploy string / QR now require a valid Ed25519 key (base64 → 32 bytes), reject placeholders, and fall back to the live Go `GET /api/server-key` when `id_ed25519.pub` is missing or bad. Windows installer also sets `PUB_KEY_PATH` in the console NSSM environment. Ships via panel update (re-run `betterdesk.ps1` service setup to refresh NSSM env on Windows).
|
||||
|
||||
### Changed
|
||||
- _(none yet)_
|
||||
|
||||
|
||||
@@ -2089,6 +2089,7 @@ function Setup-Services {
|
||||
"KEYS_PATH=$script:RUSTDESK_PATH",
|
||||
"DATA_DIR=$script:CONSOLE_PATH\data",
|
||||
"DB_PATH=$script:RUSTDESK_PATH\db_v2.sqlite3",
|
||||
"PUB_KEY_PATH=$script:RUSTDESK_PATH\id_ed25519.pub",
|
||||
"API_KEY_PATH=$script:RUSTDESK_PATH\.api_key",
|
||||
"HBBS_API_URL=${apiScheme}://localhost:$($script:API_PORT)/api",
|
||||
"BETTERDESK_API_URL=${apiScheme}://localhost:$($script:API_PORT)/api",
|
||||
|
||||
@@ -32,9 +32,9 @@ router.get('/api/stats', requireAuth, async (req, res) => {
|
||||
// Get server health
|
||||
const hbbsHealth = await serverBackend.getHealth();
|
||||
|
||||
// Get public key info
|
||||
const publicKey = keyService.getPublicKey();
|
||||
|
||||
// Get public key info (file or live Go key)
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -148,7 +148,7 @@ router.get('/api/dashboard/client-config', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const queryHost = typeof req.query.host === 'string' ? req.query.host : '';
|
||||
const endpoints = clientConfigHost.resolveRustDeskEndpoints(req, queryHost);
|
||||
const clientConfig = keyService.getClientConfig(endpoints);
|
||||
const clientConfig = await keyService.getClientConfig(endpoints);
|
||||
const qr = await keyService.getServerConfigQR(endpoints);
|
||||
|
||||
res.json({
|
||||
|
||||
@@ -112,8 +112,13 @@ function finalizeBundleBrandingSync(input) {
|
||||
* installation registers on its own and receives a unique device_token
|
||||
* after operator approval (managed enrollment).
|
||||
*/
|
||||
function finalizeBundleBranding(input) {
|
||||
async function finalizeBundleBranding(input) {
|
||||
const branding = finalizeBundleBrandingSync(input);
|
||||
const pubKey = (await keyService.resolvePublicKey()) || '';
|
||||
if (branding.server) {
|
||||
branding.server.public_key = pubKey;
|
||||
}
|
||||
branding.server_key = pubKey;
|
||||
// Strip legacy shared tokens from older bundles on save/rebuild.
|
||||
delete branding.enrollment_token;
|
||||
delete branding.has_enrollment_token;
|
||||
@@ -189,14 +194,14 @@ router.get('/api/generator/bundles/:bundleId', requireAuth, requireAdmin, async
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/generator/defaults', requireAuth, requireAdmin, (req, res) => {
|
||||
router.get('/api/generator/defaults', requireAuth, requireAdmin, async (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
server_host: conn.defaultServerHost(),
|
||||
use_https: conn.defaultUseHttps(),
|
||||
api_port: conn.defaultApiPort(),
|
||||
public_key: keyService.getPublicKey() || '',
|
||||
public_key: (await keyService.resolvePublicKey()) || '',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -231,7 +236,7 @@ router.post('/api/generator/bundles', requireAuth, requireAdmin, async (req, res
|
||||
}
|
||||
const normalized = productType === 'rdclient'
|
||||
? { ...base, bundle_id: bundleId, server_url: base.panel_url }
|
||||
: finalizeBundleBranding(base);
|
||||
: await finalizeBundleBranding(base);
|
||||
if (productType !== 'rdclient') {
|
||||
normalized.bundle_id = bundleId;
|
||||
normalized.product_name = productType === 'agent-client'
|
||||
@@ -271,7 +276,7 @@ router.put('/api/generator/bundles/:bundleId', requireAuth, requireAdmin, async
|
||||
if (!valid) {
|
||||
return res.status(400).json({ success: false, error: req.t('generator.errors.validation_failed'), errors, details: errors });
|
||||
}
|
||||
const normalized = finalizeBundleBranding(base);
|
||||
const normalized = await finalizeBundleBranding(base);
|
||||
normalized.bundle_id = req.params.bundleId;
|
||||
normalized.product_name = normalizeProductType(existing.product_type) === 'agent-client'
|
||||
? (normalized.company_name ? `${normalized.company_name} Agent` : 'BetterDesk Agent')
|
||||
@@ -511,9 +516,9 @@ router.get('/api/d/:publicId/download/:platform/:arch/:format', async (req, res)
|
||||
// Legacy TOML config generator (deprecated, kept for compatibility)
|
||||
// =========================================================================
|
||||
|
||||
router.get('/api/generator/config', requireAuth, (req, res) => {
|
||||
router.get('/api/generator/config', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const publicKey = keyService.getPublicKey();
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -528,13 +533,13 @@ router.get('/api/generator/config', requireAuth, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/api/generator/generate-config', requireAuth, (req, res) => {
|
||||
router.post('/api/generator/generate-config', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { serverHost, serverPort, relayHost, relayPort, clientName } = req.body;
|
||||
if (!serverHost) {
|
||||
return res.status(400).json({ success: false, error: 'Server host is required' });
|
||||
}
|
||||
const publicKey = keyService.getPublicKey();
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
const lines = [];
|
||||
lines.push(`rendezvous_server = ${serverHost}:${serverPort || 21116}`);
|
||||
if (relayHost) lines.push(`relay_server = ${relayHost}:${relayPort || 21117}`);
|
||||
|
||||
@@ -39,17 +39,17 @@ router.get('/keys', requireAuth, (req, res) => {
|
||||
/**
|
||||
* GET /api/keys/public - Get public key
|
||||
*/
|
||||
router.get('/api/keys/public', requireAuth, (req, res) => {
|
||||
router.get('/api/keys/public', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const publicKey = keyService.getPublicKey();
|
||||
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
|
||||
if (!publicKey) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: req.t('keys.not_found')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
@@ -100,17 +100,17 @@ router.get('/api/keys/public/qr', requireAuth, async (req, res) => {
|
||||
/**
|
||||
* GET /api/keys/public/download - Download public key file
|
||||
*/
|
||||
router.get('/api/keys/public/download', requireAuth, (req, res) => {
|
||||
router.get('/api/keys/public/download', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const publicKey = keyService.getPublicKey();
|
||||
|
||||
const publicKey = await keyService.resolvePublicKey();
|
||||
|
||||
if (!publicKey) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: req.t('keys.not_found')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.setHeader('Content-Type', 'text/plain');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="id_ed25519.pub"');
|
||||
res.send(publicKey);
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const db = require('../services/database');
|
||||
const config = require('../config/config');
|
||||
const { requirePermission } = require('../middleware/auth');
|
||||
@@ -57,13 +56,12 @@ function getClientIp(req) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the server public key from disk (base64).
|
||||
* Read the server public key (validated file, then live Go key).
|
||||
*/
|
||||
function getServerPublicKey() {
|
||||
async function getServerPublicKey() {
|
||||
try {
|
||||
if (fs.existsSync(config.pubKeyPath)) {
|
||||
return fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
}
|
||||
const keyService = require('../services/keyService');
|
||||
return (await keyService.resolvePublicKey()) || '';
|
||||
} catch (_) { /* ignore */ }
|
||||
return '';
|
||||
}
|
||||
@@ -78,14 +76,14 @@ function generateDeviceAccessToken() {
|
||||
/**
|
||||
* Build the server config payload returned to devices upon approval.
|
||||
*/
|
||||
function buildServerConfig() {
|
||||
async function buildServerConfig() {
|
||||
const protocol = config.httpsEnabled ? 'https' : 'http';
|
||||
const consoleUrl = `${protocol}://0.0.0.0:${config.port}`;
|
||||
|
||||
return {
|
||||
console_url: consoleUrl,
|
||||
server_address: `0.0.0.0:21116`,
|
||||
server_key: getServerPublicKey(),
|
||||
server_key: await getServerPublicKey(),
|
||||
access_token: generateDeviceAccessToken(),
|
||||
};
|
||||
}
|
||||
@@ -283,7 +281,7 @@ router.put('/api/registrations/:id/approve', requirePermission('enrollment.appro
|
||||
}
|
||||
|
||||
// Build server config — use actual server address from the request
|
||||
const serverConfig = buildServerConfig();
|
||||
const serverConfig = await buildServerConfig();
|
||||
|
||||
// Replace 0.0.0.0 with the actual hostname / IP the admin is accessing
|
||||
const actualHost = req.headers.host?.split(':')[0] || req.hostname || 'localhost';
|
||||
|
||||
@@ -78,13 +78,13 @@ function getRemoteRelay() {
|
||||
try { return require('../services/remoteRelay'); } catch { return null; }
|
||||
}
|
||||
|
||||
// Read server public key on each viewer render (Go may write id_ed25519.pub after
|
||||
// console start; avoid caching empty/stale key across the process lifetime).
|
||||
function getServerPubKey() {
|
||||
// Resolve server public key on each viewer render (file may be stale/wrong;
|
||||
// fall back to live Go /api/server-key — issue #340).
|
||||
async function resolveServerPubKey() {
|
||||
try {
|
||||
return keyService.getPublicKey() || '';
|
||||
return (await keyService.resolvePublicKey()) || '';
|
||||
} catch (err) {
|
||||
console.warn('Warning: Could not read server public key:', err.message);
|
||||
console.warn('Warning: Could not resolve server public key:', err.message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ router.get('/remote/:deviceId', rdClientPageLimiter, requireRemoteAccess, async
|
||||
activePage: 'remote',
|
||||
deviceId: deviceId,
|
||||
device: device || { id: deviceId, hostname: '', platform: '', note: '' },
|
||||
serverPubKey: getServerPubKey(),
|
||||
serverPubKey: await resolveServerPubKey(),
|
||||
capabilities,
|
||||
guestToken: req.guestToken || getGuestTokenFromQuery(req) || '',
|
||||
layout: 'viewer'
|
||||
|
||||
@@ -1596,19 +1596,11 @@ function cleanupTfaSessions(sessions) {
|
||||
* This key is used by clients to verify peer identity (signed_id_pk).
|
||||
* Public key is inherently safe to expose — no auth required.
|
||||
*/
|
||||
router.get('/api/server-key', (req, res) => {
|
||||
router.get('/api/server-key', async (req, res) => {
|
||||
try {
|
||||
if (!fs.existsSync(config.pubKeyPath)) {
|
||||
return res.json({ key: '' });
|
||||
}
|
||||
const key = fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
// Validate: should decode to 32 bytes (Ed25519 public key)
|
||||
const decoded = Buffer.from(key, 'base64');
|
||||
if (decoded.length !== 32) {
|
||||
console.warn('[API:SERVER-KEY] Invalid RS public key length:', decoded.length);
|
||||
return res.json({ key: '' });
|
||||
}
|
||||
return res.json({ key });
|
||||
const keyService = require('../services/keyService');
|
||||
const key = await keyService.resolvePublicKey();
|
||||
return res.json({ key: key || '' });
|
||||
} catch (err) {
|
||||
console.warn('[API:SERVER-KEY] Error reading public key:', err.message);
|
||||
return res.json({ key: '' });
|
||||
@@ -1619,12 +1611,13 @@ router.get('/api/server-key', (req, res) => {
|
||||
* GET /api/server-key/fingerprint
|
||||
* Returns SHA-256 fingerprint of RS public key for out-of-band verification.
|
||||
*/
|
||||
router.get('/api/server-key/fingerprint', (req, res) => {
|
||||
router.get('/api/server-key/fingerprint', async (req, res) => {
|
||||
try {
|
||||
if (!fs.existsSync(config.pubKeyPath)) {
|
||||
const keyService = require('../services/keyService');
|
||||
const key = await keyService.resolvePublicKey();
|
||||
if (!key) {
|
||||
return res.json({ fingerprint: '', algorithm: 'SHA-256' });
|
||||
}
|
||||
const key = fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
const hash = crypto.createHash('sha256').update(Buffer.from(key, 'base64')).digest('hex');
|
||||
return res.json({
|
||||
fingerprint: hash.match(/.{2}/g).join(':').toUpperCase(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* BetterDesk Console - Key Service
|
||||
* Reads public key and API key from filesystem
|
||||
* Reads public key and API key from filesystem; resolves live Go key as fallback.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
@@ -8,21 +8,114 @@ const QRCode = require('qrcode');
|
||||
const config = require('../config/config');
|
||||
const conn = require('./agentBundleConnection');
|
||||
|
||||
const ED25519_PUBLIC_KEY_BYTES = 32;
|
||||
const GO_KEY_CACHE_TTL_MS = 30_000;
|
||||
|
||||
/** @type {{ key: string|null, at: number }} */
|
||||
let goKeyCache = { key: null, at: 0 };
|
||||
|
||||
/**
|
||||
* Read public key from file
|
||||
* True when value is a valid RustDesk server public key (base64 → 32 bytes).
|
||||
* Rejects empty values, unresolved env tokens, and obvious placeholders.
|
||||
* @param {unknown} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidRustDeskPublicKey(value) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const key = value.trim();
|
||||
if (!key) return false;
|
||||
if (/__[^_\s]+__/.test(key)) return false;
|
||||
if (/placeholder/i.test(key)) return false;
|
||||
if (/^YOUR[_-]?PUBLIC[_-]?KEY$/i.test(key)) return false;
|
||||
if (/\s/.test(key)) return false;
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(key, 'base64');
|
||||
// Reject non-canonical base64 (padding / alphabet mismatch)
|
||||
if (decoded.length !== ED25519_PUBLIC_KEY_BYTES) return false;
|
||||
const reencoded = decoded.toString('base64');
|
||||
// Allow missing padding on input by comparing without '='
|
||||
if (reencoded.replace(/=+$/, '') !== key.replace(/=+$/, '')) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and validate public key from the configured pubkey file.
|
||||
* Invalid / placeholder content is treated as missing (never returned to clients).
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getPublicKey() {
|
||||
try {
|
||||
if (fs.existsSync(config.pubKeyPath)) {
|
||||
return fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
if (!fs.existsSync(config.pubKeyPath)) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
const raw = fs.readFileSync(config.pubKeyPath, 'utf8').trim();
|
||||
if (!raw) return null;
|
||||
if (!isValidRustDeskPublicKey(raw)) {
|
||||
console.warn(
|
||||
`Public key at ${config.pubKeyPath} is not a valid Ed25519 key ` +
|
||||
`(length=${raw.length}); ignoring for client deploy/config.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
} catch (err) {
|
||||
console.warn('Could not read public key:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the live rendezvous public key from the Go server.
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function fetchPublicKeyFromGo() {
|
||||
try {
|
||||
const betterdeskApi = require('./betterdeskApi');
|
||||
const resp = await betterdeskApi.apiClient.get('/server-key', { timeout: 5000 });
|
||||
const key = typeof resp.data?.key === 'string' ? resp.data.key.trim() : '';
|
||||
if (isValidRustDeskPublicKey(key)) {
|
||||
return key;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.warn('Could not fetch public key from Go /api/server-key:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the server public key: validated file first, then live Go API (cached).
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
async function resolvePublicKey() {
|
||||
// Prefer module.exports so tests can spy on getPublicKey.
|
||||
const fromFile = module.exports.getPublicKey();
|
||||
if (fromFile) return fromFile;
|
||||
|
||||
const now = Date.now();
|
||||
if (goKeyCache.key && (now - goKeyCache.at) < GO_KEY_CACHE_TTL_MS) {
|
||||
return goKeyCache.key;
|
||||
}
|
||||
|
||||
const fromGo = await fetchPublicKeyFromGo();
|
||||
if (fromGo) {
|
||||
goKeyCache = { key: fromGo, at: now };
|
||||
return fromGo;
|
||||
}
|
||||
|
||||
goKeyCache = { key: null, at: now };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Test helper — clears Go key cache. */
|
||||
function _resetGoKeyCacheForTests() {
|
||||
goKeyCache = { key: null, at: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key (masked for display)
|
||||
*/
|
||||
@@ -65,10 +158,12 @@ function apiUrlForHost(host, useHttps) {
|
||||
/**
|
||||
* RustDesk client config JSON payload: { host, relay, api, key }
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
* @param {{ useHttps?: boolean }} [options]
|
||||
* @param {{ useHttps?: boolean, publicKey?: string }} [options]
|
||||
*/
|
||||
function buildRustDeskConfigPayload(endpointsOrHost, options = {}) {
|
||||
const pubKey = getPublicKey() || '';
|
||||
const pubKey = options.publicKey !== undefined
|
||||
? (isValidRustDeskPublicKey(options.publicKey) ? String(options.publicKey).trim() : '')
|
||||
: (module.exports.getPublicKey() || '');
|
||||
const useHttps = options.useHttps ?? conn.defaultUseHttps();
|
||||
|
||||
if (typeof endpointsOrHost === 'string') {
|
||||
@@ -92,6 +187,18 @@ function buildRustDeskConfigPayload(endpointsOrHost, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Like buildRustDeskConfigPayload but resolves the live public key (file → Go).
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
* @param {{ useHttps?: boolean, publicKey?: string }} [options]
|
||||
*/
|
||||
async function buildRustDeskConfigPayloadAsync(endpointsOrHost, options = {}) {
|
||||
const publicKey = options.publicKey !== undefined
|
||||
? options.publicKey
|
||||
: (await resolvePublicKey()) || '';
|
||||
return buildRustDeskConfigPayload(endpointsOrHost, { ...options, publicKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* QR / deep-link format: rustdesk://config/<standard-base64-json>
|
||||
*/
|
||||
@@ -113,17 +220,17 @@ function encodeRustDeskCliConfigString(payload) {
|
||||
|
||||
/**
|
||||
* Generate QR code containing the RustDesk configuration URI.
|
||||
* Format: rustdesk://config/<base64-encoded-json>
|
||||
* Format: rustdesk://config/<base64-json>
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
*/
|
||||
async function getServerConfigQR(endpointsOrHost) {
|
||||
const pubKey = getPublicKey();
|
||||
const pubKey = await resolvePublicKey();
|
||||
if (!pubKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const configPayload = buildRustDeskConfigPayload(endpointsOrHost);
|
||||
const configPayload = await buildRustDeskConfigPayloadAsync(endpointsOrHost, { publicKey: pubKey });
|
||||
const configUri = encodeRustDeskConfigUri(configPayload);
|
||||
|
||||
const qrDataUrl = await QRCode.toDataURL(configUri, {
|
||||
@@ -145,10 +252,11 @@ async function getServerConfigQR(endpointsOrHost) {
|
||||
|
||||
/**
|
||||
* Build the RustDesk client fields operators need to enter manually.
|
||||
* Uses validated file key, then live Go /api/server-key as fallback (#340).
|
||||
* @param {{ host: string, relay?: string, api?: string } | string} endpointsOrHost
|
||||
*/
|
||||
function getClientConfig(endpointsOrHost) {
|
||||
const payload = buildRustDeskConfigPayload(endpointsOrHost);
|
||||
async function getClientConfig(endpointsOrHost) {
|
||||
const payload = await buildRustDeskConfigPayloadAsync(endpointsOrHost);
|
||||
const publicKey = payload.key;
|
||||
|
||||
return {
|
||||
@@ -166,7 +274,7 @@ function getClientConfig(endpointsOrHost) {
|
||||
* Generate QR code for public key (legacy — raw key text)
|
||||
*/
|
||||
async function getPublicKeyQR() {
|
||||
const pubKey = getPublicKey();
|
||||
const pubKey = await resolvePublicKey();
|
||||
if (!pubKey) {
|
||||
return null;
|
||||
}
|
||||
@@ -192,9 +300,9 @@ async function getPublicKeyQR() {
|
||||
/**
|
||||
* Get server configuration info
|
||||
*/
|
||||
function getServerConfig() {
|
||||
async function getServerConfig() {
|
||||
return {
|
||||
publicKey: getPublicKey(),
|
||||
publicKey: await resolvePublicKey(),
|
||||
apiKeyMasked: getApiKey(true),
|
||||
hbbsApiUrl: config.hbbsApiUrl,
|
||||
dbPath: config.dbPath,
|
||||
@@ -204,15 +312,19 @@ function getServerConfig() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidRustDeskPublicKey,
|
||||
getPublicKey,
|
||||
resolvePublicKey,
|
||||
getApiKey,
|
||||
getPublicKeyQR,
|
||||
getServerConfigQR,
|
||||
getClientConfig,
|
||||
getServerConfig,
|
||||
buildRustDeskConfigPayload,
|
||||
buildRustDeskConfigPayloadAsync,
|
||||
encodeRustDeskConfigUri,
|
||||
encodeRustDeskCliConfigString,
|
||||
normalizeHostInput,
|
||||
apiUrlForHost,
|
||||
_resetGoKeyCacheForTests,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,9 @@ describe('keyService RustDesk config encoding', () => {
|
||||
});
|
||||
|
||||
it('buildRustDeskConfigPayload normalizes host input', () => {
|
||||
const payload = keyService.buildRustDeskConfigPayload('https://desk.example.com:8443/path');
|
||||
const payload = keyService.buildRustDeskConfigPayload('https://desk.example.com:8443/path', {
|
||||
publicKey: '',
|
||||
});
|
||||
expect(payload.host).toBe('desk.example.com');
|
||||
expect(payload.relay).toBe('desk.example.com');
|
||||
});
|
||||
@@ -37,9 +39,84 @@ describe('keyService RustDesk config encoding', () => {
|
||||
host: 'remote.example.com',
|
||||
relay: 'relay.example.com',
|
||||
api: 'https://api.example.com',
|
||||
});
|
||||
}, { publicKey: '' });
|
||||
expect(payload.host).toBe('remote.example.com');
|
||||
expect(payload.relay).toBe('relay.example.com');
|
||||
expect(payload.api).toBe('https://api.example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyService public key validation (#340)', () => {
|
||||
// 32 zero bytes → canonical base64 (44 chars with padding)
|
||||
const validKey = Buffer.alloc(32, 0).toString('base64');
|
||||
|
||||
afterEach(() => {
|
||||
keyService._resetGoKeyCacheForTests();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('isValidRustDeskPublicKey accepts canonical 32-byte base64', () => {
|
||||
expect(keyService.isValidRustDeskPublicKey(validKey)).toBe(true);
|
||||
});
|
||||
|
||||
it('isValidRustDeskPublicKey rejects placeholders and junk', () => {
|
||||
expect(keyService.isValidRustDeskPublicKey('')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('v1.4.9_public_key_placeholder...')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('YOUR_PUBLIC_KEY')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('__PUB_KEY_PATH__')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey('not-base64!!!')).toBe(false);
|
||||
expect(keyService.isValidRustDeskPublicKey(Buffer.alloc(16).toString('base64'))).toBe(false);
|
||||
});
|
||||
|
||||
it('getClientConfig embeds valid public key in deploy string', async () => {
|
||||
const payload = keyService.buildRustDeskConfigPayload('desk.example.com', { publicKey: validKey });
|
||||
const deploy = keyService.encodeRustDeskCliConfigString(payload);
|
||||
const json = Buffer.from(deploy.split('').reverse().join(''), 'base64').toString('utf8');
|
||||
const decoded = JSON.parse(json);
|
||||
expect(decoded.key).toBe(validKey);
|
||||
expect(decoded.host).toBe('desk.example.com');
|
||||
expect(payload.key).toBe(validKey);
|
||||
});
|
||||
|
||||
it('buildRustDeskConfigPayload drops invalid publicKey override', () => {
|
||||
const payload = keyService.buildRustDeskConfigPayload('desk.example.com', {
|
||||
publicKey: 'v1.4.9_public_key_placeholder...',
|
||||
});
|
||||
expect(payload.key).toBe('');
|
||||
});
|
||||
|
||||
it('getClientConfig with invalid file key and Go fallback fills deploy string', async () => {
|
||||
jest.spyOn(keyService, 'getPublicKey').mockReturnValue(null);
|
||||
const betterdeskApi = require('../services/betterdeskApi');
|
||||
jest.spyOn(betterdeskApi.apiClient, 'get').mockResolvedValue({ data: { key: validKey } });
|
||||
|
||||
const config = await keyService.getClientConfig({
|
||||
host: '203.0.113.10',
|
||||
relay: '203.0.113.10',
|
||||
api: 'http://203.0.113.10:21114',
|
||||
});
|
||||
|
||||
expect(config.has_public_key).toBe(true);
|
||||
expect(config.public_key).toBe(validKey);
|
||||
expect(config.deploy_config_string).toBeTruthy();
|
||||
|
||||
const json = Buffer.from(
|
||||
config.deploy_config_string.split('').reverse().join(''),
|
||||
'base64'
|
||||
).toString('utf8');
|
||||
expect(JSON.parse(json).key).toBe(validKey);
|
||||
});
|
||||
|
||||
it('getClientConfig leaves deploy string empty when no valid key exists', async () => {
|
||||
jest.spyOn(keyService, 'getPublicKey').mockReturnValue(null);
|
||||
const betterdeskApi = require('../services/betterdeskApi');
|
||||
jest.spyOn(betterdeskApi.apiClient, 'get').mockResolvedValue({
|
||||
data: { key: 'v1.4.9_public_key_placeholder...' },
|
||||
});
|
||||
|
||||
const config = await keyService.getClientConfig('desk.example.com');
|
||||
expect(config.has_public_key).toBe(false);
|
||||
expect(config.public_key).toBe('');
|
||||
expect(config.deploy_config_string).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user