const status1 = document.getElementById("status"); const copybutton = document.getElementById("copy"); let ready = false; let channelClosedBySelf = false; let serverid = ""; let uiid = ""; let uipin = ""; let filename = ""; let filesize = 0; let download_remaining_size = 0; let filetype = ""; let key = false; let downloadId = ""; let processing = false; let encrypted_chunks = []; let decrypted_chunks = []; let socket = new WebSocket("wss://" + location.host + "/ws"); let index_downloads = 0; document.getElementById("filebutton").disabled = true; document.getElementById("close").disabled = true; document.getElementById("video-call-id").disabled = true; const settingsState = { 'end-to-end': false, }; const action_button = document.getElementById("action"); const text_area = document.getElementById("message-input"); const e2e = document.getElementById("e2e"); const chatMessages = document.getElementById('chat-messages'); let ise2e = true; text_area.value = ""; const create_channel = 0; const join_channel = 1; const send_message = 2; let chat_state = create_channel; toggleSetting(e2e); //LAST CALL: initSocket(); console.log("init done"); //NOTHING CAN BE CALLED HERE! function verifySocket() { if (!ready) { socket = new WebSocket("wss://" + location.host + "/ws"); initSocket() } } async function initSocket() { socket.onopen = async function (evt) { console.log("Websocket open"); ready = true; if (document.URL.includes("?id=")) { channel.value = document.URL.split("=")[1]; await buttonHandler(); } } socket.onclose = function (evt) { console.log("Websocket closed"); ready = false; } socket.onerror = function (evt) { console.log("Websocket error: " + evt.data); ready = false; } socket.addEventListener("message", async (event) => { //console.log(event.data.split('\n')[0]); if (typeof event.data === "object") { //TODO: more checks (event.data instanceof Blob) //console.log("received blob"); try { encrypted_chunks.push(event.data); await decrypt_and_queue(); } catch (error) { console.log("Error decrypt and download:", error); } return; } const response = event.data.split(" "); if (response[0] === "Created") { uiid = response[1] + "-" + randomId(4); serverid = response[1]; status1.innerText = "id: " + uiid; copybutton.hidden = false console.log("Created", uiid); system_message(`Created channed ID, which can be used to join:
${uiid}`); document.getElementById("close").disabled = false; } else if (response[0] === "AskPin") { uipin = prompt("give pin"); if (uipin.length != 9) { alert("Wrong pin length"); } else { let pin = uipin.substring(0, uipin.lastIndexOf("-")); let askpinmessage = "Pin " + serverid + " " + pin; console.log(askpinmessage); socket.send(askpinmessage); } } else if (response[0] === "AskJoin") { const clientpin = randomPin(4); uipin = `${response[2]}-${clientpin}`; askjoininfo = `Enter Pin for other client, pin: ${uipin}`; console.log(askjoininfo); system_message(escapeHtml(`IP=${response[1]}`)); status1.innerText = askjoininfo; } else if (response[0] === "Joined") { id = status1.value; status1.innerText = `${event.data}`; document.getElementById("filebutton").disabled = false; document.getElementById("filebutton").innerHTML = ""; document.getElementById("filebutton").innerHTML = ``; system_message(event.data); document.getElementById("close").disabled = false; await createKey(); chat_state = send_message; } else if (response[0] === "Metadata") { //console.log(event.data); const splittedmetadata = event.data.split(":"); let encrypted_file_name = splittedmetadata[2]; encrypted_file_name = base64ToUint8Array(encrypted_file_name); const rawfilename = await decrypt(encrypted_file_name); filename = new TextDecoder().decode(rawfilename); filesize = Number(splittedmetadata[0].split(" ")[1]); download_remaining_size = filesize; filetype = splittedmetadata[1]; downloadId = splittedmetadata[4]; const file_is_encrypted = (splittedmetadata[3] === 'true') console.log(filename); const incoming_file_info = `size=${filesize} type=${filetype} name=${filename} encrypted=${file_is_encrypted}`; status1.innerText = incoming_file_info other_message(incoming_file_info); if (file_is_encrypted) { system_message(`DOWNLOAD FILE!`, `button${index_downloads}`); } else { system_message(`DOWNLOAD FILE!`, `button${index_downloads}`); } } else if (response[0] === "Message") { const splittedmessagedata = event.data.split(" "); let encrypted_message = splittedmessagedata[1]; encrypted_message = base64ToUint8Array(encrypted_message); const rawmessage = await decrypt(encrypted_message); recv_message = new TextDecoder().decode(rawmessage); console.log(recv_message); other_message(recv_message); } else if (response[0] === "SentWs") { encrypted_chunks.push("EOF"); await decrypt_and_queue(); console.log("file received SentWs"); } else if (response[0] === "Sent") { console.log("file received Sent"); } else if (response[0] === "Accepted") { status1.innerText = `${event.data}`; user_message("streaming file ..."); console.log("streaming file ..."); await sendTheFile(); user_message("file sent"); } else if (response[0] === "NoChannel") { alert("Enter a valid channel id!"); close_channel(); status1.innerText = "Disconnected"; channelClosedBySelf = true; } else if (response[0] === "Closed") { if (channelClosedBySelf) { console.log("closed by self") status1.innerText = "You closed connection"; } else { status1.innerText = "Peer closed connection"; close_channel(); } channelClosedBySelf = false; } else if (response[0] === "Timeout") { alert("Connection expired"); } else { alert("unknown message: ", response[0]) } chatHandler(); }); } async function filebuttonhandler() { const fileInput = document.getElementById('fileopen'); const file = fileInput.files[0]; console.log(file.name); let encrypted_file_name = await encrypt(file.name); encrypted_file_name = uint8ArrayTobase64(encrypted_file_name); ise2e = settingsState['end-to-end']; socket.send(`fileMetadata ${serverid} ${file.size} ${file.type} ${encrypted_file_name} ${ise2e}`); console.log(`fileMetadata ${serverid} ${file.size} ${file.type} ${encrypted_file_name} ${ise2e}`); user_message(`Offered file: ${file.name}`); } function close_channel() { if (document.getElementById(`button${index_downloads}`) !== null) { document.getElementById(`button${index_downloads}`).innerHTML = ""; document.getElementById(`button${index_downloads}`).innerHTML = `Downloaded`; } ready = false; serverid = ""; uiid = ""; uipin = ""; filename = ""; filesize = 0; download_remaining_size = 0; filetype = ""; key = false; downloadId = ""; decrypted_chunks = []; index_downloads = 0; document.getElementById("filebutton").disabled = true; document.getElementById("close").disabled = true; action_button.disabled = false; } function close_button() { socket.send("closeId " + serverid); console.log("close channel"); close_channel(); channelClosedBySelf = true; } async function buttonHandler() { verifySocket() if (!ready) { alert("socket not ready, refresh page!"); return; } if (chat_state === create_channel) { socket.send("createShortID"); console.log("createShortID"); } else if (chat_state === join_channel) { uiid = text_area.value serverid = uiid.substring(0, uiid.lastIndexOf("-")); socket.send("joinID " + serverid); text_area.value = ""; } else if (chat_state === send_message) { await sendMessage(); } chatHandler(); } function chatHandler() { if (chat_state === create_channel && text_area.value.length > 0) { action_button.innerText = "Join"; chat_state = join_channel; action_button.disabled = false; } else if (chat_state === create_channel && text_area.value.length === 0) { action_button.disabled = true; } else if (chat_state === join_channel && text_area.value.length === 0) { action_button.innerText = "Create Channel"; chat_state = create_channel; action_button.disabled = false; } else if (chat_state === send_message) { action_button.disabled = false; action_button.innerText = "Send Message"; if (text_area.value.length === 0) { text_area.value = ""; text_area.placeholder = "Type a message..."; } } } function escapeHtml(text) { const div = document.createElement('div'); div.innerText = text; return div.innerHTML; } function user_message(msg) { const userMessage = document.createElement('div'); userMessage.className = 'message user'; const timestamp = new Date().toLocaleTimeString(); userMessage.innerHTML = `${escapeHtml(msg)}${timestamp}`; chatMessages.appendChild(userMessage); } function other_message(msg) { const timestamp = new Date().toLocaleTimeString(); const otherMessage = document.createElement('div'); otherMessage.className = 'message other'; otherMessage.innerHTML = `${escapeHtml(msg)}${timestamp}`; chatMessages.appendChild(otherMessage); chatMessages.scrollTop = chatMessages.scrollHeight; } function system_message(msg, id = null) { const timestamp = new Date().toLocaleTimeString(); const otherMessage = document.createElement('div'); otherMessage.className = 'message system'; if (id != null) { otherMessage.innerHTML = `${msg}${timestamp}`; } else { otherMessage.innerHTML = `${msg}${timestamp}`; } chatMessages.appendChild(otherMessage); } async function sendMessage() { verifySocket() if (!ready) { alert("socket not ready, refresh page!"); return; } const input = document.getElementById('message-input'); const unencrypted_message = input.value.trim(); if (!unencrypted_message) return; console.log(unencrypted_message); const encrypted_message_arr = await encrypt(unencrypted_message); const encrypted_message = uint8ArrayTobase64(encrypted_message_arr); socket.send(`chatMessage ${serverid} ${encrypted_message}`); // Add user's message user_message(unencrypted_message) input.value = ''; chatMessages.scrollTop = chatMessages.scrollHeight; //chatHandler(); //TODO: why is this commented out? } function showSettings() { //console.log("clicked hamburger") const settingsPanel = document.getElementById('settings-panel'); const hamburger = document.getElementById('hamburger'); if (settingsPanel.classList.contains('active')) { //console.log("remove hamburger") settingsPanel.classList.remove('active'); document.getElementById('settings-panel').style.display = 'none'; } else { //console.log("add hamburger") settingsPanel.classList.add('active'); document.getElementById('settings-panel').style.display = 'block'; } } function toggleSetting(button) { const setting = button.getAttribute('data-setting'); settingsState[setting] = !settingsState[setting]; button.classList.toggle('active', settingsState[setting]); } //old helpers: function base64ToUint8Array(base64) { const binaryString = atob(base64); const byteArray = new Uint8Array(binaryString.length); for (let i = 0; i != binaryString.length; i++) { byteArray[i] = binaryString.charCodeAt(i); } return byteArray; } function uint8ArrayTobase64(arr) { return btoa(String.fromCharCode(...arr)); } async function generateKey(keyString) { // Derive a key from the string const encoder = new TextEncoder(); let keyData = encoder.encode(keyString); if (keyData.length != 32) { const hash = await window.crypto.subtle.digest('SHA-256', keyData); keyData = new Uint8Array(hash); } return await window.crypto.subtle.importKey( "raw", // Raw key material keyData, // Key bytes { name: "AES-GCM" }, // Algorithm false, // Non-extractable ["encrypt", "decrypt"] // Allowed operations ); } async function createKey() { console.log("creating key"); key = await generateKey(`${uiid}${uipin}${downloadId}`); console.log("key is ", key); } async function encrypt(block_data) { if (typeof block_data === "string") { block_data = new TextEncoder().encode(block_data); block_data = new Uint8Array(block_data); } const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted_data = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv, }, key, block_data, ); const combined_data = new Uint8Array(iv.length + encrypted_data.byteLength); combined_data.set(new Uint8Array(iv), 0); combined_data.set(new Uint8Array(encrypted_data), iv.length); return combined_data; } async function decrypt(crypt_data) { const iv = crypt_data.slice(0, 12); const encrypted_data = crypt_data.slice(12); const decrypted_data = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv, }, key, encrypted_data ); return decrypted_data; } async function decrypt_and_queue() { if (processing) { return true; } processing = true; while (encrypted_chunks.length > 0) { const datapart = encrypted_chunks.shift(); if (typeof datapart === 'string' && datapart === "EOF") { await save_file_to_disk(); break; } const transformed_data = await datapart.arrayBuffer(); const data = await decrypt(transformed_data); decrypted_chunks.push(data); //console.log("block size= ", data.byteLength); download_remaining_size -= data.byteLength; } if (download_remaining_size == 0) { //changed to EOF console.log("File should be ready"); } processing = false; } async function sendTheFile() { const fileInput = document.getElementById('fileopen'); const file = fileInput.files[0]; const reader = file.stream().getReader(); let blocksSent = 0; reader.read().then(async function sendBlock({ done, value }) { if (done) { if (ise2e) { socket.send(`FileSentWs ${serverid}`); console.log(`FileSentWs ${serverid}`); } else { socket.send(`FileSent ${serverid}`); console.log(`FileSent ${serverid}`); } return; } let header = null; let encryptetvalue = null; if (ise2e) { header = `FileBlockWs ${serverid} ${blocksSent} \n`; encryptetvalue = await encrypt(value); } else { header = `FileBlock ${serverid} ${blocksSent} \n`; encryptetvalue = value; } const h = strToBytes(header); const packet = new Uint8Array(h.length + encryptetvalue.byteLength); packet.set(new Uint8Array(h), 0); packet.set(encryptetvalue, h.length); socket.send(packet) blocksSent++; return reader.read().then(sendBlock); }) } async function save_file_to_disk() { try { const decrypted_blob = new Blob(decrypted_chunks, { type: filetype }); const url = URL.createObjectURL(decrypted_blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); alert("file decrypted and downloaded succesfully"); system_message("file decrypted and downloaded succesfully"); decrypted_chunks = []; } catch (error) { console.error("Error downloading file: ", error); alert("download failed:", error) } } function strToBytes(s) { let string = []; for (let i = 0; i != s.length; i++) { string.push(s.charCodeAt(i)); } return string; } function randomId(length) { const chars = "abcdefghijklmnopqrstuvwxyz"; let result = ""; if (window.crypto && window.crypto.getRandomValues) { // Use crypto.getRandomValues const array = new Uint8Array(length); window.crypto.getRandomValues(array); result = Array.from(array, (value) => chars[value % chars.length]).join(""); } else { // Fallback method (Math.random) console.log("Alternative random generator") let j = 0; while (j != length) { j++; const randomIndex = Math.floor(Math.random() * chars.length); result += chars[randomIndex]; } } console.log("randomId", result); return result; } function randomPin(length) { const chars = "0123456789"; let result = ""; if (window.crypto && window.crypto.getRandomValues) { // Use crypto.getRandomValues const array = new Uint8Array(length); window.crypto.getRandomValues(array); result = Array.from(array, (value) => chars[value % chars.length]).join(""); } else { // Fallback method (Math.random) console.log("Alternative random generator") let j = 0; while (j != length) { j++; const randomIndex = Math.floor(Math.random() * chars.length); result += chars[randomIndex]; } } return result; } function accept() { document.getElementById(`button${index_downloads}`).innerHTML = ""; document.getElementById(`button${index_downloads}`).innerHTML = `Downloaded`; index_downloads += 1; socket.send("FileAccepted " + serverid); } function copy() { let text = status1.innerText; text = text.split(":")[1].trim() navigator.clipboard.writeText(text).then(function () { console.log('Async: Copying to clipboard was successful!'); }, function (err) { console.error('Async: Could not copy text: ', err); }); }