feat: add encrypted local node enrollment

This commit is contained in:
冰朔 2026-08-10 11:09:12 +08:00
commit d2ec5c6c87
6 changed files with 233 additions and 14 deletions

View file

@ -17,12 +17,21 @@ import { app, BrowserWindow, shell, dialog, ipcMain, safeStorage } from 'electro
import path from 'path';
import { spawn, ChildProcess } from 'child_process';
import fs from 'fs';
import { randomUUID } from 'node:crypto';
import { importKnowledgeFolder } from './folder-import.js';
import { DomainAccessOrchestrator } from '../../guanghu-knowledge-base/server/domain-access-orchestrator.js';
import { TrustedSignerSnapshotLoader } from '../../guanghu-knowledge-base/server/trusted-signer-snapshot.js';
import { NodeRegistrationSnapshotLoader } from '../../guanghu-knowledge-base/server/node-registration-snapshot.js';
import { HttpNodeRegistrationClaimSource } from '../../guanghu-knowledge-base/server/node-registration-client.js';
import { EncryptedNodeKeyStoreProofSource } from '../../guanghu-knowledge-base/server/node-keystore-bridge.js';
import {
EncryptedNodeKeyStoreProofSource,
type NodeKeyRecord,
type NodeKeyStoreAdapter,
} from '../../guanghu-knowledge-base/server/node-keystore-bridge.js';
import {
EncryptedLocalNodeEnrollmentStore,
type NodeKeyEnrollmentAdapter,
} from '../../guanghu-knowledge-base/server/node-keystore-enrollment.js';
import { DomainRuntimeHandoffSnapshotLoader } from '../../guanghu-knowledge-base/server/domain-runtime-handoff-snapshot.js';
import {
AnchoredDomainRuntimeHandoffSource,
@ -368,17 +377,18 @@ const domainRuntimeHandoffs = new AnchoredDomainRuntimeHandoffSource(
new HttpDomainRuntimeHandoffTransport(),
);
const nodeKeyStoreProofs = new EncryptedNodeKeyStoreProofSource({
const nodeKeyStoreAdapter: NodeKeyStoreAdapter & NodeKeyEnrollmentAdapter = {
isAvailable() {
return safeStorage.isEncryptionAvailable();
},
async readRecord() {
try {
const stat = fs.statSync(NODE_KEYSTORE_PATH);
if (!stat.isFile() || (stat.mode & 0o077) !== 0) return null;
if (!stat.isFile() || (stat.mode & 0o077) !== 0) throw new Error('node_keystore_record_unreadable');
return JSON.parse(fs.readFileSync(NODE_KEYSTORE_PATH, 'utf8')) as unknown;
} catch {
return null;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
throw new Error('node_keystore_record_unreadable');
}
},
async decryptPrivateKeyPkcs8(encrypted) {
@ -397,7 +407,26 @@ const nodeKeyStoreProofs = new EncryptedNodeKeyStoreProofSource({
return null;
}
},
});
async encryptPrivateKeyPkcs8(privateKeyPkcs8: Buffer) {
if (!safeStorage.isEncryptionAvailable()) return null;
return safeStorage.encryptString(privateKeyPkcs8.toString('base64')).toString('base64');
},
async writeRecord(record: Readonly<NodeKeyRecord>) {
fs.mkdirSync(path.dirname(NODE_KEYSTORE_PATH), { recursive: true });
const temporaryPath = `${NODE_KEYSTORE_PATH}.${process.pid}.${randomUUID()}.tmp`;
try {
fs.writeFileSync(temporaryPath, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
fs.renameSync(temporaryPath, NODE_KEYSTORE_PATH);
fs.chmodSync(NODE_KEYSTORE_PATH, 0o600);
} catch (error) {
try { fs.unlinkSync(temporaryPath); } catch {}
throw error;
}
},
};
const nodeKeyStoreProofs = new EncryptedNodeKeyStoreProofSource(nodeKeyStoreAdapter);
const localNodeEnrollments = new EncryptedLocalNodeEnrollmentStore(nodeKeyStoreAdapter);
const domainAccessOrchestrator = new DomainAccessOrchestrator(
trustedSignerSnapshots,
@ -685,6 +714,8 @@ ipcMain.handle('server:domain-access', async (_event, input: { domainId?: unknow
if (!nodeType) throw new Error('domain_access_node_type_invalid');
return domainAccessOrchestrator.domainAccess(domainId, nodeType);
});
ipcMain.handle('server:local-node-status', async () => localNodeEnrollments.status());
ipcMain.handle('server:ensure-local-node', async () => localNodeEnrollments.ensureLocalTerminal());
ipcMain.handle('server:login', async (_event, input: { nodeId: string; username: string; password: string }) => {
const username = String(input.username || '').trim();
const password = String(input.password || '');

View file

@ -33,6 +33,8 @@ contextBridge.exposeInMainWorld('hololake', {
domainRegistry: () => ipcRenderer.invoke('server:domain-registry'),
domainAccess: (domainId: string, nodeType: 'local-terminal' | 'cloud-resident') =>
ipcRenderer.invoke('server:domain-access', { domainId, nodeType }),
localNodeStatus: () => ipcRenderer.invoke('server:local-node-status'),
ensureLocalNode: () => ipcRenderer.invoke('server:ensure-local-node'),
session: (nodeId?: string) => ipcRenderer.invoke('server:session', nodeId),
login: (input: { nodeId: string; username: string; password: string }) =>
ipcRenderer.invoke('server:login', input),