diff --git a/src/daemon/admin/commands/create_new_key.ts b/src/daemon/admin/commands/create_new_key.ts index 468c3cf..cdedcd3 100644 --- a/src/daemon/admin/commands/create_new_key.ts +++ b/src/daemon/admin/commands/create_new_key.ts @@ -2,6 +2,8 @@ import NDK, { NDKEvent, NDKPrivateKeySigner, NDKRpcRequest, type NostrEvent } fr import AdminInterface from "../index.js"; import { NIP46_ADMIN_RESPONSE_KIND } from "../kinds.js"; import { saveEncrypted } from "../../../commands/add.js"; +import { getCurrentConfig } from "../../../config/index.js"; +import { decryptNsec } from "../../../config/keys.js"; import { setupSkeletonProfile } from "../../lib/profile.js"; export default async function createNewKey(admin: AdminInterface, req: NDKRpcRequest) { @@ -10,6 +12,42 @@ export default async function createNewKey(admin: AdminInterface, req: NDKRpcReq if (!keyName || !passphrase) throw new Error("Invalid params"); if (!admin.loadNsec) throw new Error("No unlockKey method"); + // Idempotency guard. Callers re-pair a machine through the same keyName + // (e.g. spirekeeper's `spire-`) and rely on create_new_key being + // idempotent — "returns the existing key if the name is taken". Without + // this guard the command unconditionally generated a FRESH key and + // `saveEncrypted` overwrote the existing encrypted blob on disk, silently + // destroying the in-use signing identity (unrecoverable — the old nsec then + // survives only in the running process until the next restart). NEVER + // generate-and-overwrite an existing name: recover and return it instead. + const currentConfig = await getCurrentConfig(admin.configFile); + const existing = currentConfig.keys?.[keyName]; + if (existing) { + if (_nsec) { + throw new Error( + `key '${keyName}' already exists; refusing to overwrite it with an ` + + `imported nsec — delete it explicitly first if replacement is intended`, + ); + } + if (!existing.iv || !existing.data) { + throw new Error( + `key '${keyName}' already exists in an unrecognized form; refusing to overwrite`, + ); + } + let existingNsec: string; + try { + existingNsec = decryptNsec(existing.iv, existing.data, passphrase); + } catch (e: any) { + throw new Error( + `key '${keyName}' already exists but the supplied passphrase did not ` + + `decrypt it; refusing to overwrite (${e.message})`, + ); + } + const existingUser = await new NDKPrivateKeySigner(existingNsec).user(); + const result = JSON.stringify({ npub: existingUser.npub }); + return admin.rpc.sendResponse(req.id, req.pubkey, result, NIP46_ADMIN_RESPONSE_KIND); + } + let key; if (_nsec) {