feat: Implement comprehensive Nostr identity and social features

## Core Identity Management
- Add secure key generation and import functionality
- Implement AES-GCM encryption with PBKDF2 key derivation
- Create password-protected identity storage
- Add browser-compatible crypto utilities (no Buffer dependency)

## User Interface
- Build identity management dialog with tabs for setup and profile
- Add navbar integration with user dropdown and mobile support
- Create password unlock dialog for encrypted identities
- Integrate vue-sonner for toast notifications

## Nostr Protocol Integration
- Implement event creation (notes, reactions, profiles, contacts)
- Add reply thread detection and engagement metrics
- Create social interaction composables for publishing
- Support multi-relay publishing with failure handling
- Add profile fetching and caching system

## Security Features
- Web Crypto API with 100k PBKDF2 iterations
- Secure random salt and IV generation
- Automatic password prompts for encrypted storage
- Legacy support for unencrypted identities

## Technical Improvements
- Replace all Buffer usage with browser-native APIs
- Add comprehensive error handling and validation
- Implement reactive state management with Vue composables
- Create reusable crypto utility functions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2025-07-02 16:25:20 +02:00
parent d3e8b23c86
commit ee7eb461c4
12 changed files with 1777 additions and 21 deletions

210
src/lib/nostr/identity.ts Normal file
View file

@ -0,0 +1,210 @@
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'
import { SecureStorage, type EncryptedData } from '@/lib/crypto/encryption'
import { bytesToHex, hexToBytes } from '@/lib/utils/crypto'
export interface NostrIdentity {
privateKey: string
publicKey: string
npub: string
nsec: string
}
export interface NostrProfile {
name?: string
display_name?: string
about?: string
picture?: string
banner?: string
website?: string
nip05?: string
lud16?: string // Lightning address
}
export class IdentityManager {
private static readonly STORAGE_KEY = 'nostr_identity'
private static readonly PROFILE_KEY = 'nostr_profile'
/**
* Generate a new Nostr identity
*/
static generateIdentity(): NostrIdentity {
const privateKey = generateSecretKey()
const publicKey = getPublicKey(privateKey)
return {
privateKey: bytesToHex(privateKey),
publicKey,
npub: nip19.npubEncode(publicKey),
nsec: nip19.nsecEncode(privateKey)
}
}
/**
* Import identity from private key (hex or nsec format)
*/
static importIdentity(privateKeyInput: string): NostrIdentity {
let privateKeyBytes: Uint8Array
try {
// Try to decode as nsec first
if (privateKeyInput.startsWith('nsec')) {
const decoded = nip19.decode(privateKeyInput)
if (decoded.type === 'nsec') {
privateKeyBytes = decoded.data
} else {
throw new Error('Invalid nsec format')
}
} else {
// Try as hex string
if (privateKeyInput.length !== 64) {
throw new Error('Private key must be 64 hex characters')
}
privateKeyBytes = hexToBytes(privateKeyInput)
}
if (privateKeyBytes.length !== 32) {
throw new Error('Private key must be 32 bytes')
}
const publicKey = getPublicKey(privateKeyBytes)
return {
privateKey: bytesToHex(privateKeyBytes),
publicKey,
npub: nip19.npubEncode(publicKey),
nsec: nip19.nsecEncode(privateKeyBytes)
}
} catch (error) {
throw new Error(`Failed to import identity: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Save identity to localStorage (encrypted)
*/
static async saveIdentity(identity: NostrIdentity, password?: string): Promise<void> {
try {
let dataToStore: any = identity
if (password) {
if (!SecureStorage.isSupported()) {
throw new Error('Secure encryption is not supported in this browser')
}
// Encrypt sensitive data using Web Crypto API
const sensitiveData = JSON.stringify({
privateKey: identity.privateKey,
nsec: identity.nsec
})
const encryptedData = await SecureStorage.encrypt(sensitiveData, password)
dataToStore = {
publicKey: identity.publicKey,
npub: identity.npub,
encrypted: true,
encryptedData
}
}
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(dataToStore))
} catch (error) {
throw new Error(`Failed to save identity: ${error instanceof Error ? error.message : 'Unknown error'}`)
}
}
/**
* Load identity from localStorage
*/
static async loadIdentity(password?: string): Promise<NostrIdentity | null> {
try {
const stored = localStorage.getItem(this.STORAGE_KEY)
if (!stored) return null
const storedData = JSON.parse(stored)
if (storedData.encrypted && password) {
if (!SecureStorage.isSupported()) {
throw new Error('Secure encryption is not supported in this browser')
}
// Decrypt sensitive data
const decryptedSensitiveData = await SecureStorage.decrypt(storedData.encryptedData, password)
const { privateKey, nsec } = JSON.parse(decryptedSensitiveData)
return {
privateKey,
publicKey: storedData.publicKey,
npub: storedData.npub,
nsec
}
} else if (storedData.encrypted && !password) {
throw new Error('Password required to decrypt stored identity')
}
// Non-encrypted identity (legacy support)
const identity = storedData as NostrIdentity
// Validate the loaded identity
if (!identity.privateKey || !identity.publicKey) {
throw new Error('Invalid stored identity')
}
return identity
} catch (error) {
console.error('Failed to load identity:', error)
throw error
}
}
/**
* Clear stored identity
*/
static clearIdentity(): void {
localStorage.removeItem(this.STORAGE_KEY)
localStorage.removeItem(this.PROFILE_KEY)
}
/**
* Check if identity exists in storage
*/
static hasStoredIdentity(): boolean {
return !!localStorage.getItem(this.STORAGE_KEY)
}
/**
* Check if stored identity is encrypted
*/
static isStoredIdentityEncrypted(): boolean {
try {
const stored = localStorage.getItem(this.STORAGE_KEY)
if (!stored) return false
const storedData = JSON.parse(stored)
return !!storedData.encrypted
} catch {
return false
}
}
/**
* Save user profile
*/
static saveProfile(profile: NostrProfile): void {
localStorage.setItem(this.PROFILE_KEY, JSON.stringify(profile))
}
/**
* Load user profile
*/
static loadProfile(): NostrProfile | null {
try {
const stored = localStorage.getItem(this.PROFILE_KEY)
return stored ? JSON.parse(stored) : null
} catch (error) {
console.error('Failed to load profile:', error)
return null
}
}
}