diff --git a/Backend/server.js b/Backend/server.js index 13a92ee..f3e1357 100644 --- a/Backend/server.js +++ b/Backend/server.js @@ -38,7 +38,7 @@ io.on('connection', (socket) => { // Load recent messages try { const result = await db.query( - `SELECT m.*, u.username + `SELECT m.*, u.username, u.public_signing_key FROM messages m JOIN users u ON m.sender_id = u.id WHERE m.channel_id = $1 @@ -69,10 +69,11 @@ io.on('connection', (socket) => { ...data }; - // Get username for display - const userRes = await db.query('SELECT username FROM users WHERE id = $1', [senderId]); + // Get username and signing key for display/verification + const userRes = await db.query('SELECT username, public_signing_key FROM users WHERE id = $1', [senderId]); if (userRes.rows.length > 0) { message.username = userRes.rows[0].username; + message.public_signing_key = userRes.rows[0].public_signing_key; } // Broadcast to channel diff --git a/Frontend/Electron/main.cjs b/Frontend/Electron/main.cjs index 3fdfa25..114d030 100644 --- a/Frontend/Electron/main.cjs +++ b/Frontend/Electron/main.cjs @@ -110,6 +110,15 @@ app.whenReady().then(() => { return crypto.createHash('sha256').update(data).digest('hex'); }); + ipcMain.handle('sign-message', (event, privateKeyPem, message) => { + // message should be a string or buffer + return crypto.sign(null, Buffer.from(message), privateKeyPem).toString('hex'); + }); + + ipcMain.handle('verify-signature', (event, publicKeyPem, message, signature) => { + return crypto.verify(null, Buffer.from(message), publicKeyPem, Buffer.from(signature, 'hex')); + }); + ipcMain.handle('derive-auth-keys', (event, password, salt) => { return new Promise((resolve, reject) => { crypto.scrypt(password, salt, 64, (err, derivedKey) => { diff --git a/Frontend/Electron/preload.cjs b/Frontend/Electron/preload.cjs index 9ee21fc..2c18702 100644 --- a/Frontend/Electron/preload.cjs +++ b/Frontend/Electron/preload.cjs @@ -4,6 +4,8 @@ contextBridge.exposeInMainWorld('cryptoAPI', { generateKeys: () => ipcRenderer.invoke('generate-keys'), randomBytes: (size) => ipcRenderer.invoke('random-bytes', size), sha256: (data) => ipcRenderer.invoke('sha256', data), + signMessage: (privateKey, message) => ipcRenderer.invoke('sign-message', privateKey, message), + verifySignature: (publicKey, message, signature) => ipcRenderer.invoke('verify-signature', publicKey, message, signature), deriveAuthKeys: (password, salt) => ipcRenderer.invoke('derive-auth-keys', password, salt), encryptData: (data, key) => ipcRenderer.invoke('encrypt-data', data, key), decryptData: (encryptedData, key, iv, tag) => ipcRenderer.invoke('decrypt-data', encryptedData, key, iv, tag), diff --git a/Frontend/Electron/src/components/ChatArea.jsx b/Frontend/Electron/src/components/ChatArea.jsx index 4859621..9040612 100644 --- a/Frontend/Electron/src/components/ChatArea.jsx +++ b/Frontend/Electron/src/components/ChatArea.jsx @@ -5,7 +5,91 @@ import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; -const ChatArea = ({ channelId, username }) => { +// Cache for link metadata to prevent pop-in +const metadataCache = new Map(); + +// Extracted LinkPreview to prevent re-renders on ChatArea updates +const LinkPreview = ({ url }) => { + const [metadata, setMetadata] = useState(metadataCache.get(url) || null); + const [loading, setLoading] = useState(!metadataCache.has(url)); + const [playing, setPlaying] = useState(false); + + useEffect(() => { + if (metadataCache.has(url)) { + setMetadata(metadataCache.get(url)); + setLoading(false); + return; + } + + let isMounted = true; + const fetchMeta = async () => { + try { + const data = await window.cryptoAPI.fetchMetadata(url); + if (isMounted) { + if (data) metadataCache.set(url, data); + setMetadata(data); + setLoading(false); + } + } catch (err) { + console.error("Failed to fetch metadata", err); + if (isMounted) setLoading(false); + } + }; + fetchMeta(); + return () => { isMounted = false; }; + }, [url]); + + // Helper to extract video ID + const getYouTubeId = (link) => { + const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + const match = link.match(regExp); + return (match && match[2].length === 11) ? match[2] : null; + }; + + const videoId = getYouTubeId(url); + const isYouTube = !!videoId; + + if (loading || !metadata || (!metadata.title && !metadata.image)) return null; + + return ( +
+
+ {metadata.siteName &&
{metadata.siteName}
} + {metadata.title && ( + { e.preventDefault(); window.cryptoAPI.openExternal(url); }} className="preview-title"> + {metadata.title} + + )} + {metadata.description &&
{metadata.description}
} + + {isYouTube && playing && ( +
+