const channel = document.getElementById("channel"); const chatin = document.getElementById("chatin"); const chatoutput = document.getElementById("Chatoutput"); const status1 = document.getElementById("status"); let button1 = document.getElementById("button1"); const button2 = document.getElementById("button2"); const button3 = document.getElementById("button3"); const button4 = document.getElementById("button4"); 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 decrypted_chunks = []; let socket = new WebSocket("wss://" + location.host + "/ws"); //LAST CALL: initSocket(); //NOTHING CAN BE CALLED HERE! function verifySocket() { if (!ready) { socket = new WebSocket("wss://" + location.host + "/ws"); initSocket() } } async function initSocket() { socket.onopen = function (evt) { console.log("Websocket open"); ready = true; if (document.URL.includes("?id=")) { channel.value = document.URL.split("=")[1]; button2Handler() } } 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 { const transformed_data = await event.data.arrayBuffer(); await decrypt_and_queue(transformed_data); } 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] channel.value = uiid; button2.innerText = "Close channel"; button1.disabled = true; button1.innerText = "Create channel"; button1.title = "You have to close the channel before creating a new one, or join the channel on another device"; document.getElementById("link").innerHTML = ""; document.getElementById("link").innerHTML = `<a class="download" onclick="copy()">COPY LINK</a>`; } 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: ip= ${response[1]}, pin= ${uipin}`; console.log(askjoininfo); status1.value = askjoininfo; } else if (response[0] === "Joined") { id = channel.value; button2.innerText = "Close channel"; button2.disabled = false; button1.disabled = false; buttonchatout.disabled = false; status1.value = `${event.data}`; button1.innerText = "Open File"; button1.title = "You have to join the channel on another device before opening a file"; document.getElementById("filetd").innerHTML = ""; document.getElementById("filetd").innerHTML = `<input type="file" class="yesbutton" id="button1" onChange="button1Handler()"/>`; button1 = document.getElementById("button1"); await createKey(); } else if (response[0] === "Metadata") { //status1.value = `${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[3]; console.log(filename); status1.value = `size=${filesize} type=${filetype} name=${filename}`; document.getElementById("filetd").innerHTML = ""; document.getElementById("filetd").innerHTML = `<a class="download" id="button1" onclick="accept();">DOWNLOAD FILE!</a>`; button1 = document.getElementById("button1"); } 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); chatoutput.append("\n", recv_message); } else if (response[0] === "Sent") { document.getElementById("filetd").innerHTML = ""; document.getElementById("filetd").innerHTML = `<input type="file" class="yesbutton" id="button1" onChange="button1Handler()"/>`; button1 = document.getElementById("button1"); } else if (response[0] === "Accepted") { status1.value = `${event.data}`; await sendTheFile(); console.log("streaming file ..."); } else if (response[0] === "NoChannel") { alert("Enter a valid channel id!"); channel.value = ""; document.getElementById("filetd").innerHTML = "" document.getElementById("filetd").innerHTML = `<button class="yesbutton" id="button1" onclick="button1Handler()">Create channel</button>`; button2.innerText = "Join channel"; button2.disabled = false channel.value = ""; status1.value = "Disconnected"; document.getElementById("download").innerHTML = ""; channelClosedBySelf = true; button1 = document.getElementById("button1"); } else if (response[0] === "Closed") { if (channelClosedBySelf) { document.getElementById("download").innerHTML = ""; button1 = document.getElementById("button1"); button1.innerText = "Create channel"; } else { status1.value = `Peer closed connection`; document.getElementById("download").innerHTML = ""; document.getElementById("filetd").innerHTML = "" document.getElementById("filetd").innerHTML = `<button class="yesbutton" id="button1" onclick="button1Handler()">Create channel</button>`; button1 = document.getElementById("button1"); button1.disabled = true; button1.innerText = "Create channel"; button1.title = "You have to close the channel before creating a new one, or join the channel on another device"; buttonchatout.disabled = true; } channelClosedBySelf = false; } else if (response[0] === "Timeout") { alert("Connection expired"); } }); } async function startLoad() { channel.value = ""; status1.value = "Disconnected"; } async function buttonchatoutHandler() { verifySocket() if (!ready) { alert("socket not ready, refresh page!"); return; } const unencrypted_message = chatin.value; console.log(unencrypted_message); chatin.value = ""; const encrypted_message_arr = await encrypt(unencrypted_message); const encrypted_message = uint8ArrayTobase64(encrypted_message_arr); socket.send(`chatMessage ${serverid} ${encrypted_message}`); } async function button1Handler() { verifySocket() if (!ready) { alert("socket not ready, refresh page!"); return; } if (button1.innerText === "Create channel") { button1.disabled = true; const idtype = document.getElementById('idtype'); if (idtype.value == "words") { socket.send("createShortID") } else { socket.send("createID"); } } else { const fileInput = document.getElementById('button1'); const file = fileInput.files[0]; button1.disabled = true; console.log(file.name); let encrypted_file_name = await encrypt(file.name); encrypted_file_name = uint8ArrayTobase64(encrypted_file_name); socket.send(`fileMetadata ${serverid} ${file.size} ${file.type} ${encrypted_file_name}`); console.log(`fileMetadata ${serverid} ${file.size} ${file.type} ${encrypted_file_name}`); } } function button2Handler() { verifySocket() if (!ready) { alert("socket not ready, refresh page!"); return; } if (button2.innerText === "Close channel") { buttonchatout.disabled = true; socket.send("closeId " + serverid); console.log("close channel"); channel.value = ""; document.getElementById("filetd").innerHTML = "" document.getElementById("filetd").innerHTML = `<button class="yesbutton" id="button1" onclick="button1Handler()">Create channel</button>`; button2.innerText = "Join channel"; channel.value = ""; status1.value = "Disconnected"; document.getElementById("download").innerHTML = ""; channelClosedBySelf = true; button1 = document.getElementById("button1"); } else if (button2.innerText === "Join channel") { if (channel.value === "") { alert("Enter a valid channel id!"); return; } button1.disabled = true; button2.disabled = true; uiid = channel.value serverid = uiid.substring(0, uiid.lastIndexOf("-")); socket.send("joinID " + serverid); } else { console.log("Error, you broke my app!") } } 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(crypt_data) { const data = await decrypt(crypt_data); decrypted_chunks.push(data); //console.log("block size= ", data.byteLength); download_remaining_size -= data.byteLength; if (download_remaining_size == 0) { await save_file_to_disk(); } } async function sendTheFile() { const fileInput = document.getElementById('button1'); const file = fileInput.files[0]; const reader = file.stream().getReader(); let blocksSent = 0; reader.read().then(async function sendBlock({ done, value }) { if (done) { button1.disabled = false; return; } const header = `FileBlockWs ${serverid} ${blocksSent} \n`; const h = strToBytes(header); const encryptetvalue = await encrypt(value); 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"); } 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]; } } 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("filetd").innerHTML = ""; document.getElementById("filetd").innerHTML = `<a class="downloadD" id="button1">DOWNLOAD FILE!</a>`; socket.send("FileAccepted " + serverid); } function copy() { const text = `https://${location.host}/filestreamer?id=${channel.value}`; navigator.clipboard.writeText(text).then(function () { console.log('Async: Copying to clipboard was successful!'); }, function (err) { console.error('Async: Could not copy text: ', err); }); }