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.image && (!isYouTube || !playing) && (
+
isYouTube && setPlaying(true)}
+ style={isYouTube ? { cursor: 'pointer' } : {}}
+ >
+

+ {isYouTube &&
▶
}
+
+ )}
+
+ );
+};
+
+const ChatArea = ({ channelId, channelName, username }) => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [socket, setSocket] = useState(null);
@@ -50,77 +134,20 @@ const ChatArea = ({ channelId, username }) => {
return text.match(urlRegex) || [];
};
- const LinkPreview = ({ url }) => {
- const [metadata, setMetadata] = useState(null);
- const [loading, setLoading] = useState(true);
- const [playing, setPlaying] = useState(false);
-
- useEffect(() => {
- let isMounted = true;
- const fetchMeta = async () => {
- try {
- const data = await window.cryptoAPI.fetchMetadata(url);
- if (isMounted) {
- 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.image && (!isYouTube || !playing) && (
-
isYouTube && setPlaying(true)}
- style={isYouTube ? { cursor: 'pointer' } : {}}
- >
-

- {isYouTube &&
▶
}
-
- )}
-
- );
+ // Helper to verify message signature
+ const verifyMessage = async (msg) => {
+ if (!msg.signature || !msg.public_signing_key) return false;
+ try {
+ const isValid = await window.cryptoAPI.verifySignature(
+ msg.public_signing_key,
+ msg.ciphertext,
+ msg.signature
+ );
+ return isValid;
+ } catch (e) {
+ console.error('Verification error for msg:', msg.id, e);
+ return false;
+ }
};
useEffect(() => {
@@ -130,16 +157,18 @@ const ChatArea = ({ channelId, username }) => {
newSocket.emit('join_channel', channelId);
newSocket.on('recent_messages', async (msgs) => {
- const decryptedMessages = await Promise.all(msgs.map(async (msg) => {
+ const processedMessages = await Promise.all(msgs.map(async (msg) => {
const content = await decryptMessage(msg);
- return { ...msg, content };
+ const isVerified = await verifyMessage(msg);
+ return { ...msg, content, isVerified };
}));
- setMessages(decryptedMessages);
+ setMessages(processedMessages);
});
newSocket.on('new_message', async (msg) => {
const content = await decryptMessage(msg);
- setMessages(prev => [...prev, { ...msg, content }]);
+ const isVerified = await verifyMessage(msg);
+ setMessages(prev => [...prev, { ...msg, content, isVerified }]);
});
return () => newSocket.close();
@@ -163,20 +192,24 @@ const ChatArea = ({ channelId, username }) => {
try {
const { content: encryptedContent, iv, tag } = await window.cryptoAPI.encryptData(input, DEMO_CHANNEL_KEY);
const ciphertext = encryptedContent + tag;
- const signature = 'placeholder_signature';
-
const senderId = localStorage.getItem('userId');
+ const signingKey = sessionStorage.getItem('signingKey');
+
if (!senderId) {
console.error('No userId found in localStorage');
return;
}
+ if (!signingKey) {
+ console.error('No signingKey found in sessionStorage. Please relogin.');
+ return; // Prevent sending unsigned messages
+ }
const messageData = {
channelId,
senderId,
ciphertext,
nonce: iv,
- signature,
+ signature: await window.cryptoAPI.signMessage(signingKey, ciphertext),
keyVersion: 1
};
@@ -197,81 +230,106 @@ const ChatArea = ({ channelId, username }) => {
return (
- {messages.map((msg, idx) => {
- const urls = extractUrls(msg.content);
- return (
-
-
-
- {(msg.username || '?').substring(0, 1).toUpperCase()}
+
+ {messages.map((msg, idx) => {
+ const urls = extractUrls(msg.content);
+ const currentDate = new Date(msg.created_at);
+ const previousDate = idx > 0 ? new Date(messages[idx - 1].created_at) : null;
+
+ const isNewDay = !previousDate || (
+ currentDate.getDate() !== previousDate.getDate() ||
+ currentDate.getMonth() !== previousDate.getMonth() ||
+ currentDate.getFullYear() !== previousDate.getFullYear()
+ );
+
+ return (
+
+ {isNewDay && (
+
+ {currentDate.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}
+
+ )}
+
+
+
+ {(msg.username || '?').substring(0, 1).toUpperCase()}
+
+
+
+
+
+ {msg.username || 'Unknown'}
+
+ {!msg.isVerified && (
+
+
+
+ )}
+
+ {currentDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
+
+
+
+
(
+ {
+ e.preventDefault();
+ window.cryptoAPI.openExternal(props.href);
+ }}
+ style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }}
+ onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
+ onMouseOut={(e) => e.target.style.textDecoration = 'none'}
+ />
+ ),
+ code({ node, inline, className, children, ...props }) {
+ const match = /language-(\w+)/.exec(className || '')
+ return !inline && match ? (
+
+ {String(children).replace(/\n$/, '')}
+
+ ) : (
+
+ {children}
+
+ )
+ },
+ p: ({ node, ...props }) => ,
+ h1: ({ node, ...props }) => ,
+ h2: ({ node, ...props }) => ,
+ h3: ({ node, ...props }) => ,
+ ul: ({ node, ...props }) => ,
+ ol: ({ node, ...props }) =>
,
+ li: ({ node, ...props }) => ,
+ hr: ({ node, ...props }) =>
,
+ }}
+ >
+ {msg.content}
+
+ {urls.map((url, i) => (
+
+ ))}
+
+
-
-
-
-
- {msg.username || 'Unknown'}
-
-
- {new Date(msg.created_at).toLocaleDateString()} at {new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
-
-
-
(
- {
- e.preventDefault();
- window.cryptoAPI.openExternal(props.href);
- }}
- style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }}
- onMouseOver={(e) => e.target.style.textDecoration = 'underline'}
- onMouseOut={(e) => e.target.style.textDecoration = 'none'}
- />
- ),
- code({ node, inline, className, children, ...props }) {
- const match = /language-(\w+)/.exec(className || '')
- return !inline && match ? (
-
- {String(children).replace(/\n$/, '')}
-
- ) : (
-
- {children}
-
- )
- },
- p: ({ node, ...props }) => ,
- h1: ({ node, ...props }) => ,
- h2: ({ node, ...props }) => ,
- h3: ({ node, ...props }) => ,
- ul: ({ node, ...props }) => ,
- ol: ({ node, ...props }) =>
,
- li: ({ node, ...props }) => ,
- hr: ({ node, ...props }) =>
,
- }}
- >
- {msg.content}
-
- {urls.map((url, i) => (
-
- ))}
-
-
-
- );
- })}
-
+
+ );
+ })}
+
+