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);
});
}