/** * Identity persistence for the nodes cadre-host runs. * * Each managed node's libp2p peer identity is an Ed25519 keypair generated on * its first spawn (`node-identity.ts`). The protobuf-encoded * private key is written with mode 0600 on POSIX and loaded with * `privateKeyFromProtobuf`. * * We persist the protobuf form (not raw bytes) because that's the format * libp2p's `privateKeyFromProtobuf` consumes directly and it preserves the * key type tag — future migrations to a different key algorithm don't * require a schema change here. Raw bytes carry no type tag AND no way to tell * a damaged file from a valid one: `privateKeyFromRaw` accepts any 64 bytes as * an Ed25519 key without checking that the public half matches the private, so * a truncated file would load as a *different* working identity. * * This is the repo-wide identity file format, not a cadre-host-local choice: * `cadre enroll create` writes exactly these bytes, and `cadre-cli`'s * `identity.keyFile` accepts nothing else (`config/loader.ts` → `loadIdentityKey`). */ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname } from 'node:path'; import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf, } from '@libp2p/crypto/keys'; import { peerIdFromPrivateKey } from '@libp2p/peer-id'; import type { PrivateKey } from '@libp2p/interface'; export interface IdentityRecord { privateKey: PrivateKey; /** Stringified libp2p peer ID (`12D3Koo…`). */ peerId: string; } /** Generate a fresh Ed25519 identity and persist it. Returns the in-memory record. */ export async function generateIdentity(filePath: string): Promise { const privateKey = await generateKeyPair('Ed25519'); const bytes = privateKeyToProtobuf(privateKey); mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, bytes); // 0600 — owner read/write only. No-op on Windows. if (process.platform !== 'win32') { chmodSync(filePath, 0o600); } return { privateKey, peerId: peerIdFromPrivateKey(privateKey).toString() }; } /** Load an existing identity file. Throws if it doesn't decode. */ export function loadIdentity(filePath: string): IdentityRecord { const bytes = readFileSync(filePath); const privateKey = privateKeyFromProtobuf(new Uint8Array(bytes)); return { privateKey, peerId: peerIdFromPrivateKey(privateKey).toString() }; }