Files
BetterDesk/web-nodejs/public/js/rdclientSecureStore.js
T
UNITRONIX 2e01955f6c feat(rdclient): enhance desktop client with server validation, LAN discovery, and settings management
- Introduced server URL validation via `GET /api/bd/server-info` and `probe_server_url`.
- Added LAN discovery capabilities using UDP and optional mDNS for local network panel detection.
- Implemented a local settings window for managing URL, TLS settings, and user preferences.
- Enhanced dashboard with a unified sidebar and improved scrolling behavior.
- Updated documentation and pre-release checklist to reflect new features and requirements.
2026-06-14 21:12:41 +02:00

285 lines
9.3 KiB
JavaScript

/**
* BetterDesk RdClient — encrypted credential storage (browser).
* Passwords are stored in IndexedDB with AES-GCM (Web Crypto); never plaintext in localStorage.
*/
var RdClientSecureStore = (function () {
'use strict';
var DB_NAME = 'betterdesk-rdclient';
var DB_VERSION = 2;
var STORE = 'vault';
var KEY_ID = 'device-key';
var CRED_ID = 'credentials';
var USERNAME_KEY = 'bd_rdclient_last_user';
function openDb() {
return new Promise(function (resolve, reject) {
if (!window.indexedDB) {
reject(new Error('IndexedDB unavailable'));
return;
}
var req = indexedDB.open(DB_NAME, DB_VERSION);
req.onerror = function () { reject(req.error || new Error('IndexedDB open failed')); };
req.onupgradeneeded = function (ev) {
var db = ev.target.result;
if (!db.objectStoreNames.contains(STORE)) {
db.createObjectStore(STORE);
}
};
req.onsuccess = function () { resolve(req.result); };
});
}
function idbGet(db, key) {
return new Promise(function (resolve, reject) {
var tx = db.transaction(STORE, 'readonly');
var req = tx.objectStore(STORE).get(key);
req.onsuccess = function () { resolve(req.result); };
req.onerror = function () { reject(req.error); };
});
}
function idbPut(db, key, value) {
return new Promise(function (resolve, reject) {
var tx = db.transaction(STORE, 'readwrite');
var req = tx.objectStore(STORE).put(value, key);
req.onsuccess = function () { resolve(); };
req.onerror = function () { reject(req.error); };
});
}
function idbDelete(db, key) {
return new Promise(function (resolve, reject) {
var tx = db.transaction(STORE, 'readwrite');
var req = tx.objectStore(STORE).delete(key);
req.onsuccess = function () { resolve(); };
req.onerror = function () { reject(req.error); };
});
}
function bufToB64(buf) {
var bytes = new Uint8Array(buf);
var bin = '';
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function b64ToBuf(b64) {
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
return bytes.buffer;
}
async function getOrCreateDeviceKey(db) {
var existing = await idbGet(db, KEY_ID);
if (existing && existing.key) {
return crypto.subtle.importKey(
'raw',
b64ToBuf(existing.key),
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
var raw = crypto.getRandomValues(new Uint8Array(32));
await idbPut(db, KEY_ID, { key: bufToB64(raw.buffer) });
return crypto.subtle.importKey(
'raw',
raw,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
async function encryptPassword(key, password) {
var iv = crypto.getRandomValues(new Uint8Array(12));
var enc = new TextEncoder().encode(password);
var cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, enc);
return { iv: bufToB64(iv.buffer), data: bufToB64(cipher) };
}
async function decryptPassword(key, payload) {
if (!payload || !payload.iv || !payload.data) return '';
var iv = new Uint8Array(b64ToBuf(payload.iv));
var data = b64ToBuf(payload.data);
var plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, data);
return new TextDecoder().decode(plain);
}
function saveLastUsername(username) {
try {
if (username) localStorage.setItem(USERNAME_KEY, username);
else localStorage.removeItem(USERNAME_KEY);
} catch (_) { /* quota / private mode */ }
}
function loadLastUsername() {
try {
return localStorage.getItem(USERNAME_KEY) || '';
} catch (_) {
return '';
}
}
async function saveCredentials(username, password, rememberPassword) {
saveLastUsername(username);
if (!rememberPassword || !password) {
await clearStoredPassword();
return;
}
if (!window.crypto || !window.crypto.subtle) return;
var db = await openDb();
try {
var key = await getOrCreateDeviceKey(db);
var encrypted = await encryptPassword(key, password);
await idbPut(db, CRED_ID, {
username: username,
password: encrypted,
rememberPassword: true,
updatedAt: Date.now()
});
} finally {
db.close();
}
}
async function clearStoredPassword() {
if (!window.indexedDB) return;
var db = await openDb();
try {
await idbDelete(db, CRED_ID);
} finally {
db.close();
}
}
async function clearCredentials() {
saveLastUsername('');
await clearStoredPassword();
}
async function loadCredentials() {
var username = loadLastUsername();
var result = { username: username, password: '', rememberPassword: false };
if (!window.crypto || !window.crypto.subtle || !window.indexedDB) {
return result;
}
var db = await openDb();
try {
var record = await idbGet(db, CRED_ID);
if (!record || !record.rememberPassword || !record.password) {
return result;
}
var key = await getOrCreateDeviceKey(db);
var password = await decryptPassword(key, record.password);
result.username = record.username || username;
result.password = password;
result.rememberPassword = true;
return result;
} catch (_) {
return result;
} finally {
db.close();
}
}
async function hasStoredPassword() {
if (!window.indexedDB) return false;
var db = await openDb();
try {
var record = await idbGet(db, CRED_ID);
return !!(record && record.rememberPassword && record.password);
} catch (_) {
return false;
} finally {
db.close();
}
}
function peerKey(deviceId) {
return 'peer:' + String(deviceId || '').trim();
}
async function savePeerPassword(deviceId, password) {
if (!deviceId || !password || !window.crypto || !window.crypto.subtle) return;
var db = await openDb();
try {
var key = await getOrCreateDeviceKey(db);
var encrypted = await encryptPassword(key, password);
await idbPut(db, peerKey(deviceId), {
encrypted: encrypted,
updatedAt: Date.now()
});
} finally {
db.close();
}
}
async function loadPeerPassword(deviceId) {
if (!deviceId || !window.crypto || !window.crypto.subtle || !window.indexedDB) return '';
var db = await openDb();
try {
var record = await idbGet(db, peerKey(deviceId));
if (!record || !record.encrypted) return '';
var key = await getOrCreateDeviceKey(db);
return await decryptPassword(key, record.encrypted);
} catch (_) {
return '';
} finally {
db.close();
}
}
async function clearPeerPassword(deviceId) {
if (!deviceId || !window.indexedDB) return;
var db = await openDb();
try {
await idbDelete(db, peerKey(deviceId));
} finally {
db.close();
}
}
async function clearAllPeerPasswords() {
if (!window.indexedDB) return;
var db = await openDb();
try {
var tx = db.transaction(STORE, 'readwrite');
var store = tx.objectStore(STORE);
var req = store.openCursor();
await new Promise(function (resolve, reject) {
req.onsuccess = function () {
var cursor = req.result;
if (!cursor) { resolve(); return; }
if (String(cursor.key).indexOf('peer:') === 0) {
cursor.delete();
}
cursor.continue();
};
req.onerror = function () { reject(req.error); };
});
} finally {
db.close();
}
}
return {
saveCredentials: saveCredentials,
loadCredentials: loadCredentials,
clearCredentials: clearCredentials,
clearStoredPassword: clearStoredPassword,
hasStoredPassword: hasStoredPassword,
loadLastUsername: loadLastUsername,
savePeerPassword: savePeerPassword,
loadPeerPassword: loadPeerPassword,
clearPeerPassword: clearPeerPassword,
clearAllPeerPasswords: clearAllPeerPasswords
};
})();
if (typeof window !== 'undefined') {
window.RdClientSecureStore = RdClientSecureStore;
}