diff --git a/client/src/crypto/groupCrypto.ts b/client/src/crypto/groupCrypto.ts index a7cfd85..d4726cc 100644 --- a/client/src/crypto/groupCrypto.ts +++ b/client/src/crypto/groupCrypto.ts @@ -178,10 +178,20 @@ export async function distributeSenderKey( recipientIkPub: string, recipientKemPub: string | null | undefined ): Promise<{ encrypted_key: string; header: string }> { - const result = await encryptHybrid(recipientIkPub, recipientKemPub, senderKey) - return { - encrypted_key: result.ciphertext, - header: result.header, + try { + const result = await encryptHybrid(recipientIkPub, recipientKemPub, senderKey) + return { + encrypted_key: result.ciphertext, + header: result.header, + } + } catch (err) { + console.error('[groupCrypto] distributeSenderKey failed:', { + recipientIkPubLen: recipientIkPub?.length, + kemPubPresent: !!recipientKemPub, + senderKeyLen: senderKey?.length, + error: err, + }) + throw err } } @@ -201,5 +211,17 @@ export async function receiveSenderKey( myPrivKey: string, myKemPriv: string | null | undefined ): Promise { - return await decryptHybrid(header, myPrivKey, myKemPriv, encryptedKey) + try { + const result = await decryptHybrid(header, myPrivKey, myKemPriv, encryptedKey) + return result + } catch (err) { + console.error('[groupCrypto] receiveSenderKey decryption failed:', { + encryptedKeyLen: encryptedKey?.length, + headerLen: header?.length, + myPrivKeyLen: myPrivKey?.length, + kemPrivPresent: !!myKemPriv, + error: err, + }) + throw err + } } diff --git a/client/src/hooks/useSocket.ts b/client/src/hooks/useSocket.ts index 4700526..0a693c1 100644 --- a/client/src/hooks/useSocket.ts +++ b/client/src/hooks/useSocket.ts @@ -125,12 +125,22 @@ export function useSocket() { await fetchAndStoreSenderKeys(data.group_id) sk = getSenderKey(data.group_id, data.from) } + if (!sk) { + // Still no key — retry after a brief delay. + // The sender distributes keys via HTTP POST before sending the WS message, + // but there can be a race where the message arrives before the POST response + // has been fully committed to the DB. + console.log(`[useSocket] Retrying sender key fetch after 1s delay for ${data.from} in group ${data.group_id}...`) + await new Promise(r => setTimeout(r, 1000)) + await fetchAndStoreSenderKeys(data.group_id) + sk = getSenderKey(data.group_id, data.from) + } if (sk) { const text = await decryptWithSenderKey(data.ciphertext, data.nonce, sk.senderKey) msgToAdd = { ...data, decrypted: text } } else { // Still don't have sender key — store as 🔒 but keep nonce in data for retry - console.warn(`[useSocket] Still no sender key for ${data.from} in group ${data.group_id} after fetch. Message will show 🔒`) + console.warn(`[useSocket] Still no sender key for ${data.from} in group ${data.group_id} after retries. Message will show 🔒`) msgToAdd = { ...data, decrypted: '🔒' } } } catch (err) { @@ -218,11 +228,15 @@ export function useSocket() { if (pending && data.msg_id) { const chatId = pending.group_id || pending.to if (chatId) { - useStore.getState().addMessage(chatId, { + // Build the message to add. For encrypted group messages, pendingMsg + // now carries encryption metadata (ciphertext, nonce, sender_key_version) + // set during the encryption step in Chat.tsx. + const msgToStore: any = { ...pending, id: data.msg_id, ts: data.ts || Date.now(), - }) + } + useStore.getState().addMessage(chatId, msgToStore) } ;(window as any).__pendingMsg = null } diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index d526982..c15ca65 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -573,13 +573,20 @@ export default function Chat() { const memberKeys = await get(`/api/users/${m.id}`) ikPub = memberKeys?.ik_pub kemPub = memberKeys?.kem_pub - } catch {} + } catch (fetchKeyErr) { + console.warn(`[Chat] Failed to fetch public keys for member ${m.id}:`, fetchKeyErr) + } } if (ikPub && keys) { try { - const dist = await distributeSenderKey(newKey, ikPub, kemPub) + // Pass null for kemPub — kem_pub on server is an Ed25519 signing key, + // NOT a valid Kyber KEM key. Passing it causes unnecessary Kyber encap + // failures. Using null goes directly to pure ECDH (version 1). + const dist = await distributeSenderKey(newKey, ikPub, null) distributions.push({ to_id: m.id, encrypted_key: dist.encrypted_key, header: dist.header }) - } catch {} + } catch (distMemberErr) { + console.warn(`[Chat] distributeSenderKey failed for member ${m.id}:`, distMemberErr) + } } } if (distributions.length > 0) { @@ -602,6 +609,12 @@ export default function Chat() { } if (sk && !sent) { const encrypted = await encryptWithSenderKey(content, sk.senderKey) + // Update pendingMsg with actual encryption metadata so the ack handler + // stores the message with correct encrypted fields (matching server data). + // This ensures consistency when messages are later loaded from server. + pendingMsg.ciphertext = encrypted.ciphertext + pendingMsg.nonce = encrypted.nonce + pendingMsg.sender_key_version = sk.keyVersion sent = sendWs({ type: 'message', msg_type: msgType, group_id: id, ciphertext: encrypted.ciphertext, nonce: encrypted.nonce, sender_key_version: sk.keyVersion }) } } catch (encErr) {