1 Commits

Author SHA1 Message Date
68741badd8 Issue 1: Fixed ( Most of it )
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 11m9s
2025-08-27 17:29:22 +02:00
3 changed files with 405 additions and 64 deletions

View File

@@ -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()
}

View File

@@ -11,9 +11,10 @@
"windows": [
{
"title": "ByteChat",
"width": 800,
"height": 600,
"devtools": false
"width": 600,
"height": 700,
"devtools": false,
"zoomHotkeysEnabled": false
}
],
"security": {

View File

@@ -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 = () => {};
}