// Content Security Policy enforcement if (!window.crypto || !window.crypto.subtle) { alert('This browser does not support required cryptographic features. Please use a modern browser.'); throw new Error('WebCrypto API not available'); } // Global variables let socket = null; let currentRoom = null; let currentUserId = null; let currentDisplayName = null; let keyPair = null; let sessionKey = null; let roomUsers = {}; let keysReady = false; let lastMessageTime = 0; let messageQueue = []; let isConnected = false; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 10; const MESSAGE_RATE_LIMIT = 1000; const MAX_MESSAGE_LENGTH = 4000; const MAX_ROOM_ID_LENGTH = 32; const MAX_MESSAGES = 512; // Security utilities const SecurityUtils = { sanitizeInput: function(input) { if (typeof input !== 'string') return ''; return input.trim().substring(0, MAX_MESSAGE_LENGTH); }, isValidRoomId: function(roomId) { if (!roomId || typeof roomId !== 'string') return false; if (roomId.length > MAX_ROOM_ID_LENGTH) return false; return /^[a-zA-Z0-9\-_]+$/.test(roomId); }, canSendMessage: function() { const now = Date.now(); if (now - lastMessageTime < MESSAGE_RATE_LIMIT) { return false; } lastMessageTime = now; return true; }, generateSecureId: function(length = 8) { const array = new Uint8Array(length); crypto.getRandomValues(array); return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase().substring(0, length); }, isValidBase64: function(str) { try { return btoa(atob(str)) === str; } catch (err) { return false; } } }; class SecureChatError extends Error { constructor(message, code = 'UNKNOWN') { super(message); this.name = 'SecureChatError'; this.code = code; } } // Initialize the app // Define your backend server (for now ngrok) const SERVER_URL = "https://kind-mosquito-multiply.ngrok-free.app"; async function initializeApp() { try { console.log("Initializing bytechat"); // Initialize socket connection to ngrok server socket = io(SERVER_URL, { transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: MAX_RECONNECT_ATTEMPTS, reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 10000, secure: true }); await generateKeyPair(); setupSocketListeners(); setupSecurityFeatures(); } catch (error) { console.error('Failed to initialize app:', error); updateStatus('Failed to initialize application', true); } } function setupSecurityFeatures() { document.addEventListener('visibilitychange', function() { if (document.hidden) { const messageInput = document.getElementById('messageInput'); if (messageInput) { messageInput.value = ''; } } }); window.addEventListener('beforeunload', function() { clearSensitiveData(); }); const securityIndicator = document.getElementById('securityIndicator'); //securityIndicator.style.display = 'block'; } function clearSensitiveData() { sessionKey = null; keyPair = null; roomUsers = {}; const passwordInput = document.getElementById('roomPasswordInput'); if (passwordInput) { passwordInput.value = ''; } } async function generateKeyPair() { try { updateStatus('Generating encryption keys...', false); keyPair = await window.crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, // Reduced from 4096 for better performance publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, false, ["encrypt", "decrypt"] ); keysReady = true; updateStatus("Ready to join a room", false); console.log("Key pair generated successfully"); } catch (error) { console.error("Failed to generate key pair:", error); updateStatus("Error: Failed to generate encryption keys", true); throw new SecureChatError("Key generation failed", "KEY_GEN_ERROR"); } } async function generateSessionKey() { try { sessionKey = await window.crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"] ); console.log("Session key generated"); return sessionKey; } catch (error) { console.error("Failed to generate session key:", error); throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR"); } } async function exportPublicKey(key) { try { const exported = await window.crypto.subtle.exportKey("spki", key); const base64Key = btoa(String.fromCharCode(...new Uint8Array(exported))); if (!SecurityUtils.isValidBase64(base64Key)) { throw new Error('Invalid key export'); } return base64Key; } catch (error) { console.error("Failed to export public key:", error); throw new SecureChatError("Public key export failed", "KEY_EXPORT_ERROR"); } } async function importPublicKey(keyString) { try { if (!SecurityUtils.isValidBase64(keyString)) { throw new Error('Invalid base64 key format'); } const keyData = Uint8Array.from(atob(keyString), c => c.charCodeAt(0)); return await window.crypto.subtle.importKey( "spki", keyData, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"] ); } catch (error) { console.error("Failed to import public key:", error); throw new SecureChatError("Public key import failed", "KEY_IMPORT_ERROR"); } } async function encryptSessionKey(sessionKey, publicKey) { try { const keyData = await window.crypto.subtle.exportKey("raw", sessionKey); const encrypted = await window.crypto.subtle.encrypt( { name: "RSA-OAEP" }, publicKey, keyData ); return btoa(String.fromCharCode(...new Uint8Array(encrypted))); } catch (error) { console.error("Failed to encrypt session key:", error); throw new SecureChatError("Session key encryption failed", "SESSION_KEY_ENCRYPT_ERROR"); } } async function decryptSessionKey(encryptedKey) { try { if (!SecurityUtils.isValidBase64(encryptedKey)) { throw new Error('Invalid encrypted key format'); } const keyData = Uint8Array.from(atob(encryptedKey), c => c.charCodeAt(0)); const decrypted = await window.crypto.subtle.decrypt( { name: "RSA-OAEP" }, keyPair.privateKey, keyData ); return await window.crypto.subtle.importKey( "raw", decrypted, { name: "AES-GCM" }, false, ["encrypt", "decrypt"] ); } catch (error) { console.error("Failed to decrypt session key:", error); throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR"); } } async function encryptMessage(message) { if (!sessionKey) { throw new SecureChatError("No session key available", "NO_SESSION_KEY"); } try { const sanitizedMessage = SecurityUtils.sanitizeInput(message); const encoder = new TextEncoder(); const data = encoder.encode(sanitizedMessage); const iv = window.crypto.getRandomValues(new Uint8Array(12)); const encrypted = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, sessionKey, data ); return { encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))), iv: btoa(String.fromCharCode(...iv)) }; } catch (error) { console.error("Failed to encrypt message:", error); throw new SecureChatError("Message encryption failed", "MESSAGE_ENCRYPT_ERROR"); } } async function decryptMessage(encryptedData, ivString) { if (!sessionKey) { return "[Encrypted message - no session key]"; } try { if (!SecurityUtils.isValidBase64(encryptedData) || !SecurityUtils.isValidBase64(ivString)) { return "[Invalid encrypted message format]"; } const encrypted = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0)); const iv = Uint8Array.from(atob(ivString), c => c.charCodeAt(0)); const decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, sessionKey, encrypted ); const decoder = new TextDecoder(); const decryptedMessage = decoder.decode(decrypted); return SecurityUtils.sanitizeInput(decryptedMessage); } catch (error) { console.error("Failed to decrypt message:", error); return "[Failed to decrypt message]"; } } // Enhanced socket listeners with fixed key exchange function setupSocketListeners() { socket.on('connect', () => { isConnected = true; reconnectAttempts = 0; console.log("Socket connected securely"); updateConnectionStatus(true); updateInputState(); // If user is already in a room, re-emit join_room after reconnect if (currentRoom && keyPair && keysReady) { exportPublicKey(keyPair.publicKey).then((publicKeyString) => { socket.emit('join_room', { room_id: currentRoom, public_key: publicKeyString, password: '' }); }); } }); socket.on('disconnect', (reason) => { isConnected = false; updateStatus("Connection lost. Attempting to reconnect...", true); updateConnectionStatus(false); updateInputState(); console.log('Socket disconnected:', reason); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; setTimeout(() => { if (!isConnected && reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) { socket.connect(); } }, 2000 * Math.pow(2, reconnectAttempts)); } else { updateStatus("Connection failed. Please refresh the page.", true); updateInputState(); } }); socket.on('connect_error', (error) => { console.error('Connection error:', error); updateStatus("Connection error. Please check your internet connection.", true); }); socket.on('user_connected', (data) => { if (data && data.user_id) { currentUserId = data.user_id; console.log("Connected as user:", currentUserId); } }); socket.on('room_joined', async (data) => { try { if (data.error) { updateStatus(data.error, true); updateInputState(); return; } if (!data.room_id || !data.user_id || !SecurityUtils.isValidRoomId(data.room_id)) { throw new SecureChatError("Invalid room data received", "INVALID_ROOM_DATA"); } currentRoom = data.room_id; currentUserId = data.user_id; currentDisplayName = data.display_name; roomUsers = data.user_keys || {}; // Switch to chat screen // WelcomeScreen div gets removed if you join a room, this is required cause it looks very weird document.getElementById('welcomeScreen').style.display = 'none'; document.getElementById('chatScreen').style.display = 'flex'; document.getElementById('roomInfo').style.display = 'flex'; document.getElementById('inputSection').style.display = 'block'; // Update room info document.getElementById('currentRoomId').textContent = currentRoom; document.getElementById('userCounter').textContent = `Users: ${data.users?.length || 0}`; document.getElementById('messageCounter').textContent = `Messages: ${data.message_count || 0}/256`; // Handle session key based on user status if (data.is_first_user) { console.log("First user - generating session key"); await generateSessionKey(); updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false); updateInputState(); } else if (sessionKey) { console.log("Rejoining with existing session key"); updateStatus("Session key present. You can chat.", false); updateInputState(); } else { console.log("Waiting for session key from existing users"); updateStatus("Waiting for session key from other users...", false); updateInputState(); } // Load existing messages if (data.messages && Array.isArray(data.messages)) { for (const msg of data.messages) { if (msg && typeof msg === 'object') { await displayMessage(msg); } } } console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user); } catch (error) { console.error("Error processing room_joined:", error); updateStatus("Error joining room", true); updateInputState(); } }); socket.on('user_joined', async (data) => { try { if (!data || !data.user_id || !data.public_key) return; roomUsers[data.user_id] = data.public_key; updateUsersCount(); addSystemMessage(`${data.display_name || 'User'} joined the room`); // If we have a session key, share it with the new user if (sessionKey) { console.log("Sharing session key with new user:", data.user_id); try { const publicKey = await importPublicKey(data.public_key); const encryptedKey = await encryptSessionKey(sessionKey, publicKey); socket.emit('share_session_key', { room_id: currentRoom, target_user_id: data.user_id, encrypted_key: encryptedKey }); console.log("Session key shared with new user successfully"); } catch (error) { console.error("Failed to share session key with new user:", error); } } } catch (error) { console.error("Error processing user_joined:", error); } }); socket.on('request_session_key', async (data) => { try { console.log("Session key requested for new user:", data.new_user_id); if (!sessionKey || !data.new_user_id || !data.public_key) { console.log("Cannot fulfill session key request - missing data"); return; } try { const publicKey = await importPublicKey(data.public_key); const encryptedKey = await encryptSessionKey(sessionKey, publicKey); socket.emit('share_session_key', { room_id: currentRoom, target_user_id: data.new_user_id, encrypted_key: encryptedKey }); console.log("Session key shared in response to request"); } catch (error) { console.error("Failed to share session key on request:", error); } } catch (error) { console.error("Error processing request_session_key:", error); } }); socket.on('session_key_received', async (data) => { try { console.log("Session key received from:", data.from_user_id); if (!data || !data.encrypted_key) { console.log("Invalid session key data received"); return; } if (!sessionKey) { try { sessionKey = await decryptSessionKey(data.encrypted_key); console.log("Session key decrypted successfully"); updateStatus("Session key received! You can now chat securely.", false); updateInputState(); addSystemMessage("🔑 Session key received - you can now chat!"); } catch (error) { console.error("Failed to decrypt received session key:", error); updateStatus("Failed to decrypt session key", true); } } else { console.log("Session key already exists, ignoring duplicate"); } } catch (error) { console.error("Failed to process session key:", error); updateStatus("Error: Failed to decrypt session key", true); } }); socket.on('new_message', async (data) => { try { if (!data || typeof data !== 'object') return; await displayMessage(data); if (data.message_count) { document.getElementById('messageCounter').textContent = `Messages: ${data.message_count}/256`; } } catch (error) { console.error("Error processing new message:", error); } }); socket.on('user_left', (data) => { try { if (data && data.user_id) { delete roomUsers[data.user_id]; updateUsersCount(); addSystemMessage(`User left the room`); } } catch (error) { console.error("Error processing user_left:", error); } }); socket.on('error', (error) => { console.error('Socket error:', error); updateStatus("Connection error occurred", true); }); socket.on('ping', () => { socket.emit('pong'); }); } async function createRoom() { try { const roomId = SecurityUtils.generateSecureId(6); const password = document.getElementById('roomPasswordInput').value; await joinSpecificRoom(roomId, password); } catch (error) { console.error("Failed to create room:", error); updateStatus("Error creating room", true); } } async function joinRoom() { try { const roomInput = document.getElementById('roomInput'); const roomId = SecurityUtils.sanitizeInput(roomInput.value); const password = document.getElementById('roomPasswordInput').value; if (!roomId) { await createRoom(); return; } if (!SecurityUtils.isValidRoomId(roomId)) { updateStatus("Invalid room ID format. Use only letters, numbers, hyphens and underscores.", true); return; } await joinSpecificRoom(roomId, password); } catch (error) { console.error("Failed to join room:", error); updateStatus("Error joining room", true); } } async function joinSpecificRoom(roomId, password = "") { if (!keysReady) { updateStatus("Error: Encryption keys not ready", true); return; } if (!SecurityUtils.isValidRoomId(roomId)) { updateStatus("Invalid room ID format", true); return; } try { updateStatus("Joining room...", false); const publicKeyString = await exportPublicKey(keyPair.publicKey); socket.emit('join_room', { room_id: roomId, public_key: publicKeyString, password: password }); } catch (error) { console.error("Failed to join room:", error); updateStatus("Error: Failed to join room", true); } } async function sendMessage() { try { const input = document.getElementById('messageInput'); let message = SecurityUtils.sanitizeInput(input.value); if (!message || !sessionKey) return; if (!SecurityUtils.canSendMessage()) { updateStatus("Please wait before sending another message", true); return; } if (message.length > MAX_MESSAGE_LENGTH) { updateStatus("Message too long", true); return; } const encryptedData = await encryptMessage(message); socket.emit('send_message', { encrypted_content: encryptedData.encrypted, iv: encryptedData.iv }); input.value = ''; autoResize(input); } catch (error) { console.error("Failed to send message:", error); updateStatus("Error: Failed to send message", true); } } async function displayMessage(messageData) { try { if (!messageData || typeof messageData !== 'object') return; const container = document.getElementById('messagesContainer'); const isOwnMessage = messageData.sender_id === currentUserId; let displayText; if (messageData.iv && sessionKey && messageData.encrypted_content) { displayText = await decryptMessage(messageData.encrypted_content, messageData.iv); } else { displayText = "[Encrypted message]"; } const escapeHtml = (text) => { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }; const timestamp = messageData.timestamp ? new Date(messageData.timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`; const avatarText = displayName.substring(0, 2).toUpperCase(); const messageGroup = document.createElement('div'); messageGroup.className = 'message-group'; messageGroup.classList.add(isOwnMessage ? 'sent' : 'received'); messageGroup.innerHTML = `
`; const messageContentEl = messageGroup.querySelector('.message-content'); messageContentEl.textContent = displayText; container.appendChild(messageGroup); container.scrollTop = container.scrollHeight; const messages = container.children; if (messages.length > MAX_MESSAGES + 50) { for (let i = 0; i < 50; i++) { if (messages[0]) { container.removeChild(messages[0]); } } } } catch (error) { console.error("Error displaying message:", error); } } function addSystemMessage(text) { try { const container = document.getElementById('messagesContainer'); const systemMessage = document.createElement('div'); systemMessage.className = 'message-group system'; // ✅ add system class const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); systemMessage.innerHTML = ` `; const messageContentEl = systemMessage.querySelector('.message-content'); messageContentEl.textContent = text; container.appendChild(systemMessage); container.scrollTop = container.scrollHeight; } catch (error) { console.error("Error adding system message:", error); } } function updateStatus(message, isError = false) { try { const statusEl = document.getElementById('statusText'); const statusDiv = document.createElement('div'); if (isError) { statusDiv.className = 'error-message'; } else { statusDiv.className = 'success-message'; } statusDiv.textContent = message; statusEl.innerHTML = ''; statusEl.appendChild(statusDiv); } catch (error) { console.error("Error updating status:", error); } } function updateUsersCount() { try { const count = Object.keys(roomUsers).length; const counterEl = document.getElementById('userCounter'); if (counterEl) { counterEl.textContent = `Users: ${count}`; } } catch (error) { console.error("Error updating users count:", error); } } function updateInputState() { try { const input = document.getElementById('messageInput'); const btn = document.getElementById('sendButton'); if (!input || !btn) return; if (sessionKey && isConnected) { input.disabled = false; btn.disabled = false; input.placeholder = "Message ByteChat…"; } else { input.disabled = true; btn.disabled = true; input.placeholder = isConnected ? "Waiting for encryption key..." : "Disconnected..."; } } catch (error) { console.error("Error updating input state:", error); } } function autoResize(textarea) { try { if (!textarea) return; textarea.style.height = 'auto'; const newHeight = Math.min(Math.max(textarea.scrollHeight, 44), 200); textarea.style.height = newHeight + 'px'; } catch (error) { console.error("Error in autoResize:", error); } } function updateConnectionStatus(connected) { const encryptionBadge = document.getElementById('encryptionBadge'); const securityIndicator = document.getElementById('securityIndicator'); if (connected) { if (encryptionBadge) encryptionBadge.textContent = 'E2E Encrypted'; if (securityIndicator) securityIndicator.textContent = '🔒 End-to-End Encrypted'; } else { if (encryptionBadge) encryptionBadge.textContent = 'Disconnected'; if (securityIndicator) securityIndicator.textContent = '⚠️ Disconnected'; } } // Event listeners setup document.addEventListener('DOMContentLoaded', () => { try { const messageInput = document.getElementById('messageInput'); const roomInput = document.getElementById('roomInput'); const roomPasswordInput = document.getElementById('roomPasswordInput'); if (messageInput) { messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); messageInput.addEventListener('input', (e) => { autoResize(e.target); if (e.target.value.length > MAX_MESSAGE_LENGTH) { e.target.value = e.target.value.substring(0, MAX_MESSAGE_LENGTH); updateStatus(`Message truncated to ${MAX_MESSAGE_LENGTH} characters`, true); } }); messageInput.addEventListener('paste', (e) => { setTimeout(() => { if (messageInput.value.length > MAX_MESSAGE_LENGTH) { messageInput.value = messageInput.value.substring(0, MAX_MESSAGE_LENGTH); updateStatus(`Pasted content truncated to ${MAX_MESSAGE_LENGTH} characters`, true); } autoResize(messageInput); }, 0); }); } if (roomInput) { roomInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); joinRoom(); } }); roomInput.addEventListener('input', (e) => { const value = e.target.value; if (value && !SecurityUtils.isValidRoomId(value)) { e.target.setCustomValidity('Only letters, numbers, hyphens and underscores allowed'); } else { e.target.setCustomValidity(''); } }); } if (roomPasswordInput) { roomPasswordInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); joinRoom(); } }); } const joinRoomBtn = document.getElementById('joinRoomBtn'); if (joinRoomBtn) joinRoomBtn.addEventListener('click', joinRoom); const createRoomBtn = document.getElementById('createRoomBtn'); if (createRoomBtn) createRoomBtn.addEventListener('click', createRoom); const sendButton = document.getElementById('sendButton'); if (sendButton) sendButton.addEventListener('click', sendMessage); document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && messageInput && !messageInput.disabled) { e.preventDefault(); sendMessage(); } if (e.key === 'Escape' && messageInput) { messageInput.value = ''; autoResize(messageInput); } }); initializeApp(); } catch (error) { console.error("Error setting up event listeners:", error); updateStatus("Error initializing interface", true); } }); // Periodic connection health check setInterval(() => { if (socket && isConnected) { socket.volatile.emit('ping'); } }, 30000); // Prevent clickjacking if (top !== self) { top.location = self.location; } // CSP violation reporting document.addEventListener('securitypolicyviolation', (e) => { console.error('CSP Violation:', { violatedDirective: e.violatedDirective, blockedURI: e.blockedURI, lineNumber: e.lineNumber, columnNumber: e.columnNumber }); }); // Expose necessary functions window.SecureChat = { joinRoom, createRoom, sendMessage }; // Prevent console access in production if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { console.log = console.warn = console.error = () => {}; }