6 Commits

Author SHA1 Message Date
ff05ffbb5e Merge branch 'main' of https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/bytechat-desktop
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 11m6s
2025-08-28 22:21:09 +02:00
04068b3e96 Issue 1: Completely Fixed Now. 2025-08-28 22:19:45 +02:00
df59ae8792 fixed CI pipeline
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 11m13s
2025-08-27 19:12:19 +00:00
526e5a2f63 fixed the CI pipeline again 2025-08-27 21:11:40 +02:00
9145be78a3 GPG - KeySign and runnable Linux ELF Binary
Some checks failed
Build Tauri App (Linux + Windows exe) / build (push) Failing after 1m11s
2025-08-27 21:08:04 +02:00
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
10 changed files with 854 additions and 90 deletions

View File

@@ -33,7 +33,8 @@ jobs:
build-essential \ build-essential \
curl \ curl \
pkg-config \ pkg-config \
xdg-utils xdg-utils \
gnupg2
- name: Add Windows Rust target - name: Add Windows Rust target
run: rustup target add x86_64-pc-windows-gnu run: rustup target add x86_64-pc-windows-gnu
@@ -51,17 +52,58 @@ jobs:
cd src-tauri cd src-tauri
cargo build --release --target x86_64-pc-windows-gnu 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 - name: Upload Linux packages
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: tauri-linux name: tauri-linux
path: | path: |
src-tauri/target/release/bytechat-desktop 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/*.deb
src-tauri/target/release/bundle/deb/*.asc
src-tauri/target/release/bundle/rpm/*.rpm 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 - name: Upload Windows exe
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: tauri-windows-exe 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

View File

@@ -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
View File

@@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=4tP+
-----END PGP PUBLIC KEY BLOCK-----

29
src-tauri/Cargo.lock generated
View File

@@ -346,6 +346,7 @@ dependencies = [
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-store",
] ]
[[package]] [[package]]
@@ -3725,6 +3726,22 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-store"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
dependencies = [
"dunce",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.16",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.8.0" version = "2.8.0"
@@ -3944,9 +3961,21 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"slab", "slab",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.16" version = "0.7.16"

View File

@@ -22,4 +22,5 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tauri-plugin-store = "2"

View File

@@ -2,9 +2,12 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": [
"main"
],
"permissions": [ "permissions": [
"core:default", "core:default",
"opener:default" "opener:default",
"store:default"
] ]
} }

View File

@@ -1,6 +1,7 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@@ -1,6 +1,19 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![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() { fn main() {
disable_dmabuf_if_true();
bytechat_desktop_lib::run() bytechat_desktop_lib::run()
} }

View File

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

View File

@@ -1,9 +1,36 @@
// Tauri-compatible ByteChat client
// Fixes localStorage/sessionStorage issues and WebCrypto compatibility
// Content Security Policy enforcement // Content Security Policy enforcement
if (!window.crypto || !window.crypto.subtle) { if (!window.crypto || !window.crypto.subtle) {
alert('This browser does not support required cryptographic features. Please use a modern browser.'); alert('This browser does not support required cryptographic features. Please use a modern browser.');
throw new Error('WebCrypto API not available'); 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 // Global variables
let socket = null; let socket = null;
let currentRoom = null; let currentRoom = null;
@@ -23,6 +50,364 @@ const MAX_MESSAGE_LENGTH = 4000;
const MAX_ROOM_ID_LENGTH = 32; const MAX_ROOM_ID_LENGTH = 32;
const MAX_MESSAGES = 512; const MAX_MESSAGES = 512;
// Enhanced keyStorage with better error handling and forced saves
const keyStorage = {
// Store RSA keypair as JWK for persistence
async initTauriStore() {
if (!isRunningInTauri) return null;
try {
// Try modern Tauri store API first
if (window.__TAURI_PLUGIN_STORE__) {
const { Store } = window.__TAURI_PLUGIN_STORE__;
return await Store.load('bytechat-keys.json');
}
// Try legacy API
else if (window.__TAURI__ && window.__TAURI__.store) {
const { Store } = window.__TAURI__.store;
return await Store.load('bytechat-keys.json');
}
// Try direct access to store
else if (window.__TAURI__ && window.__TAURI__.invoke) {
// Use invoke API directly
return {
get: async (key) => {
try {
return await window.__TAURI__.invoke('plugin:store|get', { key });
} catch (e) {
console.warn('Store get failed:', e);
return null;
}
},
set: async (key, value) => {
try {
await window.__TAURI__.invoke('plugin:store|set', { key, value });
return true;
} catch (e) {
console.warn('Store set failed:', e);
return false;
}
},
delete: async (key) => {
try {
await window.__TAURI__.invoke('plugin:store|delete', { key });
return true;
} catch (e) {
console.warn('Store delete failed:', e);
return false;
}
}
};
}
} catch (e) {
console.warn('Tauri store initialization failed:', e);
}
return null;
},
// 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) {
try {
const store = await this.initTauriStore();
if (store) {
await store.set('rsa_keypair', keyData);
console.log('RSA keypair stored in Tauri store');
return true;
}
} catch (storeError) {
console.warn('Failed to use Tauri store, falling back to memory:', storeError);
}
}
// Fallback to memory storage
this._memoryKeys = keyData;
console.log('RSA keypair stored in memory (no persistent storage)');
return false;
} catch (error) {
console.error('Failed to store keypair:', error);
throw error;
}
},
// Fixed loadKeyPair function with correct extractable settings
async loadKeyPair() {
try {
let keyData;
if (isRunningInTauri) {
try {
const store = await this.initTauriStore();
if (store) {
keyData = await store.get('rsa_keypair');
if (keyData) {
console.log('RSA keypair loaded from Tauri store');
}
}
} catch (storeError) {
console.warn('Failed to read from Tauri store:', storeError);
}
}
// Fallback to memory if Tauri store failed
if (!keyData) {
keyData = this._memoryKeys;
if (keyData) {
console.log('RSA keypair loaded from memory');
}
}
if (!keyData || !keyData.publicKey || !keyData.privateKey) {
console.log('No stored keypair found');
return null;
}
// Import keys from JWK - CRITICAL: Make public key extractable for export
const publicKey = await window.crypto.subtle.importKey(
'jwk',
keyData.publicKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
true, // Make extractable so we can export it later
['encrypt']
);
const privateKey = await window.crypto.subtle.importKey(
'jwk',
keyData.privateKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false, // Private key doesn't need to be extractable for our use case
['decrypt']
);
return { publicKey, privateKey };
} catch (error) {
console.error('Failed to load keypair:', error);
return null;
}
},
// Also fix the generateKeyPair function to ensure consistent extractable settings
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()
};
let stored = false;
if (isRunningInTauri) {
try {
const store = await this.initTauriStore();
if (store) {
await store.set(`session_key_${roomId}`, sessionData);
console.log('✅ Session key stored in Tauri store for room:', roomId);
// Verify storage worked
const verification = await store.get(`session_key_${roomId}`);
if (verification) {
stored = true;
} else {
throw new Error('Session key storage verification failed');
}
}
} catch (storeError) {
console.warn('❌ Failed to store session key in Tauri store:', storeError);
}
}
if (!stored) {
// Fallback to memory
this._sessionKeys = this._sessionKeys || {};
this._sessionKeys[roomId] = sessionData;
console.log('📝 Session key stored in memory for room:', roomId);
}
return stored;
} catch (error) {
console.error('❌ Critical error storing session key:', error);
throw error;
}
},
async loadSessionKey(roomId) {
try {
let sessionData;
if (isRunningInTauri) {
try {
const store = await this.initTauriStore();
if (store) {
sessionData = await store.get(`session_key_${roomId}`);
if (sessionData) {
console.log('✅ Session key loaded from Tauri store for room:', roomId);
}
}
} catch (storeError) {
console.warn('⚠️ Failed to load session key from Tauri store:', storeError);
}
}
// Fallback to memory if Tauri store failed
if (!sessionData) {
this._sessionKeys = this._sessionKeys || {};
sessionData = this._sessionKeys[roomId];
if (sessionData) {
console.log('📝 Session key loaded from memory for room:', 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' },
true, // Make extractable so we can re-store if needed
['encrypt', 'decrypt']
);
console.log('✅ Session key successfully imported for room:', roomId);
return sessionKey;
} catch (error) {
console.error('❌ Failed to load session key for room', roomId, ':', error);
return null;
}
},
async clearSessionKey(roomId) {
try {
if (isRunningInTauri) {
try {
const store = await this.initTauriStore();
if (store) {
await store.delete(`session_key_${roomId}`);
console.log('🗑️ Session key cleared from Tauri store for room:', roomId);
}
} catch (storeError) {
console.warn('Failed to clear from Tauri store:', storeError);
}
}
if (this._sessionKeys && this._sessionKeys[roomId]) {
delete this._sessionKeys[roomId];
console.log('🗑️ Session key cleared from memory for room:', roomId);
}
} catch (error) {
console.error('❌ Failed to clear session key for room', roomId, ':', error);
}
},
async forceSave() {
// Modern Tauri stores auto-save, this is mainly for compatibility
return true;
},
_memoryKeys: null,
_sessionKeys: {}
};
async function generateKeyPair() {
try {
console.log('=== Starting key generation process ===');
updateStatus('Loading or generating encryption keys...', false);
// 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 AND export
["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');
}
// Test that public key is extractable
try {
await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
console.log("✅ Public key export test passed");
} catch (exportError) {
console.error("❌ Public key is not extractable:", exportError);
throw new Error('Public key must be extractable for protocol to work');
}
keysReady = true;
updateStatus("Ready to join a room", false);
console.log("✅ Key pair ready - can now join rooms");
console.log('=== Key generation process complete ===');
} catch (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
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"
},
true, // Must be extractable for export
["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");
}
}
}
// Security utilities // Security utilities
const SecurityUtils = { const SecurityUtils = {
sanitizeInput: function(input) { sanitizeInput: function(input) {
@@ -64,15 +449,14 @@ class SecureChatError extends Error {
} }
} }
// Initialize the app // Define your backend server
// Define your backend server (for now ngrok)
const SERVER_URL = "https://kind-mosquito-multiply.ngrok-free.app"; const SERVER_URL = "https://kind-mosquito-multiply.ngrok-free.app";
async function initializeApp() { async function initializeApp() {
try { 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, { socket = io(SERVER_URL, {
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
reconnection: true, reconnection: true,
@@ -80,9 +464,17 @@ async function initializeApp() {
reconnectionDelay: 1000, reconnectionDelay: 1000,
reconnectionDelayMax: 5000, reconnectionDelayMax: 5000,
timeout: 10000, 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(); await generateKeyPair();
setupSocketListeners(); setupSocketListeners();
setupSecurityFeatures(); setupSecurityFeatures();
@@ -93,7 +485,6 @@ async function initializeApp() {
} }
} }
function setupSecurityFeatures() { function setupSecurityFeatures() {
document.addEventListener('visibilitychange', function() { document.addEventListener('visibilitychange', function() {
if (document.hidden) { if (document.hidden) {
@@ -107,14 +498,11 @@ function setupSecurityFeatures() {
window.addEventListener('beforeunload', function() { window.addEventListener('beforeunload', function() {
clearSensitiveData(); clearSensitiveData();
}); });
const securityIndicator = document.getElementById('securityIndicator');
//securityIndicator.style.display = 'block';
} }
function clearSensitiveData() { function clearSensitiveData() {
sessionKey = null; sessionKey = null;
keyPair = null; // Don't clear keyPair as we want to reuse it
roomUsers = {}; roomUsers = {};
const passwordInput = document.getElementById('roomPasswordInput'); const passwordInput = document.getElementById('roomPasswordInput');
@@ -123,43 +511,37 @@ function clearSensitiveData() {
} }
} }
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");
}
}
// Enhanced generateSessionKey with guaranteed storage
async function generateSessionKey() { async function generateSessionKey() {
try { try {
console.log("🔧 Generating new session key...");
sessionKey = await window.crypto.subtle.generateKey( sessionKey = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, { name: "AES-GCM", length: 256 },
true, true, // Make extractable for storage
["encrypt", "decrypt"] ["encrypt", "decrypt"]
); );
console.log("Session key generated");
if (!sessionKey) {
throw new Error('Session key generation returned null/undefined');
}
console.log("✅ Session key generated");
// CRITICAL: Immediately store the session key
if (currentRoom) {
console.log("💾 Storing newly generated session key...");
await keyStorage.storeSessionKey(currentRoom, sessionKey);
await keyStorage.forceSave();
console.log("✅ Session key stored and saved successfully");
} else {
console.warn("⚠️ No current room - session key not stored");
}
return sessionKey; return sessionKey;
} catch (error) { } catch (error) {
console.error("Failed to generate session key:", error); console.error("Failed to generate session key:", error);
throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR"); throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR");
} }
} }
@@ -201,6 +583,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) { async function encryptSessionKey(sessionKey, publicKey) {
try { try {
const keyData = await window.crypto.subtle.exportKey("raw", sessionKey); const keyData = await window.crypto.subtle.exportKey("raw", sessionKey);
@@ -216,28 +634,22 @@ async function encryptSessionKey(sessionKey, publicKey) {
} }
} }
// Enhanced decryptSessionKey function
async function decryptSessionKey(encryptedKey) { async function decryptSessionKey(encryptedKey) {
try { try {
if (!SecurityUtils.isValidBase64(encryptedKey)) { console.log("🔓 Decrypting session key...");
throw new Error('Invalid encrypted key format');
const decryptedSessionKey = await unwrapAesKeyWithRsa(keyPair.privateKey, encryptedKey);
if (!decryptedSessionKey) {
throw new Error('Failed to unwrap session key - got null/undefined result');
} }
const keyData = Uint8Array.from(atob(encryptedKey), c => c.charCodeAt(0)); console.log("✅ Session key decrypted successfully");
const decrypted = await window.crypto.subtle.decrypt( return decryptedSessionKey;
{ name: "RSA-OAEP" },
keyPair.privateKey,
keyData
);
return await window.crypto.subtle.importKey(
"raw",
decrypted,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
} catch (error) { } catch (error) {
console.error("Failed to decrypt session key:", error); console.error("Failed to decrypt session key:", error);
throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR"); throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR");
} }
} }
@@ -271,7 +683,8 @@ async function encryptMessage(message) {
async function decryptMessage(encryptedData, ivString) { async function decryptMessage(encryptedData, ivString) {
if (!sessionKey) { 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 { try {
@@ -298,7 +711,7 @@ async function decryptMessage(encryptedData, ivString) {
} }
} }
// Enhanced socket listeners with fixed key exchange // Enhanced socket listeners with better debugging
function setupSocketListeners() { function setupSocketListeners() {
socket.on('connect', () => { socket.on('connect', () => {
isConnected = true; isConnected = true;
@@ -351,6 +764,7 @@ function setupSocketListeners() {
} }
}); });
// Enhanced room_joined handler with better key loading
socket.on('room_joined', async (data) => { socket.on('room_joined', async (data) => {
try { try {
if (data.error) { if (data.error) {
@@ -368,8 +782,19 @@ function setupSocketListeners() {
currentDisplayName = data.display_name; currentDisplayName = data.display_name;
roomUsers = data.user_keys || {}; roomUsers = data.user_keys || {};
// ENHANCED: Try to load existing session key for this room FIRST
console.log("🔍 Checking for existing session key for room:", currentRoom);
const storedSessionKey = await keyStorage.loadSessionKey(currentRoom);
if (storedSessionKey) {
sessionKey = storedSessionKey;
console.log("✅ Using stored session key for room:", currentRoom);
} else {
console.log("❌ No stored session key found for room:", currentRoom);
sessionKey = null; // Explicitly clear
}
// Switch to chat screen // 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('welcomeScreen').style.display = 'none';
document.getElementById('chatScreen').style.display = 'flex'; document.getElementById('chatScreen').style.display = 'flex';
document.getElementById('roomInfo').style.display = 'flex'; document.getElementById('roomInfo').style.display = 'flex';
@@ -382,17 +807,49 @@ function setupSocketListeners() {
// Handle session key based on user status // Handle session key based on user status
if (data.is_first_user) { if (data.is_first_user) {
console.log("First user - generating session key"); console.log("👑 First user - generating new session key");
await generateSessionKey(); await generateSessionKey();
// Key is stored in generateSessionKey()
updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false); updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false);
updateInputState(); updateInputState();
} else if (sessionKey) { } else if (sessionKey) {
console.log("Rejoining with existing session key"); console.log("✅ Using existing/stored session key");
updateStatus("Session key present. You can chat.", false); updateStatus("Session key available. You can chat.", false);
updateInputState(); updateInputState();
} else { } else {
console.log("Waiting for session key from existing users"); console.log("❓ Need session key - requesting from existing users");
updateStatus("Waiting for session key from other users...", false); updateStatus("Requesting session key from other users...", false);
// Explicitly request session key with retry logic
const publicKeyString = await exportPublicKey(keyPair.publicKey);
socket.emit('request_session_key', {
room_id: currentRoom,
public_key: publicKeyString
});
// Set up retry mechanism for session key request
let retryCount = 0;
const maxRetries = 3;
const retryInterval = setInterval(() => {
if (sessionKey) {
clearInterval(retryInterval);
return;
}
retryCount++;
if (retryCount <= maxRetries) {
console.log(`🔄 Retrying session key request (${retryCount}/${maxRetries})`);
socket.emit('request_session_key', {
room_id: currentRoom,
public_key: publicKeyString
});
} else {
clearInterval(retryInterval);
console.log("❌ Max session key request retries reached");
updateStatus("Failed to obtain session key. Try leaving and rejoining the room.", true);
}
}, 3000);
updateInputState(); updateInputState();
} }
@@ -405,10 +862,10 @@ function setupSocketListeners() {
} }
} }
console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user); console.log(`🏠 Room joined successfully. SessionKey: ${!!sessionKey}, FirstUser: ${data.is_first_user}`);
} catch (error) { } catch (error) {
console.error("Error processing room_joined:", error); console.error("Error processing room_joined:", error);
updateStatus("Error joining room", true); updateStatus("Error joining room", true);
updateInputState(); updateInputState();
} }
@@ -447,9 +904,9 @@ function setupSocketListeners() {
socket.on('request_session_key', async (data) => { socket.on('request_session_key', async (data) => {
try { 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"); console.log("Cannot fulfill session key request - missing data");
return; return;
} }
@@ -460,7 +917,7 @@ function setupSocketListeners() {
socket.emit('share_session_key', { socket.emit('share_session_key', {
room_id: currentRoom, room_id: currentRoom,
target_user_id: data.new_user_id, target_user_id: data.requester_user_id,
encrypted_key: encryptedKey encrypted_key: encryptedKey
}); });
@@ -473,32 +930,62 @@ function setupSocketListeners() {
} }
}); });
// Enhanced session_key_received handler with better storage
socket.on('session_key_received', async (data) => { socket.on('session_key_received', async (data) => {
try { try {
console.log("Session key received from:", data.from_user_id); console.log("🔑 session_key_received event fired for room:", currentRoom);
if (!data || !data.encrypted_key) { if (!data || !data.encrypted_key) {
console.log("Invalid session key data received"); console.log("Invalid session key data received");
return; return;
} }
if (!sessionKey) { if (!sessionKey) {
try { try {
sessionKey = await decryptSessionKey(data.encrypted_key); console.log("🔓 Decrypting received session key...");
console.log("Session key decrypted successfully"); const decryptedKey = await decryptSessionKey(data.encrypted_key);
if (!decryptedKey) {
throw new Error('Decryption returned null/undefined key');
}
// Set the session key
sessionKey = decryptedKey;
// CRITICAL: Explicitly store the session key immediately
console.log("💾 Storing decrypted session key...");
await keyStorage.storeSessionKey(currentRoom, sessionKey);
// Modern Tauri store auto-saves
await keyStorage.forceSave();
console.log("✅ Session key decrypted, stored, and saved successfully!");
updateStatus("Session key received! You can now chat securely.", false); updateStatus("Session key received! You can now chat securely.", false);
updateInputState(); updateInputState();
addSystemMessage("🔑 Session key received - you can now chat!"); addSystemMessage("🔑 Session key received - you can now chat!");
// Verify storage by attempting to reload
if (isRunningInTauri) {
console.log("🔍 Verifying session key storage...");
const verificationKey = await keyStorage.loadSessionKey(currentRoom);
if (verificationKey) {
console.log("✅ Session key storage verification successful");
} else {
console.error("❌ Session key storage verification failed");
updateStatus("Warning: Session key may not be saved", true);
}
}
} catch (error) { } catch (error) {
console.error("Failed to decrypt received session key:", error); console.error("Failed to process received session key:", error);
updateStatus("Failed to decrypt session key", true); updateStatus("Failed to decrypt session key - please try rejoining", true);
} }
} else { } else {
console.log("Session key already exists, ignoring duplicate"); console.log(" Session key already exists, ignoring duplicate");
} }
} catch (error) { } catch (error) {
console.error("Failed to process session key:", error); console.error("❌ Critical error in session_key_received:", error);
updateStatus("Error: Failed to decrypt session key", true); updateStatus("Error: Failed to process session key", true);
} }
}); });
@@ -538,6 +1025,7 @@ function setupSocketListeners() {
}); });
} }
// Rest of the functions remain the same...
async function createRoom() { async function createRoom() {
try { try {
const roomId = SecurityUtils.generateSecureId(6); const roomId = SecurityUtils.generateSecureId(6);
@@ -586,6 +1074,11 @@ async function joinSpecificRoom(roomId, password = "") {
try { try {
updateStatus("Joining room...", false); 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); const publicKeyString = await exportPublicKey(keyPair.publicKey);
socket.emit('join_room', { socket.emit('join_room', {
@@ -702,7 +1195,7 @@ function addSystemMessage(text) {
try { try {
const container = document.getElementById('messagesContainer'); const container = document.getElementById('messagesContainer');
const systemMessage = document.createElement('div'); 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'}); const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
@@ -726,7 +1219,6 @@ function addSystemMessage(text) {
} }
} }
function updateStatus(message, isError = false) { function updateStatus(message, isError = false) {
try { try {
const statusEl = document.getElementById('statusText'); const statusEl = document.getElementById('statusText');
@@ -896,6 +1388,26 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
// Add periodic session key backup for Tauri
if (isRunningInTauri) {
// Save session keys periodically in case of unexpected shutdown
setInterval(async () => {
if (sessionKey && currentRoom) {
try {
await keyStorage.storeSessionKey(currentRoom, sessionKey);
console.log("🔄 Periodic session key backup completed");
} catch (error) {
console.warn("⚠️ Periodic session key backup failed:", error);
}
}
}, 30000); // Every 30 seconds
// Note: Modern Tauri store auto-saves on app close
window.addEventListener('beforeunload', async () => {
console.log("🔄 App closing - Tauri store will auto-save");
});
}
// Periodic connection health check // Periodic connection health check
setInterval(() => { setInterval(() => {
if (socket && isConnected) { if (socket && isConnected) {
@@ -922,10 +1434,111 @@ document.addEventListener('securitypolicyviolation', (e) => {
window.SecureChat = { window.SecureChat = {
joinRoom, joinRoom,
createRoom, createRoom,
sendMessage sendMessage,
// Debug functions for Tauri
debugKeyStorage: keyStorage,
debugCurrentState: () => ({
currentRoom,
hasSessionKey: !!sessionKey,
hasKeyPair: !!keyPair,
keysReady,
isConnected,
isTauri: isRunningInTauri
})
}; };
// Prevent console access in production // Enhanced debugging for Tauri environment
if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { 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;
},
// Test session key persistence
testSessionKeyPersistence: async () => {
if (!sessionKey || !currentRoom) {
console.log("❌ No session key or room to test");
return false;
}
console.log("🧪 Testing session key persistence...");
try {
// Store current session key
await keyStorage.storeSessionKey(currentRoom, sessionKey);
await keyStorage.forceSave();
// Try to load it back
const loaded = await keyStorage.loadSessionKey(currentRoom);
if (loaded) {
console.log("✅ Session key persistence test: SUCCESS");
return true;
} else {
console.log("❌ Session key persistence test: FAILED");
return false;
}
} catch (error) {
console.error("❌ Session key persistence test error:", error);
return false;
}
},
// Force session key request
forceRequestSessionKey: async () => {
if (!currentRoom || !keyPair) {
console.log("❌ Cannot request session key - missing room or keypair");
return;
}
console.log("🔄 Forcing session key request...");
const publicKeyString = await exportPublicKey(keyPair.publicKey);
socket.emit('request_session_key', {
room_id: currentRoom,
public_key: publicKeyString
});
},
// Manual session key storage
forceStoreSessionKey: async () => {
if (!sessionKey || !currentRoom) {
console.log("❌ No session key or room to store");
return false;
}
try {
await keyStorage.storeSessionKey(currentRoom, sessionKey);
console.log("✅ Manual session key storage completed");
return true;
} catch (error) {
console.error("❌ Manual session key storage failed:", error);
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 = () => {}; console.log = console.warn = console.error = () => {};
} }