From 68741badd8dab601548fb6b594e88680be022092 Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Wed, 27 Aug 2025 17:29:22 +0200 Subject: [PATCH] Issue 1: Fixed ( Most of it ) --- src-tauri/src/main.rs | 14 ++ src-tauri/tauri.conf.json | 7 +- src/main.js | 448 ++++++++++++++++++++++++++++++++------ 3 files changed, 405 insertions(+), 64 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1cd03f0..b0c8100 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,20 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +// if there are nvidia drivers and were on wayland disable dma buffer. +// on a x11 system this wont fire +fn disable_dmabuf_if_true() { + if std::env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland" { + if let Ok(vendor) = std::fs::read_to_string("/proc/driver/nvidia/version") { + if vendor.contains("NVIDIA") { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + } + } + +} + fn main() { + disable_dmabuf_if_true(); bytechat_desktop_lib::run() } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 68595b5..e686e07 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -11,9 +11,10 @@ "windows": [ { "title": "ByteChat", - "width": 800, - "height": 600, - "devtools": false + "width": 600, + "height": 700, + "devtools": false, + "zoomHotkeysEnabled": false } ], "security": { diff --git a/src/main.js b/src/main.js index 4e7b62e..39653ec 100644 --- a/src/main.js +++ b/src/main.js @@ -1,9 +1,36 @@ +// Tauri-compatible ByteChat client +// Fixes localStorage/sessionStorage issues and WebCrypto compatibility + // 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'); } +// Check if we're running in Tauri +const isRunningInTauri = typeof window.__TAURI__ !== 'undefined'; +let tauriStore = null; + +// Initialize Tauri store if available +if (isRunningInTauri) { + try { + // Try different ways to access Tauri store + if (window.__TAURI__ && window.__TAURI__.store) { + const { Store } = window.__TAURI__.store; + tauriStore = new Store('.bytechat-keys.json'); + console.log('Running in Tauri environment with persistent storage (method 1)'); + } else if (window.__TAURI_PLUGIN_STORE__) { + const { Store } = window.__TAURI_PLUGIN_STORE__; + tauriStore = new Store('.bytechat-keys.json'); + console.log('Running in Tauri environment with persistent storage (method 2)'); + } else { + console.warn('Tauri detected but store plugin not available'); + } + } catch (e) { + console.warn('Tauri store not available, using memory storage:', e); + } +} + // Global variables let socket = null; let currentRoom = null; @@ -23,6 +50,160 @@ const MAX_MESSAGE_LENGTH = 4000; const MAX_ROOM_ID_LENGTH = 32; const MAX_MESSAGES = 512; +// In-memory storage for keys (Tauri-compatible) +const keyStorage = { + // Store RSA keypair as JWK for persistence + async storeKeyPair(keyPair) { + try { + const publicJWK = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey); + const privateJWK = await window.crypto.subtle.exportKey('jwk', keyPair.privateKey); + + const keyData = { + publicKey: publicJWK, + privateKey: privateJWK, + timestamp: Date.now() + }; + + if (isRunningInTauri && tauriStore) { + try { + await tauriStore.set('rsa_keypair', keyData); + await tauriStore.save(); + console.log('RSA keypair stored in Tauri store'); + } catch (storeError) { + console.warn('Failed to use Tauri store, falling back to memory:', storeError); + this._memoryKeys = keyData; + } + } else { + // Fallback to memory storage + this._memoryKeys = keyData; + console.log('RSA keypair stored in memory (no persistent storage)'); + } + } catch (error) { + console.error('Failed to store keypair:', error); + } + }, + + async loadKeyPair() { + try { + let keyData; + + if (isRunningInTauri && tauriStore) { + try { + keyData = await tauriStore.get('rsa_keypair'); + } catch (storeError) { + console.warn('Failed to read from Tauri store:', storeError); + keyData = this._memoryKeys; + } + } else { + keyData = this._memoryKeys; + } + + if (!keyData || !keyData.publicKey || !keyData.privateKey) { + console.log('No stored keypair found'); + return null; + } + + // Import keys from JWK + const publicKey = await window.crypto.subtle.importKey( + 'jwk', + keyData.publicKey, + { name: 'RSA-OAEP', hash: 'SHA-256' }, + false, + ['encrypt'] + ); + + const privateKey = await window.crypto.subtle.importKey( + 'jwk', + keyData.privateKey, + { name: 'RSA-OAEP', hash: 'SHA-256' }, + false, + ['decrypt'] + ); + + console.log('RSA keypair loaded from storage'); + return { publicKey, privateKey }; + + } catch (error) { + console.error('Failed to load keypair:', error); + return null; + } + }, + + async storeSessionKey(roomId, sessionKey) { + try { + const keyData = await window.crypto.subtle.exportKey('raw', sessionKey); + const base64Key = btoa(String.fromCharCode(...new Uint8Array(keyData))); + + const sessionData = { + roomId, + key: base64Key, + timestamp: Date.now() + }; + + if (isRunningInTauri && tauriStore) { + try { + await tauriStore.set(`session_key_${roomId}`, sessionData); + await tauriStore.save(); + console.log('Session key stored for room:', roomId); + } catch (storeError) { + console.warn('Failed to store session key in Tauri store:', storeError); + this._sessionKeys = this._sessionKeys || {}; + this._sessionKeys[roomId] = sessionData; + } + } else { + this._sessionKeys = this._sessionKeys || {}; + this._sessionKeys[roomId] = sessionData; + console.log('Session key stored in memory for room:', roomId); + } + } catch (error) { + console.error('Failed to store session key:', error); + } + }, + + async loadSessionKey(roomId) { + try { + let sessionData; + + if (isRunningInTauri && tauriStore) { + try { + sessionData = await tauriStore.get(`session_key_${roomId}`); + } catch (storeError) { + console.warn('Failed to load session key from Tauri store:', storeError); + this._sessionKeys = this._sessionKeys || {}; + sessionData = this._sessionKeys[roomId]; + } + } else { + this._sessionKeys = this._sessionKeys || {}; + sessionData = this._sessionKeys[roomId]; + } + + if (!sessionData || !sessionData.key) { + console.log('No stored session key found for room:', roomId); + return null; + } + + const keyData = Uint8Array.from(atob(sessionData.key), c => c.charCodeAt(0)); + const sessionKey = await window.crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-GCM' }, + false, + ['encrypt', 'decrypt'] + ); + + console.log('Session key loaded for room:', roomId); + return sessionKey; + + } catch (error) { + console.error('Failed to load session key:', error); + return null; + } + }, + + _memoryKeys: null, + _sessionKeys: {} +}; + // Security utilities const SecurityUtils = { sanitizeInput: function(input) { @@ -64,15 +245,14 @@ class SecureChatError extends Error { } } -// Initialize the app -// Define your backend server (for now ngrok) +// Define your backend server const SERVER_URL = "https://kind-mosquito-multiply.ngrok-free.app"; async function initializeApp() { try { - console.log("Initializing bytechat"); + console.log("Initializing bytechat in", isRunningInTauri ? "Tauri" : "browser", "environment"); - // Initialize socket connection to ngrok server + // Initialize socket connection socket = io(SERVER_URL, { transports: ['websocket', 'polling'], reconnection: true, @@ -80,9 +260,17 @@ async function initializeApp() { reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 10000, - secure: true + secure: true, + forceNew: false }); + // Add socket event logging for debugging + if (isRunningInTauri) { + socket.onAny((event, payload) => { + console.log('SOCKET RECV', event, payload); + }); + } + await generateKeyPair(); setupSocketListeners(); setupSecurityFeatures(); @@ -93,7 +281,6 @@ async function initializeApp() { } } - function setupSecurityFeatures() { document.addEventListener('visibilitychange', function() { if (document.hidden) { @@ -107,14 +294,11 @@ function setupSecurityFeatures() { window.addEventListener('beforeunload', function() { clearSensitiveData(); }); - - const securityIndicator = document.getElementById('securityIndicator'); - //securityIndicator.style.display = 'block'; } function clearSensitiveData() { sessionKey = null; - keyPair = null; + // Don't clear keyPair as we want to reuse it roomUsers = {}; const passwordInput = document.getElementById('roomPasswordInput'); @@ -125,27 +309,72 @@ function clearSensitiveData() { async function generateKeyPair() { try { - updateStatus('Generating encryption keys...', false); + console.log('=== Starting key generation process ==='); + updateStatus('Loading or 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"] - ); + // Try to load existing keypair first + console.log('Attempting to load stored keypair...'); + const storedKeyPair = await keyStorage.loadKeyPair(); + + if (storedKeyPair) { + keyPair = storedKeyPair; + console.log("✅ Using stored RSA keypair"); + } else { + console.log("🔧 Generating new RSA keypair..."); + + keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256" + }, + true, // Make extractable for storage + ["encrypt", "decrypt"] + ); + + console.log("✅ New RSA keypair generated"); + + // Store the new keypair + console.log("💾 Storing new keypair..."); + await keyStorage.storeKeyPair(keyPair); + console.log("✅ Keypair stored successfully"); + } + + // Validate the keypair + if (!keyPair || !keyPair.publicKey || !keyPair.privateKey) { + throw new Error('Invalid keypair generated/loaded'); + } keysReady = true; updateStatus("Ready to join a room", false); - console.log("Key pair generated successfully"); + console.log("✅ Key pair ready - can now join rooms"); + console.log('=== Key generation process complete ==='); } 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"); + console.error("❌ Failed to generate/load key pair:", error); + updateStatus("Error: Failed to set up encryption keys", true); + + // Try to generate a fallback keypair without storage + try { + console.log("🔄 Attempting fallback key generation..."); + keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256" + }, + false, // Not extractable for fallback + ["encrypt", "decrypt"] + ); + keysReady = true; + updateStatus("Ready to join a room (temporary keys)", false); + console.log("✅ Fallback keypair generated successfully"); + } catch (fallbackError) { + console.error("❌ Fallback key generation also failed:", fallbackError); + throw new SecureChatError("Key generation failed completely", "KEY_GEN_ERROR"); + } } } @@ -157,6 +386,12 @@ async function generateSessionKey() { ["encrypt", "decrypt"] ); console.log("Session key generated"); + + // Store session key for this room + if (currentRoom) { + await keyStorage.storeSessionKey(currentRoom, sessionKey); + } + return sessionKey; } catch (error) { console.error("Failed to generate session key:", error); @@ -201,6 +436,42 @@ async function importPublicKey(keyString) { } } +// Enhanced unwrap function for better compatibility +async function unwrapAesKeyWithRsa(privateKey, wrappedKeyBase64) { + try { + console.log("Attempting to unwrap AES key with RSA private key"); + + if (!SecurityUtils.isValidBase64(wrappedKeyBase64)) { + throw new Error('Invalid base64 wrapped key format'); + } + + const wrappedKeyData = Uint8Array.from(atob(wrappedKeyBase64), c => c.charCodeAt(0)); + + // Decrypt the wrapped key + const unwrappedKeyData = await window.crypto.subtle.decrypt( + { name: "RSA-OAEP" }, + privateKey, + wrappedKeyData + ); + + // Import as AES-GCM key + const aesKey = await window.crypto.subtle.importKey( + 'raw', + unwrappedKeyData, + { name: 'AES-GCM' }, + true, // Make extractable for storage + ['encrypt', 'decrypt'] + ); + + console.log("AES key successfully unwrapped"); + return aesKey; + + } catch (error) { + console.error("Failed to unwrap AES key:", error); + throw new SecureChatError("Failed to unwrap AES session key", "AES_UNWRAP_ERROR"); + } +} + async function encryptSessionKey(sessionKey, publicKey) { try { const keyData = await window.crypto.subtle.exportKey("raw", sessionKey); @@ -218,24 +489,16 @@ async function encryptSessionKey(sessionKey, publicKey) { async function decryptSessionKey(encryptedKey) { try { - if (!SecurityUtils.isValidBase64(encryptedKey)) { - throw new Error('Invalid encrypted key format'); + console.log("Decrypting session key..."); + + const decryptedSessionKey = await unwrapAesKeyWithRsa(keyPair.privateKey, encryptedKey); + + // Store the decrypted session key + if (currentRoom) { + await keyStorage.storeSessionKey(currentRoom, decryptedSessionKey); } - 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"] - ); + return decryptedSessionKey; } catch (error) { console.error("Failed to decrypt session key:", error); throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR"); @@ -271,7 +534,8 @@ async function encryptMessage(message) { async function decryptMessage(encryptedData, ivString) { if (!sessionKey) { - return "[Encrypted message - no session key]"; + console.log("No session key available for decryption"); + return "[Encrypted message - failed to get decryption key]"; } try { @@ -298,7 +562,7 @@ async function decryptMessage(encryptedData, ivString) { } } -// Enhanced socket listeners with fixed key exchange +// Enhanced socket listeners with better debugging function setupSocketListeners() { socket.on('connect', () => { isConnected = true; @@ -368,8 +632,14 @@ function setupSocketListeners() { currentDisplayName = data.display_name; roomUsers = data.user_keys || {}; + // Try to load existing session key for this room + const storedSessionKey = await keyStorage.loadSessionKey(currentRoom); + if (storedSessionKey && !sessionKey) { + sessionKey = storedSessionKey; + console.log("Loaded stored session key for room:", currentRoom); + } + // 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'; @@ -387,12 +657,20 @@ function setupSocketListeners() { 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); + console.log("Using existing/stored session key"); + updateStatus("Session key available. You can chat.", false); updateInputState(); } else { - console.log("Waiting for session key from existing users"); - updateStatus("Waiting for session key from other users...", false); + console.log("Requesting session key from existing users"); + updateStatus("Requesting session key from other users...", false); + + // Explicitly request session key + const publicKeyString = await exportPublicKey(keyPair.publicKey); + socket.emit('request_session_key', { + room_id: currentRoom, + public_key: publicKeyString + }); + updateInputState(); } @@ -447,9 +725,9 @@ function setupSocketListeners() { socket.on('request_session_key', async (data) => { try { - console.log("Session key requested for new user:", data.new_user_id); + console.log("Session key requested by:", data.requester_user_id); - if (!sessionKey || !data.new_user_id || !data.public_key) { + if (!sessionKey || !data.requester_user_id || !data.public_key) { console.log("Cannot fulfill session key request - missing data"); return; } @@ -460,7 +738,7 @@ function setupSocketListeners() { socket.emit('share_session_key', { room_id: currentRoom, - target_user_id: data.new_user_id, + target_user_id: data.requester_user_id, encrypted_key: encryptedKey }); @@ -473,9 +751,10 @@ function setupSocketListeners() { } }); + // This is the critical event that was likely not firing properly socket.on('session_key_received', async (data) => { try { - console.log("Session key received from:", data.from_user_id); + console.log("session_key_received event fired:", data); if (!data || !data.encrypted_key) { console.log("Invalid session key data received"); @@ -484,21 +763,23 @@ function setupSocketListeners() { if (!sessionKey) { try { + console.log("Attempting to decrypt received session key"); sessionKey = await decryptSessionKey(data.encrypted_key); - console.log("Session key decrypted successfully"); + + 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); + updateStatus("Failed to decrypt session key - please try rejoining", 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); + console.error("Failed to process session_key_received:", error); + updateStatus("Error: Failed to process session key", true); } }); @@ -538,6 +819,7 @@ function setupSocketListeners() { }); } +// Rest of the functions remain the same... async function createRoom() { try { const roomId = SecurityUtils.generateSecureId(6); @@ -586,6 +868,11 @@ async function joinSpecificRoom(roomId, password = "") { try { updateStatus("Joining room...", false); + // Clear any existing session key when joining a new room + if (currentRoom !== roomId) { + sessionKey = null; + } + const publicKeyString = await exportPublicKey(keyPair.publicKey); socket.emit('join_room', { @@ -702,7 +989,7 @@ function addSystemMessage(text) { try { const container = document.getElementById('messagesContainer'); const systemMessage = document.createElement('div'); - systemMessage.className = 'message-group system'; // ✅ add system class + systemMessage.className = 'message-group system'; const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); @@ -726,7 +1013,6 @@ function addSystemMessage(text) { } } - function updateStatus(message, isError = false) { try { const statusEl = document.getElementById('statusText'); @@ -922,10 +1208,50 @@ document.addEventListener('securitypolicyviolation', (e) => { window.SecureChat = { joinRoom, createRoom, - sendMessage + sendMessage, + // Debug functions for Tauri + debugKeyStorage: keyStorage, + debugCurrentState: () => ({ + currentRoom, + hasSessionKey: !!sessionKey, + hasKeyPair: !!keyPair, + keysReady, + isConnected, + isTauri + }) }; -// Prevent console access in production -if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { +// Enhanced debugging for Tauri environment +if (isRunningInTauri) { + console.log('Tauri environment detected - enhanced debugging enabled'); + + // Make debugging functions available globally + window.debugBytechat = { + keyStorage, + getCurrentState: () => ({ + currentRoom, + currentUserId, + hasSessionKey: !!sessionKey, + hasKeyPair: !!keyPair, + keysReady, + isConnected, + roomUsers: Object.keys(roomUsers), + isRunningInTauri + }), + testKeyPersistence: async () => { + if (keyPair) { + console.log('Testing key persistence...'); + await keyStorage.storeKeyPair(keyPair); + const loaded = await keyStorage.loadKeyPair(); + console.log('Key persistence test:', loaded ? 'SUCCESS' : 'FAILED'); + return !!loaded; + } + return false; + } + }; +} + +// Prevent console access in production (skip for Tauri debugging) +if (!isRunningInTauri && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { console.log = console.warn = console.error = () => {}; } \ No newline at end of file