feat: descrypt event in client
This commit is contained in:
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user