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
This commit is contained in:
Christomatt
2026-01-27 06:18:20 +01:00
parent 863cbc8dce
commit 7d1e22e457
3 changed files with 65 additions and 16 deletions
+23 -11
View File
@@ -65,26 +65,38 @@ export async function GET(request: NextRequest) {
limit, limit,
}); });
// Decrypt messages // Get recipient info for sent messages
// Note: In production, you'd decrypt the private key first using a user password/session key const recipientHandle = conversation.participant2Handle;
// For now, we'll return encrypted content and decrypt client-side 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 messagesWithDecryption = messages.map((msg) => {
const isSentByMe = msg.senderHandle === session.user.handle; const isSentByMe = msg.senderHandle === session.user.handle;
// Return the appropriate encrypted content: const senderPubKey = isSentByMe ? recipientPublicKey : msg.senderChatPublicKey;
// - For sent messages: use senderEncryptedContent (encrypted with sender's key)
// - For received messages: use encryptedContent (encrypted with recipient's key) console.log('[Messages API] Message:', msg.id, 'isSentByMe:', isSentByMe, 'senderPubKey:', !!senderPubKey, 'msgSenderChatPubKey:', !!msg.senderChatPublicKey);
const encryptedForViewer = isSentByMe
? (msg.senderEncryptedContent || msg.encryptedContent)
: msg.encryptedContent;
return { return {
id: msg.id, id: msg.id,
senderHandle: msg.senderHandle, senderHandle: msg.senderHandle,
senderDisplayName: msg.senderDisplayName, senderDisplayName: msg.senderDisplayName,
senderAvatarUrl: msg.senderAvatarUrl, senderAvatarUrl: msg.senderAvatarUrl,
senderPublicKey: msg.senderChatPublicKey, // For E2E decryption // For decryption:
encryptedContent: encryptedForViewer, // - 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, deliveredAt: msg.deliveredAt,
readAt: msg.readAt, readAt: msg.readAt,
createdAt: msg.createdAt, createdAt: msg.createdAt,
+27 -4
View File
@@ -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 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 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) => { const loadMessages = async (conversationId: string) => {
console.log('[LoadMessages] Starting load for conversation:', conversationId);
try { try {
// Ensure we have the recipient's key before trying to decrypt // Ensure we have the recipient's key before trying to decrypt
let chatPartnerKey = selectedConversation?.participant2?.chatPublicKey || recipientPublicKey; let chatPartnerKey = selectedConversation?.participant2?.chatPublicKey || recipientPublicKey;
console.log('[LoadMessages] Initial chatPartnerKey:', !!chatPartnerKey);
// If we don't have the key yet, fetch it // If we don't have the key yet, fetch it
if (!chatPartnerKey && selectedConversation?.participant2?.handle) { if (!chatPartnerKey && selectedConversation?.participant2?.handle) {
console.log('[LoadMessages] Fetching recipient key for:', selectedConversation.participant2.handle);
try { try {
const userRes = await fetch(`/api/users/${encodeURIComponent(selectedConversation.participant2.handle)}`); const userRes = await fetch(`/api/users/${encodeURIComponent(selectedConversation.participant2.handle)}`);
const userData = await userRes.json(); const userData = await userRes.json();
chatPartnerKey = userData.user?.chatPublicKey || null; chatPartnerKey = userData.user?.chatPublicKey || null;
console.log('[LoadMessages] Fetched chatPartnerKey:', !!chatPartnerKey);
if (chatPartnerKey) setRecipientPublicKey(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 res = await fetch(`/api/swarm/chat/messages?conversationId=${conversationId}`);
const data = await res.json(); 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 { try {
// Check if this is an E2E encrypted message (has senderPublicKey) // Check if this is an E2E encrypted message (has senderPublicKey)
// or a legacy RSA message (no senderPublicKey) // or a legacy RSA message (no senderPublicKey)
const isE2E = !!msg.senderPublicKey; const isE2E = !!msg.senderPublicKey;
if (!isE2E) { if (!isE2E) {
console.log(`[LoadMessages] Message ${idx} is legacy RSA`);
// Legacy RSA message - can't decrypt client-side // Legacy RSA message - can't decrypt client-side
return { ...msg, decryptedContent: '[Legacy encrypted message]' }; return { ...msg, decryptedContent: '[Legacy encrypted message]' };
} }
@@ -93,23 +110,29 @@ export default function ChatPage() {
// For messages I received: use sender's public key // For messages I received: use sender's public key
const otherPartyKey = msg.isSentByMe ? chatPartnerKey : msg.senderPublicKey; const otherPartyKey = msg.isSentByMe ? chatPartnerKey : msg.senderPublicKey;
console.log(`[LoadMessages] Message ${idx} otherPartyKey:`, !!otherPartyKey, 'isSentByMe:', msg.isSentByMe);
if (!otherPartyKey) { if (!otherPartyKey) {
console.warn('Missing key for decryption. isSentByMe:', msg.isSentByMe, 'chatPartnerKey:', !!chatPartnerKey); console.warn('Missing key for decryption. isSentByMe:', msg.isSentByMe, 'chatPartnerKey:', !!chatPartnerKey);
return { ...msg, decryptedContent: '[Missing decryption key]' }; return { ...msg, decryptedContent: '[Missing decryption key]' };
} }
if (msg.encryptedContent) { if (msg.encryptedContent) {
console.log(`[LoadMessages] Calling decryptMessage for message ${idx}`);
const decrypted = await decryptMessage(msg.encryptedContent, otherPartyKey); const decrypted = await decryptMessage(msg.encryptedContent, otherPartyKey);
console.log(`[LoadMessages] Message ${idx} decrypted successfully`);
return { ...msg, decryptedContent: decrypted }; return { ...msg, decryptedContent: decrypted };
} }
} catch (err) { } 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]' }; return { ...msg, decryptedContent: '[Unable to decrypt]' };
})); }));
console.log('[LoadMessages] Setting messages:', decrypted.length);
setMessages(decrypted); setMessages(decrypted);
} catch (err) { } 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 { } }; 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 { } };
+15 -1
View File
@@ -172,9 +172,16 @@ export function useChatEncryption() {
senderPublicKey: string senderPublicKey: string
): Promise<string> => { ): Promise<string> => {
if (!keys?.privateKey) { if (!keys?.privateKey) {
console.error('[Decrypt] No private key available');
throw new Error('No chat keys 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 myPrivateKey = await importPrivateKey(keys.privateKey);
const theirPublicKey = await importPublicKey(senderPublicKey); const theirPublicKey = await importPublicKey(senderPublicKey);
const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey); const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey);
@@ -183,6 +190,11 @@ export function useChatEncryption() {
const iv = combined.slice(0, 12); const iv = combined.slice(0, 12);
const ciphertext = combined.slice(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( const decrypted = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv }, { name: 'AES-GCM', iv },
sharedKey, sharedKey,
@@ -190,7 +202,9 @@ export function useChatEncryption() {
); );
const decoder = new TextDecoder(); const decoder = new TextDecoder();
return decoder.decode(decrypted); const result = decoder.decode(decrypted);
console.log('[Decrypt] Success:', result.substring(0, 50));
return result;
}, [keys]); }, [keys]);
// Clear keys (on logout) // Clear keys (on logout)