Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df59ae8792 | |||
| 9145be78a3 | |||
| 68741badd8 |
@@ -33,7 +33,8 @@ jobs:
|
||||
build-essential \
|
||||
curl \
|
||||
pkg-config \
|
||||
xdg-utils
|
||||
xdg-utils \
|
||||
gnupg2
|
||||
|
||||
- name: Add Windows Rust target
|
||||
run: rustup target add x86_64-pc-windows-gnu
|
||||
@@ -51,17 +52,58 @@ jobs:
|
||||
cd src-tauri
|
||||
cargo build --release --target x86_64-pc-windows-gnu
|
||||
|
||||
- name: Make Linux ELF executable
|
||||
run: chmod +x src-tauri/target/release/bytechat-desktop
|
||||
|
||||
# 🔑 Import your private GPG key
|
||||
- name: Import GPG key
|
||||
run: |
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
echo "$GPG_KEY_ID:6:" | gpg --import-ownertrust
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
|
||||
|
||||
# 🐧 Sign Linux binaries (.deb, .rpm, ELF)
|
||||
- name: Sign Linux artifacts
|
||||
run: |
|
||||
for f in src-tauri/target/release/bytechat-desktop \
|
||||
src-tauri/target/release/bundle/deb/*.deb \
|
||||
src-tauri/target/release/bundle/rpm/*.rpm; do
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback --detach-sign -a "$f"
|
||||
done
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
|
||||
# 🪟 Sign Windows exe with GPG
|
||||
- name: Sign Windows exe
|
||||
run: |
|
||||
exe=src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe
|
||||
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
|
||||
--pinentry-mode loopback --detach-sign -a "$exe"
|
||||
env:
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
|
||||
# 📦 Upload Linux artifacts + signatures + public key
|
||||
- name: Upload Linux packages
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: tauri-linux
|
||||
path: |
|
||||
src-tauri/target/release/bytechat-desktop
|
||||
src-tauri/target/release/bytechat-desktop.asc
|
||||
src-tauri/target/release/bundle/deb/*.deb
|
||||
src-tauri/target/release/bundle/deb/*.asc
|
||||
src-tauri/target/release/bundle/rpm/*.rpm
|
||||
src-tauri/target/release/bundle/rpm/*.asc
|
||||
key/bytechat-public.gpg
|
||||
|
||||
# 📦 Upload Windows exe + signature
|
||||
- name: Upload Windows exe
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: tauri-windows-exe
|
||||
path: src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe
|
||||
path: |
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe
|
||||
src-tauri/target/x86_64-pc-windows-gnu/release/bytechat-desktop.exe.asc
|
||||
|
||||
13
README.md
13
README.md
@@ -1 +1,12 @@
|
||||
# Tauri Desktop Application
|
||||
# Tauri Desktop Application
|
||||
|
||||
This is the Desktop App for [ByteChat](https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/bytechat) ,it supports Windows and Linux for now. And probably the future
|
||||
|
||||
## Recent Focus:
|
||||
|
||||
- [x] - GPG Key Signage
|
||||
|
||||
### Stack
|
||||
|
||||
Idk Rust javascript html and css. just look at the repo.
|
||||
and tauri
|
||||
51
key/bytechat-public.gpg
Normal file
51
key/bytechat-public.gpg
Normal file
@@ -0,0 +1,51 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=4tP+
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "ByteChat",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"devtools": false
|
||||
"width": 600,
|
||||
"height": 700,
|
||||
"devtools": false,
|
||||
"zoomHotkeysEnabled": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
448
src/main.js
448
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 = () => {};
|
||||
}
|
||||
Reference in New Issue
Block a user