feat: descrypt event in client

This commit is contained in:
complex 2025-04-09 18:01:12 +02:00
parent fe233ee417
commit 75cec4f063

@ -41,12 +41,400 @@ const NostrLogin = typeof require !== 'undefined' ? require('nostr-login') : nul
declare global {
interface Window {
currentSignedEvent?: NostrEvent;
nostrTools: Record<string, unknown>;
nostr: {
getPublicKey: () => Promise<string>;
signEvent: (event: NostrEvent) => Promise<NostrEvent>;
nip44?: {
encrypt(pubkey: string, plaintext: string): Promise<string>;
decrypt(pubkey: string, ciphertext: string): Promise<string>;
};
};
}
}
// Track active response subscriptions
const activeResponseSubscriptions = new Map<string, () => void>();
// Store received events for decryption
const receivedEvents = new Map<string, {
id: string;
event: NostrEvent;
receivedAt: number;
decrypted: boolean;
decryptedContent?: string;
}>();
// Helper function for Web Crypto API decryption
async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promise<string> {
try {
// Convert base64 to byte array
const encryptedBytes = new Uint8Array(
atob(encryptedBase64)
.split('')
.map(char => char.charCodeAt(0))
);
// Extract IV (first 12 bytes)
const iv = encryptedBytes.slice(0, 12);
// Extract ciphertext (remaining bytes)
const ciphertext = encryptedBytes.slice(12);
// Create key material from the decryption key
const keyMaterial = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(key)
);
// Import the key for AES-GCM decryption
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['decrypt']
);
// Decrypt the data
const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
cryptoKey,
ciphertext
);
// Convert decrypted data to string
return new TextDecoder().decode(decryptedData);
} catch (error) {
throw new Error(`WebCrypto decryption failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Auto-decrypt event using NIP-44
async function decryptEvent(eventId: string): Promise<void> {
const receivedEvent = receivedEvents.get(eventId);
if (!receivedEvent || receivedEvent.decrypted) {
return;
}
try {
const event = receivedEvent.event;
let decryptedContent: string;
// Look for a "key" tag in the event
const keyTag = event.tags.find(tag => tag[0] === 'key');
if (!keyTag || keyTag.length < 2) {
decryptedContent = `[No key tag found for decryption]\n${event.content}`;
} else {
try {
// Extract the encrypted key from the tag
const encryptedKey = keyTag[1];
// Check if window.nostr and nip44.decrypt are available
if (!window.nostr) {
throw new Error("window.nostr is not available - ensure a NIP-07 extension is installed");
}
if (!window.nostr.nip44 || !window.nostr.nip44.decrypt) {
console.warn("NIP-44 decryption not available - trying to connect to a compatible extension");
// Log diagnostic information
console.log("Available nostr methods:", Object.keys(window.nostr).join(", "));
// Check if we can trigger a connection via NostrLogin
if (typeof window.nostr.getPublicKey === 'function') {
try {
await window.nostr.getPublicKey();
// Check again after getting public key
if (typeof window.nostr.nip44 === 'object' &&
typeof window.nostr.nip44.decrypt === 'function') {
console.log("NIP-44 is now available after connecting");
} else {
throw new Error("NIP-44 decryption is still not available after connecting");
}
} catch (e) {
throw new Error(`Failed to connect to extension: ${e}`);
}
} else {
throw new Error("window.nostr.nip44.decrypt is not available");
}
}
// Use window.nostr to decrypt the key tag with NIP-44
console.log("Using window.nostr.nip44.decrypt for key decryption");
// Note that according to the NIP-07 spec, the first parameter is the pubkey (sender)
// and the second parameter is the ciphertext
// Declare decryptedKey outside the try block so it's available in the outer scope
let decryptedKey: string;
try {
// Log the actual values being passed to help debug
console.log(`Attempting to decrypt with pubkey: ${event.pubkey.substring(0, 8)}... and encrypted key: ${encryptedKey.substring(0, 10)}...`);
decryptedKey = await window.nostr.nip44.decrypt(
event.pubkey, // The pubkey that encrypted the key
encryptedKey // The encrypted key from the "key" tag
);
console.log("Decryption successful, decrypted key:", decryptedKey.substring(0, 5) + "...");
} catch (decryptKeyError) {
console.error("Error in direct decryption call:", decryptKeyError);
throw decryptKeyError; // Re-throw to be caught by the outer catch block
}
console.log("Key decryption successful");
// Now use the decrypted key to decrypt the content using Web Crypto API
try {
// Decrypt the content using Web Crypto API and the decrypted key
const decryptedEventContent = await decryptWithWebCrypto(
event.content,
decryptedKey
);
// The decrypted content is the direct HTTP request/response
// No need to try parsing as JSON anymore
console.log("Successfully decrypted HTTP content");
decryptedContent = decryptedEventContent;
} catch (contentDecryptError) {
console.error("Content decryption failed:", contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError));
decryptedContent = `Key decryption successful, but content decryption failed: ${contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError)}
Encrypted content: ${event.content.substring(0, 50)}...
Decrypted key: ${decryptedKey}`;
}
} catch (decryptError) {
console.error("Failed to decrypt with window.nostr.nip44:", decryptError instanceof Error ? decryptError.message : String(decryptError));
decryptedContent = `[NIP-44 decryption failed: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}]\n${event.content}`;
}
}
// Update the event
receivedEvent.decrypted = true;
receivedEvent.decryptedContent = decryptedContent;
receivedEvents.set(eventId, receivedEvent);
// Update UI if this event is currently being viewed
const eventDetails = document.getElementById('eventDetails');
if (eventDetails) {
const selectedEventId = eventDetails.querySelector('h3')?.textContent?.match(/(.+)\.\.\./)?.[1];
if (selectedEventId && `${selectedEventId}...` === `${eventId.substring(0, 8)}...`) {
// If we have a showEventDetails function, call it
if (typeof showEventDetails === 'function') {
showEventDetails(eventId);
}
}
}
} catch (error) {
console.error("Failed to process event:", error);
}
}
// Function to show event details (replicated from receiver.ts)
function showEventDetails(eventId: string): void {
const eventDetails = document.getElementById('eventDetails');
if (!eventDetails) {
return;
}
const receivedEvent = receivedEvents.get(eventId);
if (!receivedEvent) {
return;
}
const event = receivedEvent.event;
// Ensure event has an ID (should already be verified)
const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
const fullEventId = event.id || 'unknown';
// Generate tags HTML with better formatting
let tagsHtml = '';
event.tags.forEach((tag: string[]) => {
if (tag.length >= 2) {
// For p and e tags, add additional explanation
if (tag[0] === 'p') {
tagsHtml += `<li><strong>p:</strong> ${tag[1]} (p-tag: recipient/target)</li>`;
} else if (tag[0] === 'e') {
tagsHtml += `<li><strong>e:</strong> ${tag[1]} (e-tag: reference to another event)</li>`;
} else {
tagsHtml += `<li><strong>${tag[0]}:</strong> ${tag[1]}</li>`;
}
} else if (tag.length === 1) {
tagsHtml += `<li><strong>${tag[0]}</strong></li>`;
}
});
// Determine if this is a request or response
const isRequest = event.tags.some(tag => tag[0] === 'p');
const isResponse = event.tags.some(tag => tag[0] === 'e');
const eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
// Get the decrypted or original content
const httpContent = receivedEvent.decrypted ?
(receivedEvent.decryptedContent || event.content) :
event.content;
// Display the event details
eventDetails.innerHTML = `
<div class="event-detail-header">
<h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
<div class="event-timestamp">${new Date(event.created_at * 1000).toLocaleString()}</div>
</div>
<div class="event-detail-metadata">
<div class="event-detail-item">
<strong>ID:</strong> <span class="metadata-value">${fullEventId}</span>
</div>
<div class="event-detail-item">
<strong>From:</strong> <span class="metadata-value">${event.pubkey}</span>
</div>
<div class="event-detail-item">
<strong>Created:</strong> <span class="metadata-value">${new Date(event.created_at * 1000).toLocaleString()}</span>
</div>
<div class="event-detail-item">
<strong>Tags:</strong>
<ul class="event-tags">${tagsHtml}</ul>
</div>
</div>
<div class="event-detail-content">
<div class="content-row">
<div class="event-metadata-column">
<div class="content-header">
<strong>Event Metadata</strong>
</div>
<div class="metadata-content">
<div>ID: ${fullEventId}</div>
<div>Pubkey: ${event.pubkey}</div>
<div>Created: ${new Date(event.created_at * 1000).toLocaleString()}</div>
${receivedEvent.decrypted ? '<div class="decrypted-badge">Decrypted</div>' : ''}
</div>
</div>
<div class="http-content-column">
<div class="content-header">
<strong>HTTP ${eventTypeLabel}</strong>
</div>
<pre class="http-content">${httpContent}</pre>
</div>
</div>
</div>
`;
}
// Function to process an incoming event
function processEvent(event: NostrEvent): void {
// Ensure event has an ID
if (!event.id) {
console.error('Received event with no ID, skipping');
return;
}
// Check if this is a new event
if (receivedEvents.has(event.id)) {
return;
}
// Store the event
const receivedEvent = {
id: event.id,
event: event,
receivedAt: Date.now(),
decrypted: false
};
receivedEvents.set(event.id, receivedEvent);
// Add event to UI
addEventToUI(receivedEvent);
}
// Function to add event to UI
function addEventToUI(receivedEvent: {
id: string;
event: NostrEvent;
receivedAt: number;
decrypted: boolean;
decryptedContent?: string;
}): void {
const eventsList = document.getElementById('eventsList');
if (!eventsList) {
return;
}
const event = receivedEvent.event;
// Ensure the event has an ID (should already be checked by processEvent)
if (!event.id) {
console.error('Event has no ID, cannot add to UI');
return;
}
// Create event item with improved styling
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
eventItem.dataset.id = event.id;
// Get event ID for display
const eventIdForDisplay = event.id.substring(0, 8);
// Determine if it's a request or response
const hasP = event.tags.some(tag => tag[0] === 'p');
const hasE = event.tags.some(tag => tag[0] === 'e');
const eventType = hasP ? 'HTTP Request' : (hasE ? 'HTTP Response' : 'Unknown');
// Find recipient if available
let recipient = '';
const pTag = event.tags.find(tag => tag[0] === 'p');
if (pTag && pTag.length > 1) {
recipient = `| To: ${pTag[1].substring(0, 8)}...`;
}
// Format the event item
eventItem.innerHTML = `
<div class="event-header">
<span class="event-id">${eventIdForDisplay}...</span>
<span class="event-time">${new Date(event.created_at * 1000).toLocaleTimeString()}</span>
</div>
<div class="event-summary">
<span class="event-type">${eventType}</span> | From: ${event.pubkey.substring(0, 8)}... ${recipient}
</div>
<div class="event-actions">
<button class="view-details-btn">View Details</button>
</div>
`;
// Add event listeners
const viewDetailsBtn = eventItem.querySelector('.view-details-btn');
if (viewDetailsBtn) {
viewDetailsBtn.addEventListener('click', () => {
showEventDetails(event.id as string);
// Also trigger decryption when viewing details
decryptEvent(event.id as string);
});
}
// Auto-decrypt the event when it's added to the UI
// Use timeout for better UI responsiveness
setTimeout(() => {
decryptEvent(event.id as string);
}, 100);
// Add to list
eventsList.appendChild(eventItem);
// Clear empty state if present
const emptyState = eventsList.querySelector('.empty-state');
if (emptyState) {
emptyState.remove();
}
}
// Add a new function to update the UI for waiting for a response
function updateUIForWaitingResponse(eventId: string, isWaiting: boolean) {
// Find the event element in the UI