--- description: Nostr keypair authentication for the Syntropy Institute portal (auth only, no relays) match: - nostr - auth - login - keypair - sign - challenge --- # Nostr Auth Skill ## When to Use When working with authentication flows, keypair management, or NIP-98 API auth in the portal. ## Key Principle Nostr is used ONLY for cryptographic authentication. No relay connections. No event publishing. Just keypairs and signatures. ## Keypair Generation (Easy Mode) ```typescript import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools'; // Generate new identity const secretKey = generateSecretKey(); // Uint8Array const publicKey = getPublicKey(secretKey); // hex string // Encode for display (only when user requests it) const npub = nip19.npubEncode(publicKey); const nsec = nip19.nsecEncode(secretKey); ``` ## Private Key Encryption (for localStorage) ```typescript // Encrypt private key with user's passphrase before storing async function encryptPrivateKey(secretKey: Uint8Array, passphrase: string): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(passphrase), 'PBKDF2', false, ['deriveKey'] ); const salt = crypto.getRandomValues(new Uint8Array(16)); const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt'] ); const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, derivedKey, secretKey ); // Return salt + iv + ciphertext as base64 const combined = new Uint8Array([...salt, ...iv, ...new Uint8Array(encrypted)]); return btoa(String.fromCharCode(...combined)); } ``` ## NIP-07 Detection (Nostr Native) ```typescript // Check for browser extension function hasNostrExtension(): boolean { return typeof window !== 'undefined' && 'nostr' in window; } // Sign with extension async function signWithExtension(event: object): Promise { return await (window as any).nostr.signEvent(event); } // Get public key from extension async function getExtensionPubkey(): Promise { return await (window as any).nostr.getPublicKey(); } ``` ## Challenge-Response Auth (NIP-98 style) ```typescript import { finalizeEvent, verifyEvent } from 'nostr-tools'; // Client: Sign auth challenge function createAuthEvent(secretKey: Uint8Array, url: string, method: string) { const event = finalizeEvent({ kind: 27235, // NIP-98 HTTP Auth created_at: Math.floor(Date.now() / 1000), tags: [ ['u', url], ['method', method], ], content: '', }, secretKey); return event; } // Send as Authorization header: // Authorization: Nostr // Server: Verify auth event function verifyAuthEvent(event: any, expectedUrl: string, expectedMethod: string): boolean { if (!verifyEvent(event)) return false; if (event.kind !== 27235) return false; const urlTag = event.tags.find((t: string[]) => t[0] === 'u'); const methodTag = event.tags.find((t: string[]) => t[0] === 'method'); if (urlTag?.[1] !== expectedUrl) return false; if (methodTag?.[1] !== expectedMethod) return false; // Check timestamp is within 60 seconds if (Math.abs(Date.now() / 1000 - event.created_at) > 60) return false; return true; } ``` ## Rules - NEVER store private keys on the server - NEVER connect to any Nostr relay - NEVER transmit private keys over the network - ALWAYS encrypt private keys before storing in localStorage - Use PBKDF2 with at least 600,000 iterations for key derivation - Auth events expire after 60 seconds - Server only stores npub (public key)