feat: add encrypted local node enrollment
This commit is contained in:
parent
683e135f5e
commit
d2ec5c6c87
6 changed files with 233 additions and 14 deletions
|
|
@ -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 || '');
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue