hololake-system-architecture/product-source/guanghu-knowledge-base/server/node-keystore-enrollment.ts

86 lines
3.2 KiB
TypeScript

import { generateKeyPairSync, randomUUID } from 'node:crypto';
import {
NODE_KEYSTORE_RECORD_SCHEMA,
nodePublicKeyFingerprint,
parseNodeKeyRecord,
type NodeKeyRecord,
} from './node-keystore-bridge.js';
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
export interface NodeKeyEnrollmentAdapter {
encryptPrivateKeyPkcs8(privateKeyPkcs8: Buffer): Promise<string | null>;
isAvailable(): boolean;
readRecord(): Promise<unknown | null>;
writeRecord(record: Readonly<NodeKeyRecord>): Promise<void>;
}
export interface PublicNodeKeyEnrollment {
created: boolean;
nodeId: string;
nodeKeyFingerprint: string;
nodeType: 'local-terminal';
publicKeyPem: string;
registrationState: 'LOCAL_KEY_READY';
}
function publicProjection(record: Readonly<NodeKeyRecord>, created: boolean): Readonly<PublicNodeKeyEnrollment> {
if (record.nodeType !== 'local-terminal') throw new Error('node_keystore_wrong_node_type');
return Object.freeze({
created,
nodeId: record.nodeId,
nodeKeyFingerprint: record.nodeKeyFingerprint,
nodeType: record.nodeType,
publicKeyPem: record.publicKeyPem,
registrationState: 'LOCAL_KEY_READY',
});
}
export class EncryptedLocalNodeEnrollmentStore {
constructor(
private readonly adapter: NodeKeyEnrollmentAdapter,
private readonly nodeIdFactory: () => string = () => `HL-LOCAL-${randomUUID().toUpperCase()}`,
) {}
async status(): Promise<Readonly<PublicNodeKeyEnrollment> | null> {
const raw = await this.adapter.readRecord();
if (raw === null) return null;
const record = parseNodeKeyRecord(raw);
if (!record) throw new Error('node_keystore_record_invalid');
return publicProjection(record, false);
}
async ensureLocalTerminal(): Promise<Readonly<PublicNodeKeyEnrollment>> {
if (!this.adapter.isAvailable()) throw new Error('node_keystore_encryption_unavailable');
const existing = await this.status();
if (existing) return existing;
let privateKeyPkcs8: Buffer | null = null;
try {
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
privateKeyPkcs8 = privateKey.export({ format: 'der', type: 'pkcs8' });
const publicKeyPem = publicKey.export({ format: 'pem', type: 'spki' }).toString();
const nodeKeyFingerprint = nodePublicKeyFingerprint(publicKeyPem);
const encryptedPrivateKeyPkcs8 = await this.adapter.encryptPrivateKeyPkcs8(privateKeyPkcs8);
if (!nodeKeyFingerprint
|| !encryptedPrivateKeyPkcs8
|| encryptedPrivateKeyPkcs8.length > 65_536
|| !BASE64_PATTERN.test(encryptedPrivateKeyPkcs8)) {
throw new Error('node_keystore_enrollment_failed');
}
const record: Readonly<NodeKeyRecord> = Object.freeze({
encryptedPrivateKeyPkcs8,
nodeId: this.nodeIdFactory(),
nodeKeyFingerprint,
nodeType: 'local-terminal',
publicKeyPem,
schema: NODE_KEYSTORE_RECORD_SCHEMA,
});
if (!parseNodeKeyRecord(record)) throw new Error('node_keystore_enrollment_failed');
await this.adapter.writeRecord(record);
return publicProjection(record, true);
} finally {
privateKeyPkcs8?.fill(0);
}
}
}