import { nip19, getPublicKey, generateSecretKey } from 'nostr-tools' import { bytesToHex, hexToBytes } from 'nostr-tools/utils' import { generateRecoveryPhrase } from './keyManagementRecovery' import { deriveKeyFromPhrase, encryptNsec, decryptNsec } from './keyManagementEncryption' import { storeAccountFlag, hasAccountFlag, removeAccountFlag, getEncryptedKey, setEncryptedKey, getPublicKeys as getStoredPublicKeys, setPublicKeys, deleteStoredKeys, } from './keyManagementStorage' /** * Key management service */ export class KeyManagementService { /** * Generate a new Nostr key pair * Returns the private key (hex) and public key (hex) */ generateKeyPair(): { privateKey: string; publicKey: string; npub: string } { const secretKey = generateSecretKey() const privateKeyHex = bytesToHex(secretKey) const publicKeyHex = getPublicKey(secretKey) const npub = nip19.npubEncode(publicKeyHex) return { privateKey: privateKeyHex, publicKey: publicKeyHex, npub, } } /** * Import a private key (accepts hex or nsec format) * Returns the private key (hex), public key (hex), and npub */ importPrivateKey(privateKey: string): { privateKey: string; publicKey: string; npub: string } { let privateKeyHex: string // Try to decode as nsec try { const decoded = nip19.decode(privateKey) if (decoded.type === 'nsec' && typeof decoded.data === 'string') { privateKeyHex = decoded.data } else { throw new Error('Invalid nsec format') } } catch { // Assume it's already a hex string privateKeyHex = privateKey } const secretKey = hexToBytes(privateKeyHex) const publicKeyHex = getPublicKey(secretKey) const npub = nip19.npubEncode(publicKeyHex) return { privateKey: privateKeyHex, publicKey: publicKeyHex, npub, } } /** * Create a new account: generate/import key, encrypt it, and store it * Returns the recovery phrase and npub */ async createAccount(privateKey?: string): Promise<{ recoveryPhrase: string[] npub: string publicKey: string }> { // Generate or import key pair const keyPair = privateKey ? this.importPrivateKey(privateKey) : this.generateKeyPair() // Generate recovery phrase const recoveryPhrase = generateRecoveryPhrase() // Derive encryption key from recovery phrase const derivedKey = await deriveKeyFromPhrase(recoveryPhrase) // Encrypt the private key const encryptedNsec = await encryptNsec(derivedKey, keyPair.privateKey) // Store encrypted nsec in IndexedDB await setEncryptedKey(encryptedNsec) // Store account flag in browser storage storeAccountFlag() // Store public key separately for quick access await setPublicKeys(keyPair.publicKey, keyPair.npub) return { recoveryPhrase, npub: keyPair.npub, publicKey: keyPair.publicKey, } } /** * Check if an account exists (encrypted key is stored) */ async accountExists(): Promise { try { // Check both the flag and the actual stored key if (!hasAccountFlag()) { return false } const encrypted = await getEncryptedKey() return encrypted !== null } catch { return false } } /** * Get the public key and npub if account exists */ async getPublicKeys(): Promise<{ publicKey: string; npub: string } | null> { return await getStoredPublicKeys() } /** * Decrypt and retrieve the private key using recovery phrase */ async unlockAccount(recoveryPhrase: string[]): Promise<{ privateKey: string publicKey: string npub: string }> { // Get encrypted nsec from IndexedDB const encryptedNsec = await getEncryptedKey() if (!encryptedNsec) { throw new Error('No encrypted key found. Please create an account first.') } // Derive key from recovery phrase const derivedKey = await deriveKeyFromPhrase(recoveryPhrase) // Decrypt the private key const privateKeyHex = await decryptNsec(derivedKey, encryptedNsec) // Verify by computing public key const secretKey = hexToBytes(privateKeyHex) const publicKeyHex = getPublicKey(secretKey) const npub = nip19.npubEncode(publicKeyHex) return { privateKey: privateKeyHex, publicKey: publicKeyHex, npub, } } /** * Delete the account (remove all stored keys) */ async deleteAccount(): Promise { await deleteStoredKeys() removeAccountFlag() } } export const keyManagementService = new KeyManagementService()