From 7d1e22e4572003a9764b5513b8a6294427da5a54 Mon Sep 17 00:00:00 2001 From: Christomatt Date: Tue, 27 Jan 2026 06:18:20 +0100 Subject: [PATCH] feat(chat): Enhance message decryption with improved key management and debug logging - Add recipient public key fetching in messages API endpoint for sent message decryption - Include `isE2E` flag in message response to indicate end-to-end encryption status - Simplify encrypted content handling by always returning `encryptedContent` field - Add comprehensive debug logging throughout message loading pipeline for troubleshooting - Improve key resolution logic to fetch recipient keys when not available in conversation data - Add detailed logging for each message processing step including key availability and decryption status - Enhance error handling with more descriptive error messages in key fetching operations - Better distinguish between sent and received message decryption requirements with clear comments --- src/app/api/swarm/chat/messages/route.ts | 34 ++++++++++++++++-------- src/app/chat/page.tsx | 31 ++++++++++++++++++--- src/lib/hooks/useChatEncryption.ts | 16 ++++++++++- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/app/api/swarm/chat/messages/route.ts b/src/app/api/swarm/chat/messages/route.ts index ac5bc3b..c2bdeb4 100644 --- a/src/app/api/swarm/chat/messages/route.ts +++ b/src/app/api/swarm/chat/messages/route.ts @@ -65,26 +65,38 @@ export async function GET(request: NextRequest) { limit, }); - // Decrypt messages - // Note: In production, you'd decrypt the private key first using a user password/session key - // For now, we'll return encrypted content and decrypt client-side + // Get recipient info for sent messages + const recipientHandle = conversation.participant2Handle; + let recipientPublicKey: string | null = null; + + console.log('[Messages API] Fetching recipient key for:', recipientHandle); + + // Fetch recipient's chat public key + const recipientUser = await db.query.users.findFirst({ + where: eq(users.handle, recipientHandle), + }); + recipientPublicKey = recipientUser?.chatPublicKey || null; + + console.log('[Messages API] Recipient public key found:', !!recipientPublicKey); + const messagesWithDecryption = messages.map((msg) => { const isSentByMe = msg.senderHandle === session.user.handle; - // Return the appropriate encrypted content: - // - For sent messages: use senderEncryptedContent (encrypted with sender's key) - // - For received messages: use encryptedContent (encrypted with recipient's key) - const encryptedForViewer = isSentByMe - ? (msg.senderEncryptedContent || msg.encryptedContent) - : msg.encryptedContent; + const senderPubKey = isSentByMe ? recipientPublicKey : msg.senderChatPublicKey; + + console.log('[Messages API] Message:', msg.id, 'isSentByMe:', isSentByMe, 'senderPubKey:', !!senderPubKey, 'msgSenderChatPubKey:', !!msg.senderChatPublicKey); return { id: msg.id, senderHandle: msg.senderHandle, senderDisplayName: msg.senderDisplayName, senderAvatarUrl: msg.senderAvatarUrl, - senderPublicKey: msg.senderChatPublicKey, // For E2E decryption - encryptedContent: encryptedForViewer, + // For decryption: + // - Sent messages: need recipient's public key + // - Received messages: need sender's public key + senderPublicKey: senderPubKey, + isE2E: !!msg.senderChatPublicKey || (isSentByMe && !!recipientPublicKey), + encryptedContent: msg.encryptedContent, deliveredAt: msg.deliveredAt, readAt: msg.readAt, createdAt: msg.createdAt, diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 4afbb16..8c96260 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -60,30 +60,47 @@ export default function ChatPage() { const fetchRecipientKey = async (handle: string) => { try { const res = await fetch(`/api/users/${encodeURIComponent(handle)}`); const data = await res.json(); setRecipientPublicKey(data.user?.chatPublicKey || null); } catch { setRecipientPublicKey(null); } }; const loadConversations = async () => { try { const res = await fetch('/api/swarm/chat/conversations'); const data = await res.json(); setConversations(data.conversations || []); } catch { } finally { setLoading(false); } }; const loadMessages = async (conversationId: string) => { + console.log('[LoadMessages] Starting load for conversation:', conversationId); try { // Ensure we have the recipient's key before trying to decrypt let chatPartnerKey = selectedConversation?.participant2?.chatPublicKey || recipientPublicKey; + console.log('[LoadMessages] Initial chatPartnerKey:', !!chatPartnerKey); + // If we don't have the key yet, fetch it if (!chatPartnerKey && selectedConversation?.participant2?.handle) { + console.log('[LoadMessages] Fetching recipient key for:', selectedConversation.participant2.handle); try { const userRes = await fetch(`/api/users/${encodeURIComponent(selectedConversation.participant2.handle)}`); const userData = await userRes.json(); chatPartnerKey = userData.user?.chatPublicKey || null; + console.log('[LoadMessages] Fetched chatPartnerKey:', !!chatPartnerKey); if (chatPartnerKey) setRecipientPublicKey(chatPartnerKey); - } catch {} + } catch (e) { + console.error('[LoadMessages] Failed to fetch recipient key:', e); + } } const res = await fetch(`/api/swarm/chat/messages?conversationId=${conversationId}`); const data = await res.json(); - const decrypted = await Promise.all((data.messages || []).map(async (msg: Message) => { + console.log('[LoadMessages] Received messages:', data.messages?.length || 0); + + const decrypted = await Promise.all((data.messages || []).map(async (msg: Message & { isE2E?: boolean }, idx: number) => { + console.log(`[LoadMessages] Processing message ${idx}:`, { + id: msg.id, + isSentByMe: msg.isSentByMe, + hasSenderPublicKey: !!msg.senderPublicKey, + isE2EFlag: msg.isE2E + }); + try { // Check if this is an E2E encrypted message (has senderPublicKey) // or a legacy RSA message (no senderPublicKey) const isE2E = !!msg.senderPublicKey; if (!isE2E) { + console.log(`[LoadMessages] Message ${idx} is legacy RSA`); // Legacy RSA message - can't decrypt client-side return { ...msg, decryptedContent: '[Legacy encrypted message]' }; } @@ -93,23 +110,29 @@ export default function ChatPage() { // For messages I received: use sender's public key const otherPartyKey = msg.isSentByMe ? chatPartnerKey : msg.senderPublicKey; + console.log(`[LoadMessages] Message ${idx} otherPartyKey:`, !!otherPartyKey, 'isSentByMe:', msg.isSentByMe); + if (!otherPartyKey) { console.warn('Missing key for decryption. isSentByMe:', msg.isSentByMe, 'chatPartnerKey:', !!chatPartnerKey); return { ...msg, decryptedContent: '[Missing decryption key]' }; } if (msg.encryptedContent) { + console.log(`[LoadMessages] Calling decryptMessage for message ${idx}`); const decrypted = await decryptMessage(msg.encryptedContent, otherPartyKey); + console.log(`[LoadMessages] Message ${idx} decrypted successfully`); return { ...msg, decryptedContent: decrypted }; } } catch (err) { - console.error('Decrypt error:', err, 'msg:', msg.id, 'isSentByMe:', msg.isSentByMe); + console.error(`[LoadMessages] Decrypt error for message ${idx}:`, err, 'msg:', msg.id, 'isSentByMe:', msg.isSentByMe); } return { ...msg, decryptedContent: '[Unable to decrypt]' }; })); + + console.log('[LoadMessages] Setting messages:', decrypted.length); setMessages(decrypted); } catch (err) { - console.error('Load messages error:', err); + console.error('[LoadMessages] Load messages error:', err); } }; const markAsRead = async (conversationId: string) => { try { await fetch('/api/swarm/chat/messages', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversationId }) }); setConversations(prev => prev.map(c => c.id === conversationId ? { ...c, unreadCount: 0 } : c)); } catch { } }; diff --git a/src/lib/hooks/useChatEncryption.ts b/src/lib/hooks/useChatEncryption.ts index 5eb413f..8b416a7 100644 --- a/src/lib/hooks/useChatEncryption.ts +++ b/src/lib/hooks/useChatEncryption.ts @@ -172,9 +172,16 @@ export function useChatEncryption() { senderPublicKey: string ): Promise => { if (!keys?.privateKey) { + console.error('[Decrypt] No private key available'); throw new Error('No chat keys available'); } + console.log('[Decrypt] Starting decryption', { + hasPrivateKey: !!keys.privateKey, + hasSenderPublicKey: !!senderPublicKey, + encryptedLength: encryptedMessage.length + }); + const myPrivateKey = await importPrivateKey(keys.privateKey); const theirPublicKey = await importPublicKey(senderPublicKey); const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey); @@ -183,6 +190,11 @@ export function useChatEncryption() { const iv = combined.slice(0, 12); const ciphertext = combined.slice(12); + console.log('[Decrypt] Decrypting with shared key', { + ivLength: iv.byteLength, + ciphertextLength: ciphertext.byteLength + }); + const decrypted = await window.crypto.subtle.decrypt( { name: 'AES-GCM', iv }, sharedKey, @@ -190,7 +202,9 @@ export function useChatEncryption() { ); const decoder = new TextDecoder(); - return decoder.decode(decrypted); + const result = decoder.decode(decrypted); + console.log('[Decrypt] Success:', result.substring(0, 50)); + return result; }, [keys]); // Clear keys (on logout)