diff --git a/components/KeyManagementManager.tsx b/components/KeyManagementManager.tsx new file mode 100644 index 0000000..466a02b --- /dev/null +++ b/components/KeyManagementManager.tsx @@ -0,0 +1,414 @@ +import { useState, useEffect } from 'react' +import { nostrAuthService } from '@/lib/nostrAuth' +import { keyManagementService } from '@/lib/keyManagement' +import { nip19 } from 'nostr-tools' + +interface PublicKeys { + publicKey: string + npub: string +} + +export function KeyManagementManager() { + const [publicKeys, setPublicKeys] = useState(null) + const [accountExists, setAccountExists] = useState(false) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [importKey, setImportKey] = useState('') + const [importing, setImporting] = useState(false) + const [showImportForm, setShowImportForm] = useState(false) + const [showReplaceWarning, setShowReplaceWarning] = useState(false) + const [recoveryPhrase, setRecoveryPhrase] = useState(null) + const [newNpub, setNewNpub] = useState(null) + const [copiedNpub, setCopiedNpub] = useState(false) + const [copiedPublicKey, setCopiedPublicKey] = useState(false) + const [copiedRecoveryPhrase, setCopiedRecoveryPhrase] = useState(false) + + useEffect(() => { + void loadKeys() + }, []) + + async function loadKeys() { + try { + setLoading(true) + setError(null) + const exists = await nostrAuthService.accountExists() + setAccountExists(exists) + + if (exists) { + const keys = await keyManagementService.getPublicKeys() + if (keys) { + setPublicKeys(keys) + } + } + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to load keys' + setError(errorMessage) + console.error('Error loading keys:', e) + } finally { + setLoading(false) + } + } + + function extractKeyFromUrl(url: string): string | null { + try { + // Try to parse as URL + const urlObj = new URL(url) + // Check if it's a nostr:// URL with nsec + if (urlObj.protocol === 'nostr:' || urlObj.protocol === 'nostr://') { + const path = urlObj.pathname || urlObj.href.replace(/^nostr:?\/\//, '') + if (path.startsWith('nsec')) { + return path + } + } + // Check if URL contains nsec + const nsecMatch = url.match(/nsec1[a-z0-9]+/i) + if (nsecMatch) { + return nsecMatch[0] + } + return null + } catch { + // Not a valid URL, try to extract nsec from text + const nsecMatch = url.match(/nsec1[a-z0-9]+/i) + if (nsecMatch) { + return nsecMatch[0] + } + // Assume it's already a key (hex or nsec) + return url.trim() + } + } + + async function handleImport() { + if (!importKey.trim()) { + setError('Please enter a private key') + return + } + + // Extract key from URL or text + const extractedKey = extractKeyFromUrl(importKey.trim()) + if (!extractedKey) { + setError('Invalid key format. Please provide a nsec (nsec1...) or hex private key.') + return + } + + // Validate key format + try { + // Try to decode as nsec + const decoded = nip19.decode(extractedKey) + if (decoded.type !== 'nsec' || typeof decoded.data !== 'string') { + throw new Error('Invalid nsec format') + } + } catch { + // Assume it's hex, validate length (64 hex chars = 32 bytes) + if (!/^[0-9a-f]{64}$/i.test(extractedKey)) { + setError('Invalid key format. Please provide a nsec (nsec1...) or hex (64 characters) private key.') + return + } + } + + // If account exists, show warning + if (accountExists) { + setShowReplaceWarning(true) + return + } + + await performImport(extractedKey) + } + + async function performImport(key: string) { + try { + setImporting(true) + setError(null) + setShowReplaceWarning(false) + + // If account exists, delete it first + if (accountExists) { + await nostrAuthService.deleteAccount() + } + + // Create new account with imported key + const result = await nostrAuthService.createAccount(key) + setRecoveryPhrase(result.recoveryPhrase) + setNewNpub(result.npub) + setImportKey('') + setShowImportForm(false) + await loadKeys() + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Failed to import key' + setError(errorMessage) + console.error('Error importing key:', e) + } finally { + setImporting(false) + } + } + + async function handleCopyRecoveryPhrase() { + if (!recoveryPhrase) { + return + } + try { + await navigator.clipboard.writeText(recoveryPhrase.join(' ')) + setCopiedRecoveryPhrase(true) + setTimeout(() => { + setCopiedRecoveryPhrase(false) + }, 2000) + } catch (e) { + console.error('Error copying recovery phrase:', e) + } + } + + async function handleCopyNpub() { + if (!publicKeys?.npub) { + return + } + try { + await navigator.clipboard.writeText(publicKeys.npub) + setCopiedNpub(true) + setTimeout(() => { + setCopiedNpub(false) + }, 2000) + } catch (e) { + console.error('Error copying npub:', e) + } + } + + async function handleCopyPublicKey() { + if (!publicKeys?.publicKey) { + return + } + try { + await navigator.clipboard.writeText(publicKeys.publicKey) + setCopiedPublicKey(true) + setTimeout(() => { + setCopiedPublicKey(false) + }, 2000) + } catch (e) { + console.error('Error copying public key:', e) + } + } + + if (loading) { + return ( +
+

Loading...

+
+ ) + } + + return ( +
+
+

Key Management

+ + {error && ( +
+

{error}

+
+ )} + + {/* Public Keys Display */} + {publicKeys && ( +
+
+
+

Public Key (npub)

+ +
+

{publicKeys.npub}

+
+ +
+
+

Public Key (hex)

+ +
+

{publicKeys.publicKey}

+
+
+ )} + + {!publicKeys && !accountExists && ( +
+

No account found

+

+ Create a new account by importing a private key. The key will be encrypted using a two-level encryption system. +

+
+ )} + + {/* Import Form */} + {!showImportForm && ( + + )} + + {showImportForm && ( +
+
+

⚠️ Important

+

+ After importing, you will receive 4 recovery words (BIP39 dictionary) to secure your account. + These words encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API, which then encrypts your private key stored in IndexedDB (two-level encryption system). +

+ {accountExists && ( +

+ Warning: Importing a new key will replace your existing account. Make sure you have your recovery phrase saved before proceeding. +

+ )} +
+ +
+ +