Add key management configuration page
**Motivations:** - Provide a dedicated interface for managing Nostr keys - Allow users to view public keys (npub and hex) - Enable importing private keys via URL or text format - Respect the existing two-level encryption system **Root causes:** - No dedicated interface for key management - Users need to view their public keys easily - Users need to import keys in various formats (nsec URL, nsec text, hex) **Correctifs:** - None (new feature) **Evolutions:** - Created KeyManagementManager component for key management - Added public key display (npub and hex formats) - Implemented private key import with support for: - nostr:// URLs with nsec - nsec text format (nsec1...) - hex format (64 characters) - Automatic key extraction from URLs - Account replacement warning and confirmation - Recovery phrase display after import - Individual copy buttons for each key format - Integration in settings page **Pages affectées:** - components/KeyManagementManager.tsx (new) - pages/settings.tsx (modified) - features/key-management-configuration.md (new)
This commit is contained in:
parent
b5ec69624c
commit
7791370b37
414
components/KeyManagementManager.tsx
Normal file
414
components/KeyManagementManager.tsx
Normal file
@ -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<PublicKeys | null>(null)
|
||||||
|
const [accountExists, setAccountExists] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [importKey, setImportKey] = useState('')
|
||||||
|
const [importing, setImporting] = useState(false)
|
||||||
|
const [showImportForm, setShowImportForm] = useState(false)
|
||||||
|
const [showReplaceWarning, setShowReplaceWarning] = useState(false)
|
||||||
|
const [recoveryPhrase, setRecoveryPhrase] = useState<string[] | null>(null)
|
||||||
|
const [newNpub, setNewNpub] = useState<string | null>(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 (
|
||||||
|
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
|
||||||
|
<p className="text-cyber-accent">Loading...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
|
||||||
|
<h2 className="text-2xl font-bold text-neon-cyan mb-4">Key Management</h2>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-900/20 border border-red-400/50 rounded-lg p-4 mb-4">
|
||||||
|
<p className="text-red-400">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Public Keys Display */}
|
||||||
|
{publicKeys && (
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<p className="text-neon-blue font-semibold">Public Key (npub)</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
void handleCopyNpub()
|
||||||
|
}}
|
||||||
|
className="px-3 py-1 text-xs bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded transition-colors"
|
||||||
|
>
|
||||||
|
{copiedNpub ? '✓ Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-neon-cyan text-sm font-mono break-all">{publicKeys.npub}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<p className="text-neon-blue font-semibold">Public Key (hex)</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
void handleCopyPublicKey()
|
||||||
|
}}
|
||||||
|
className="px-3 py-1 text-xs bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded transition-colors"
|
||||||
|
>
|
||||||
|
{copiedPublicKey ? '✓ Copied' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-neon-cyan text-sm font-mono break-all">{publicKeys.publicKey}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!publicKeys && !accountExists && (
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-yellow-400 font-semibold mb-2">No account found</p>
|
||||||
|
<p className="text-yellow-300/90 text-sm">
|
||||||
|
Create a new account by importing a private key. The key will be encrypted using a two-level encryption system.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Import Form */}
|
||||||
|
{!showImportForm && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowImportForm(true)
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
className="w-full py-3 px-6 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
|
||||||
|
>
|
||||||
|
{accountExists ? 'Replace Account (Import New Key)' : 'Import Private Key'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showImportForm && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4">
|
||||||
|
<p className="text-yellow-400 font-semibold mb-2">⚠️ Important</p>
|
||||||
|
<p className="text-yellow-300/90 text-sm">
|
||||||
|
After importing, you will receive <strong className="font-bold">4 recovery words</strong> (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).
|
||||||
|
</p>
|
||||||
|
{accountExists && (
|
||||||
|
<p className="text-yellow-300/90 text-sm mt-2">
|
||||||
|
<strong className="font-bold">Warning:</strong> Importing a new key will replace your existing account. Make sure you have your recovery phrase saved before proceeding.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="importKey" className="block text-sm font-medium text-cyber-accent mb-2">
|
||||||
|
Private Key (nsec URL, nsec1..., or hex)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="importKey"
|
||||||
|
value={importKey}
|
||||||
|
onChange={(e) => {
|
||||||
|
setImportKey(e.target.value)
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
placeholder="nsec1... or nostr://nsec1... or hex key"
|
||||||
|
className="w-full px-3 py-2 bg-cyber-dark border border-neon-cyan/30 rounded-lg font-mono text-sm text-neon-cyan focus:border-neon-cyan focus:outline-none"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-cyber-accent/70 mt-2">
|
||||||
|
You can paste a nsec key, a nostr:// URL containing a nsec, or a hex private key (64 characters).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showReplaceWarning && (
|
||||||
|
<div className="bg-red-900/20 border border-red-400/50 rounded-lg p-4">
|
||||||
|
<p className="text-red-400 font-semibold mb-2">⚠️ Replace Existing Account?</p>
|
||||||
|
<p className="text-red-300/90 text-sm mb-4">
|
||||||
|
This will delete your current account and create a new one with the imported key. Make sure you have saved your recovery phrase for the current account.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowReplaceWarning(false)
|
||||||
|
}}
|
||||||
|
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
void performImport(extractKeyFromUrl(importKey.trim()) || importKey.trim())
|
||||||
|
}}
|
||||||
|
disabled={importing}
|
||||||
|
className="flex-1 py-2 px-4 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg font-medium transition-all border border-red-400/50 hover:shadow-glow-red disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{importing ? 'Replacing...' : 'Replace Account'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showReplaceWarning && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowImportForm(false)
|
||||||
|
setImportKey('')
|
||||||
|
setError(null)
|
||||||
|
}}
|
||||||
|
className="flex-1 py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
void handleImport()
|
||||||
|
}}
|
||||||
|
disabled={importing}
|
||||||
|
className="flex-1 py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{importing ? 'Importing...' : 'Import'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recovery Phrase Display (after import) */}
|
||||||
|
{recoveryPhrase && newNpub && (
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-400/50 rounded-lg p-4">
|
||||||
|
<p className="text-yellow-400 font-semibold mb-2">⚠️ Important</p>
|
||||||
|
<p className="text-yellow-300/90 text-sm">
|
||||||
|
These <strong className="font-bold">4 recovery words</strong> are your only way to recover your account.
|
||||||
|
<strong className="font-bold"> They will never be displayed again.</strong>
|
||||||
|
</p>
|
||||||
|
<p className="text-yellow-300/90 text-sm mt-2">
|
||||||
|
These words (BIP39 dictionary) are used with <strong>PBKDF2</strong> to encrypt a Key Encryption Key (KEK) stored in the browser's Credentials API. This KEK then encrypts your private key stored in IndexedDB (two-level system).
|
||||||
|
</p>
|
||||||
|
<p className="text-yellow-300/90 text-sm mt-2">
|
||||||
|
Save them in a safe place. Without these words, you will permanently lose access to your account.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-cyber-darker border border-neon-cyan/30 rounded-lg p-6">
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
{recoveryPhrase.map((word, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-cyber-dark border border-neon-cyan/30 rounded-lg p-3 text-center font-mono text-lg"
|
||||||
|
>
|
||||||
|
<span className="text-cyber-accent/70 text-sm mr-2">{index + 1}.</span>
|
||||||
|
<span className="font-semibold text-neon-cyan">{word}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
void handleCopyRecoveryPhrase()
|
||||||
|
}}
|
||||||
|
className="w-full py-2 px-4 bg-cyber-light border border-neon-cyan/30 hover:border-neon-cyan/50 hover:bg-cyber-dark text-cyber-accent hover:text-neon-cyan rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{copiedRecoveryPhrase ? '✓ Copied!' : 'Copy Recovery Words'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-neon-blue/10 border border-neon-blue/30 rounded-lg p-4">
|
||||||
|
<p className="text-neon-blue font-semibold mb-2">Your new public key (npub)</p>
|
||||||
|
<p className="text-neon-cyan text-sm font-mono break-all">{newNpub}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setRecoveryPhrase(null)
|
||||||
|
setNewNpub(null)
|
||||||
|
void loadKeys()
|
||||||
|
}}
|
||||||
|
className="w-full py-2 px-4 bg-neon-cyan/20 hover:bg-neon-cyan/30 text-neon-cyan rounded-lg font-medium transition-all border border-neon-cyan/50 hover:shadow-glow-cyan"
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
124
features/key-management-configuration.md
Normal file
124
features/key-management-configuration.md
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# Key Management Configuration Page
|
||||||
|
|
||||||
|
**Auteur** : Équipe 4NK
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Créer une page de configuration permettant de :
|
||||||
|
- Consulter les clés publiques (npub et hex)
|
||||||
|
- Importer une clé privée sous forme d'URL (nostr://nsec1...) ou de texte (nsec1... ou hex)
|
||||||
|
- Respecter le système de stockage et de sécurisation à deux niveaux existant
|
||||||
|
|
||||||
|
## Impacts
|
||||||
|
|
||||||
|
### Utilisateurs
|
||||||
|
- Interface dédiée pour gérer les clés Nostr
|
||||||
|
- Consultation facile des clés publiques
|
||||||
|
- Import de clés via URL ou texte
|
||||||
|
- Affichage des mots de récupération après import
|
||||||
|
|
||||||
|
### Technique
|
||||||
|
- Nouveau composant `KeyManagementManager` dans `components/`
|
||||||
|
- Intégration dans la page `/settings`
|
||||||
|
- Utilisation des services existants (`nostrAuthService`, `keyManagementService`)
|
||||||
|
- Respect du système de chiffrement à deux niveaux (KEK + phrase de récupération)
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
### Fichiers créés
|
||||||
|
- `components/KeyManagementManager.tsx` : Composant de gestion des clés
|
||||||
|
- Affichage des clés publiques (npub et hex)
|
||||||
|
- Formulaire d'import de clé privée
|
||||||
|
- Support des formats : nsec URL (nostr://nsec1...), nsec texte (nsec1...), hex (64 caractères)
|
||||||
|
- Extraction automatique de la clé depuis une URL
|
||||||
|
- Avertissement et confirmation avant remplacement d'un compte existant
|
||||||
|
- Affichage des mots de récupération après import
|
||||||
|
|
||||||
|
### Fichiers modifiés
|
||||||
|
- `pages/settings.tsx` : Ajout du composant `KeyManagementManager` dans la page settings
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
### Consultation des clés publiques
|
||||||
|
- Affichage du npub (format NIP-19)
|
||||||
|
- Affichage de la clé publique hexadécimale
|
||||||
|
- Boutons de copie pour chaque format
|
||||||
|
|
||||||
|
### Import de clé privée
|
||||||
|
- Support de plusieurs formats :
|
||||||
|
- URL nostr:// avec nsec : `nostr://nsec1...`
|
||||||
|
- Texte nsec : `nsec1...`
|
||||||
|
- Clé hexadécimale : `64 caractères hex`
|
||||||
|
- Extraction automatique de la clé depuis une URL
|
||||||
|
- Validation du format avant import
|
||||||
|
- Gestion du remplacement d'un compte existant :
|
||||||
|
- Avertissement si un compte existe déjà
|
||||||
|
- Confirmation avant remplacement
|
||||||
|
- Suppression de l'ancien compte avant création du nouveau
|
||||||
|
|
||||||
|
### Sécurité
|
||||||
|
- Respect du système de chiffrement à deux niveaux :
|
||||||
|
- KEK (Key Encryption Key) chiffré avec la phrase de récupération
|
||||||
|
- Clé privée chiffrée avec le KEK
|
||||||
|
- KEK stocké dans Credentials API
|
||||||
|
- Clé privée chiffrée stockée dans IndexedDB
|
||||||
|
- Affichage unique des mots de récupération après import
|
||||||
|
- Avertissements clairs sur l'importance de sauvegarder la phrase de récupération
|
||||||
|
|
||||||
|
## Modalités de déploiement
|
||||||
|
|
||||||
|
1. Vérifier que le composant compile sans erreur :
|
||||||
|
```bash
|
||||||
|
npm run type-check
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Tester l'interface :
|
||||||
|
- Accéder à `/settings`
|
||||||
|
- Vérifier l'affichage des clés publiques si un compte existe
|
||||||
|
- Tester l'import d'une clé privée (nsec ou hex)
|
||||||
|
- Vérifier l'affichage des mots de récupération
|
||||||
|
- Tester le remplacement d'un compte existant
|
||||||
|
|
||||||
|
3. Déployer avec le script de déploiement standard
|
||||||
|
|
||||||
|
## Modalités d'analyse
|
||||||
|
|
||||||
|
### Logs à surveiller
|
||||||
|
- Erreurs lors du chargement des clés publiques
|
||||||
|
- Erreurs lors de l'import de clé privée
|
||||||
|
- Erreurs lors de la validation du format de clé
|
||||||
|
- Erreurs lors du remplacement d'un compte
|
||||||
|
|
||||||
|
### Points de contrôle
|
||||||
|
- Affichage correct des clés publiques (npub et hex)
|
||||||
|
- Extraction correcte de la clé depuis une URL
|
||||||
|
- Validation correcte des formats (nsec, hex)
|
||||||
|
- Gestion correcte du remplacement de compte
|
||||||
|
- Affichage correct des mots de récupération
|
||||||
|
- Copie correcte des clés et mots de récupération
|
||||||
|
|
||||||
|
### Tests à effectuer
|
||||||
|
1. **Consultation des clés publiques** :
|
||||||
|
- Vérifier l'affichage du npub
|
||||||
|
- Vérifier l'affichage de la clé hex
|
||||||
|
- Tester la copie de chaque format
|
||||||
|
|
||||||
|
2. **Import de clé privée** :
|
||||||
|
- Tester l'import avec une URL nostr://
|
||||||
|
- Tester l'import avec un nsec texte
|
||||||
|
- Tester l'import avec une clé hex
|
||||||
|
- Vérifier la validation des formats invalides
|
||||||
|
|
||||||
|
3. **Remplacement de compte** :
|
||||||
|
- Créer un compte
|
||||||
|
- Importer une nouvelle clé
|
||||||
|
- Vérifier l'avertissement et la confirmation
|
||||||
|
- Vérifier que l'ancien compte est supprimé
|
||||||
|
- Vérifier que le nouveau compte est créé
|
||||||
|
|
||||||
|
4. **Sécurité** :
|
||||||
|
- Vérifier que les mots de récupération sont affichés une seule fois
|
||||||
|
- Vérifier que la clé privée n'est jamais affichée
|
||||||
|
- Vérifier que le système de chiffrement à deux niveaux est respecté
|
||||||
|
|
||||||
@ -2,6 +2,7 @@ import Head from 'next/head'
|
|||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
import { Nip95ConfigManager } from '@/components/Nip95ConfigManager'
|
import { Nip95ConfigManager } from '@/components/Nip95ConfigManager'
|
||||||
|
import { KeyManagementManager } from '@/components/KeyManagementManager'
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
@ -16,7 +17,10 @@ export default function SettingsPage() {
|
|||||||
<PageHeader />
|
<PageHeader />
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold text-neon-cyan mb-8">Settings</h1>
|
<h1 className="text-3xl font-bold text-neon-cyan mb-8">Settings</h1>
|
||||||
<Nip95ConfigManager />
|
<div className="space-y-8">
|
||||||
|
<KeyManagementManager />
|
||||||
|
<Nip95ConfigManager />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user