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 = `COPY LINK`;
} 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 = ``;
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 = `DOWNLOAD FILE!`;
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 = ``;
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 = ``;
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 = ``;
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 = ``;
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 = `DOWNLOAD FILE!`;
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);
});
}