Skip to main content
Glama
keyManager.ts11.2 kB
/** * Secure key management using bitcoin-backup * Provides encrypted storage for BSV MCP keys with backward compatibility */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { PrivateKey } from "@bsv/sdk"; import { type BapMasterBackup, type DecryptedBackup, type OneSatBackup, decryptBackup, encryptBackup, } from "bitcoin-backup"; import { promptForPassphraseWithFallback } from "./passphrasePrompt"; /** * Key storage interface */ export interface KeyStore { payPk?: PrivateKey; identityPk?: PrivateKey; xprv?: string; } /** * Configuration for key manager */ export interface KeyManagerConfig { keyDir?: string; autoMigrate?: boolean; keepLegacy?: boolean; } /** * Secure key manager with encryption support * * Features: * - Encrypted key storage using bitcoin-backup * - Backward compatibility with legacy JSON format * - Automatic migration from unencrypted to encrypted * - Backup management */ export class SecureKeyManager { private readonly keyDir: string; private readonly legacyFile: string; private readonly encryptedFile: string; private readonly backupFile: string; private readonly config: KeyManagerConfig; constructor(config: KeyManagerConfig = {}) { this.config = config; this.keyDir = config.keyDir || path.join(os.homedir(), ".bsv-mcp"); this.legacyFile = path.join(this.keyDir, "keys.json"); this.encryptedFile = path.join(this.keyDir, "keys.bep"); this.backupFile = path.join(this.keyDir, "keys.bep.backup"); } /** * Check if user has legacy passphrase environment variable set */ hasLegacyPassphrase(): boolean { return !!process.env.BSV_MCP_PASSPHRASE; } /** * Load keys with automatic format detection and migration */ async loadKeys(): Promise<{ keys: KeyStore; source: "encrypted" | "legacy" | "none"; }> { // Warn about legacy passphrase usage if (this.hasLegacyPassphrase()) { console.warn( "\n⚠️ WARNING: BSV_MCP_PASSPHRASE environment variable is deprecated!\n" + " This is insecure as it stores your passphrase in plain text.\n" + " Please remove it from your environment.\n" + " The system will now prompt for passphrases when needed.\n", ); } // Try encrypted format first if (this.hasEncryptedBackup()) { try { // Prompt for passphrase to decrypt const passphrase = await promptForPassphraseWithFallback( "Enter passphrase to decrypt your wallet keys", ); const keys = await this.loadEncryptedKeys(passphrase); return { keys, source: "encrypted" }; } catch (error) { // If user cancels or timeout, try legacy format if ( error instanceof Error && (error.message.includes("cancelled") || error.message.includes("timeout")) ) { console.log( "⚠️ Passphrase prompt cancelled, checking for legacy keys...", ); } else { console.error("❌ Failed to decrypt keys.bep:", error); } } } // Try legacy format if (this.hasLegacyKeys()) { const keys = this.loadLegacyKeys(); // Offer to migrate if auto-migrate is enabled if (this.config.autoMigrate !== false) { console.log( "\n[Clipboard] Found unencrypted keys. Would you like to encrypt them?", ); console.log( " (You can skip this by pressing Ctrl+C in the browser)\n", ); try { const passphrase = await promptForPassphraseWithFallback( "Create a passphrase to encrypt your wallet keys (recommended)", { isNewPassphrase: true }, ); console.log("🔄 Migrating keys to encrypted format..."); await this.saveEncryptedKeys(keys, passphrase); console.log("✅ Keys migrated to encrypted format"); // Remove legacy file unless configured to keep if (!this.config.keepLegacy) { this.removeLegacyKeys(); } return { keys, source: "encrypted" }; } catch (error) { console.log("ℹ️ Continuing with unencrypted keys..."); } } return { keys, source: "legacy" }; } // No keys found return { keys: {}, source: "none" }; } /** * Save keys in the appropriate format */ async saveKeys(keys: KeyStore, forceUnencrypted = false): Promise<void> { // If forcing unencrypted (for initial generation), save as legacy if (forceUnencrypted) { this.saveLegacyKeys(keys); console.log( "💾 Saved unencrypted keys (you can encrypt them on next run)", ); return; } // If we already have encrypted keys, prompt for passphrase if (this.hasEncryptedBackup()) { try { const passphrase = await promptForPassphraseWithFallback( "Enter passphrase to update your encrypted wallet", ); await this.saveEncryptedKeys(keys, passphrase); return; } catch (error) { console.error("⚠️ Failed to encrypt keys:", error); throw error; } } // For new keys, ask if user wants to encrypt try { console.log("\n[Lock] Would you like to encrypt your new wallet keys?"); console.log(" (Recommended for security)\n"); const passphrase = await promptForPassphraseWithFallback( "Create a passphrase to encrypt your wallet keys", { isNewPassphrase: true }, ); await this.saveEncryptedKeys(keys, passphrase); } catch (error) { // User cancelled or error - save unencrypted console.log("ℹ️ Saving keys unencrypted (you can encrypt them later)"); this.saveLegacyKeys(keys); } } /** * Load keys from legacy JSON format */ private loadLegacyKeys(): KeyStore { try { const content = fs.readFileSync(this.legacyFile, "utf8"); const data = JSON.parse(content); return { payPk: data.payPk ? PrivateKey.fromWif(data.payPk) : undefined, identityPk: data.identityPk ? PrivateKey.fromWif(data.identityPk) : undefined, xprv: data.xprv, }; } catch (error) { throw new Error(`Failed to load legacy keys: ${error}`); } } /** * Save keys in legacy JSON format */ private saveLegacyKeys(keys: KeyStore): void { const data = { payPk: keys.payPk?.toWif(), identityPk: keys.identityPk?.toWif(), xprv: keys.xprv, }; fs.mkdirSync(this.keyDir, { recursive: true, mode: 0o700 }); fs.writeFileSync(this.legacyFile, JSON.stringify(data, null, 2), { mode: 0o600, }); } /** * Load keys from encrypted backup */ private async loadEncryptedKeys(passphrase: string): Promise<KeyStore> { const encrypted = fs.readFileSync(this.encryptedFile, "utf8"); const decrypted = await decryptBackup(encrypted, passphrase); // Check if it's a OneSatBackup (our format) if ("payPk" in decrypted && "identityPk" in decrypted) { const backup = decrypted as OneSatBackup; // Also check for xprv in legacy file if it exists let xprv: string | undefined; if (fs.existsSync(this.legacyFile)) { try { const legacyData = JSON.parse( fs.readFileSync(this.legacyFile, "utf8"), ); xprv = legacyData.xprv; } catch (e) { // Ignore legacy file errors } } return { payPk: backup.payPk ? PrivateKey.fromWif(backup.payPk) : undefined, identityPk: backup.identityPk ? PrivateKey.fromWif(backup.identityPk) : undefined, xprv, }; } // Check if it's a BapMasterBackup (for xprv) if ("xprv" in decrypted) { const backup = decrypted as BapMasterBackup; return { payPk: undefined, // BapMasterBackup doesn't include payment key identityPk: undefined, // BapMasterBackup doesn't include identity key xprv: backup.xprv, }; } throw new Error("Unknown backup format"); } /** * Save keys in encrypted format */ async saveEncryptedKeys(keys: KeyStore, passphrase: string): Promise<void> { // We use OneSatBackup format which includes all our keys const data: OneSatBackup = { payPk: keys.payPk?.toWif() || "", identityPk: keys.identityPk?.toWif() || "", ordPk: "", // We don't use ordinals key, but it's required by the type label: "BSV MCP Keys", createdAt: new Date().toISOString(), }; // If we have xprv, we need to store it separately in the legacy format for now // since OneSatBackup doesn't support xprv if (keys.xprv) { // Store xprv in the legacy JSON alongside encrypted keys // This is a temporary solution until we have a better format const legacyData = { xprv: keys.xprv, }; fs.mkdirSync(this.keyDir, { recursive: true, mode: 0o700 }); fs.writeFileSync(this.legacyFile, JSON.stringify(legacyData, null, 2), { mode: 0o600, }); } const encrypted = await encryptBackup(data, passphrase); // Create backup of existing file if (fs.existsSync(this.encryptedFile)) { fs.copyFileSync(this.encryptedFile, this.backupFile); } fs.mkdirSync(this.keyDir, { recursive: true, mode: 0o700 }); fs.writeFileSync(this.encryptedFile, encrypted, { mode: 0o600 }); } /** * Check if encrypted backup exists */ hasEncryptedBackup(): boolean { return fs.existsSync(this.encryptedFile); } /** * Check if legacy keys exist */ hasLegacyKeys(): boolean { return fs.existsSync(this.legacyFile); } /** * Remove legacy keys file */ private removeLegacyKeys(): void { try { fs.unlinkSync(this.legacyFile); console.log("🗑️ Removed legacy unencrypted keys file"); } catch (error) { console.error("Failed to remove legacy keys:", error); } } /** * Get status of key storage */ getStatus(): { hasEncrypted: boolean; hasLegacy: boolean; hasLegacyPassphrase: boolean; isSecure: boolean; } { const hasEncrypted = this.hasEncryptedBackup(); const hasLegacy = this.hasLegacyKeys(); const hasLegacyPassphrase = this.hasLegacyPassphrase(); return { hasEncrypted, hasLegacy, hasLegacyPassphrase, isSecure: hasEncrypted && !hasLegacy, }; } } /** * Default key manager instance */ export const keyManager = new SecureKeyManager({ autoMigrate: process.env.BSV_MCP_AUTO_MIGRATE === "true", keepLegacy: process.env.BSV_MCP_KEEP_LEGACY === "true", }); /** * Initialize keys with secure storage support * * This is a wrapper around the key manager for easier migration * from the existing initializeKeys function */ export async function initializeSecureKeys(): Promise<{ payPk?: PrivateKey; identityPk?: PrivateKey; xprv?: string; source: "env" | "encrypted" | "legacy" | "generated" | "none"; }> { // Check environment first (maintains compatibility) const privateKeyWifEnv = process.env.PRIVATE_KEY_WIF; if (privateKeyWifEnv) { try { const payPk = PrivateKey.fromWif(privateKeyWifEnv); console.log("✅ Using PRIVATE_KEY_WIF from environment"); return { payPk, source: "env", }; } catch (error) { console.error("⚠️ Invalid PRIVATE_KEY_WIF format"); } } // Try loading from secure storage try { const { keys, source } = await keyManager.loadKeys(); if (keys.payPk || keys.xprv) { const sourceMap = { encrypted: "encrypted" as const, legacy: "legacy" as const, none: "none" as const, }; return { ...keys, source: sourceMap[source] || "none" }; } } catch (error) { console.error("Failed to load keys:", error); } return { source: "none" }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/b-open-io/bsv-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server