diff --git a/.gitignore b/.gitignore index 4139860..99d27f4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ page/final/* data/* dataold/ server_data.txt +db/* diff --git a/Cargo.toml b/Cargo.toml index 081794a..518e6cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "publichat" version = "0.1.0" edition = "2021" -default-run= "client" +default-run= "server" [dependencies] aes = "0.8.1" @@ -10,7 +10,7 @@ base64 = "0.13.0" ctr = "0.9.1" sha1_smol = "1.0.0" sha3 = "0.10.1" -crossterm = "0.25" # TODO: optional for client only? +crossterm = "0.25" rand = "0.8.5" # TODO: this too ed25519-dalek = "1.0.1" diff --git a/build.rs b/build.rs deleted file mode 100644 index 3de8e78..0000000 --- a/build.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Build script generates combined & minified html/js files - -use std::{fs::File, io::Write}; - -#[cfg(feature = "minify")] -use minify_html::{Cfg, minify}; - -#[cfg(feature = "minify")] -const CONFIG: Cfg = Cfg { - do_not_minify_doctype: true, - ensure_spec_compliant_unquoted_attribute_values: true, - keep_closing_tags: false, - keep_html_and_head_opening_tags: true, - keep_spaces_between_attributes: true, - keep_comments: false, - minify_css: true, - minify_js: true, - remove_bangs: true, - remove_processing_instructions: true, -}; - -const SCRIPT_TAG: &str = r#""#; - -fn main() { - // This could be done in a loop, but since some - // files need special treatment, we do one at a time. - // I don't care about efficiency in the build script... - - // prepare data for html file names - let suf = if cfg!(feature = "tls") {"-tls"} else {""}; - let ext = if cfg!(feature = "minify") {".min.html"} else {".html"}; - - // 404 page - just minify - println!("cargo:rerun-if-changed=page/404.html"); - let html_404 = include_bytes!("page/404.html").as_slice(); - #[cfg(feature = "minify")] let html_404 = &minify(html_404, &CONFIG); - let mut file = File::create(["target/404", ext].concat()).unwrap(); - file.write_all(html_404).unwrap(); - - // client.js - load into html pages - // can't be minified properly on its own - println!("cargo:rerun-if-changed=page/client.js"); - let mut js_client = r#""); - #[cfg(feature = "tls")] let js_client = js_client.replace("ws://", "wss://"); - - // index - load, minify - println!("cargo:rerun-if-changed=page/index.html"); - let html_index = include_str!("page/index.html").replace(SCRIPT_TAG, &js_client); - let html_index_data = html_index.as_bytes(); - #[cfg(feature = "minify")] let html_index_data = &minify(html_index_data, &CONFIG); - let mut file = File::create(["target/index", suf, ext].concat()).unwrap(); - file.write_all(html_index_data).unwrap(); - - // mobile - load, minify (copy paste of index) - println!("cargo:rerun-if-changed=page/mobile.html"); - let html_mobile = include_str!("page/mobile.html").replace(SCRIPT_TAG, &js_client); - let html_mobile_data = html_mobile.as_bytes(); - #[cfg(feature = "minify")] let html_mobile_data = &minify(html_mobile_data, &CONFIG); - let mut file = File::create(["target/mobile", suf, ext].concat()).unwrap(); - file.write_all(html_mobile_data).unwrap(); -} diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/page/404.html b/page/404.html deleted file mode 100644 index e0a52a3..0000000 --- a/page/404.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - 404 - - - - - -
-
404
-
- The page or file you were looking for does not exist.

:( -
-
- - diff --git a/page/client.js b/page/client.js deleted file mode 100644 index ae2539f..0000000 --- a/page/client.js +++ /dev/null @@ -1,569 +0,0 @@ -main = function() { - const landing_page_str = [` - PubliChat is a semi-private chatting application. - Chats are encrypted with their title as a key. - Every chat is accessible to anyone, provided they know the chat's title. - The title is never sent to the server, so the server can't decrypt the chats. - This way, the server does not need to be trusted. - Enter a chat title on the top to fetch messages start reading and - enter a username and message on the bottom to send something. - Some example usages of publi.chat is the following.`, - `Chat securely and privately by picking a secure title - (like a strong password) - Make a private note for yourself by picking a secure secret title - Discuss topics in 'public' chats with insecure titles - (eg. 'Baking', 'Fishing' or 'Chess') - Discuss webpages with no comments section (set the title to page's url)`, -]; - const message_byte_size = 512; - const message_content_length = 396; - const cypher_length = message_content_length + 4 + 8 + 32; - const fch_pad = [102, 99, 104]; // "fch" - const qry_pad = [113, 114, 121]; // "qry" - const snd_pad = [115, 110, 100]; // "snd" - const end_pad = [101, 110, 100]; // "end" - const rcv_pad = [109, 115, 103]; // "msg" - var max_message_id = Number.MIN_SAFE_INTEGER; - var min_message_id = Number.MAX_SAFE_INTEGER; - var chat_id_hash = []; // hash of current chat id - var style = getComputedStyle(document.body); - var send_button = document.getElementById("send_button"); - var socket_button = document.getElementById("socket_button"); - var sending_div = document.getElementById("sending_div"); - var message_entry = document.getElementById("message_entry"); - let message_list_div = document.getElementById("message_list"); - send_button.onclick = function() {send_message()}; - socket_button.onclick = function() {toggle_loop();}; - message_list_div.addEventListener("scroll", top_scroll_query); - message_entry.addEventListener("keyup", keystroke_input); - - const utf8decoder = new TextDecoder(); - const utf8encoder = new TextEncoder(); - var reader = new FileReader(); - var socket = null; - var loop = false; - var recv_packets = 0; - var caught_timeout = false; - - var cur_status; - const status_data = { - undefined: ["", [0], ""], - 0: ["--status_wait", [0, 1, 4], "Connecting to server"], - 1: ["--status_ok", [2, 3, 4], "Everything's working fine"], - 2: ["--status_wait", [1, 4], "Fetching paused. Click me to re-enable"], - 3: ["--status_wait", [1, 4], "Not receiving updates from server, check internet connection"], - 4: ["--status_error", [0], "Connection severed. Click me to reconnect"], - }; - - open_socket(); - - // *******************************HELPERS************************************ - function get_title(){return document.getElementById("title").value;} - function get_password(){return document.getElementById("password").value;} - function get_message(){return message_entry.value;} - - function unpack_number(bytes) { - var res = 0; - for (var i = 0; i < bytes.length; i++) { - res *= 256; // same as res << 8 but also works for number > 32 bit - res += bytes[i]; - } - return res; - }; - function pack_number(num, size) { - var res = []; - var num_copy = num; - for (var i = 0; i < size; i++) { - res.unshift(num & 0xff); - num = Math.floor(num / 256); // same as >> 8 but works for ints > 32 bit - } - if (num > 0) { - console.log("warning: num too big for array", num_copy, size); - } - return res; - }; - function white_or_black(colour) { // which text colour gives more contrast - var r = parseInt(colour.slice(1,3), 16); - var g = parseInt(colour.slice(3,5), 16); - var b = parseInt(colour.slice(5,7), 16); - return ((r*0.299 + g*0.587 + b*0.114) > 150) ? "#000000" : "#ffffff"; - } - - // *******************************SET_STATUS********************************* - function set_status(status) { - let [, succ, ] = status_data[cur_status]; - if (!status in succ) { return; } // skip illegal transitions - - cur_status = status; - let [colour, , title] = status_data[status]; - socket_button.style.background = style.getPropertyValue(colour); - socket_button.title = title; - }; - function expect_response(identifer) { - var received_packets = recv_packets; - // within 2 seconds, recv_packets should have been incremented - setTimeout(()=>{ - if ( - received_packets == recv_packets // nothing new has come - && !caught_timeout // not already in timeout state - && loop // not in paused state - && socket.readyState == WebSocket.OPEN // not in shutdown state - ) { - set_status(3); - caught_timeout = true; - console.log("Response timed out: " + identifer); - } - }, 2000); - }; - - // *******************************OPEN_SOCKET******************************** - function open_socket() { - set_status(0); - socket = new WebSocket("ws://" + location.host + "/ws"); - socket.onopen = function() { - console.log("socket opened"); - setTimeout(function() {loop = true;}, 1000); - set_status(1); - }; - socket.onerror = function(e) {shutdown(e)}; - socket.onclose = function(e) {shutdown(e)}; - socket.onmessage = function(e) {ws_receive(e)}; - reset_chat(); - - if (get_title() === "") { - landing_page(); - } - }; - function ws_send(bytes) { - if (socket.readyState != WebSocket.OPEN) { - shutdown("Tried sending to dead socket"); - return; - } - var outgoing = new Uint8Array(bytes); - socket.send(outgoing); - }; - - // *********************************SHUTDOWN/RESET*************************** - function shutdown(e) { - loop = false; - set_status(4); // red button top left - send_button.style.backgroundColor = style.getPropertyValue("--status_err"); - if (typeof e != "string") {console.log("ws error! "+e.code+e.reason);} - else {console.log(e);} - }; - function reset_chat(){ - message_list_div.replaceChildren(); - max_message_id = Number.MIN_SAFE_INTEGER; - min_message_id = Number.MAX_SAFE_INTEGER; - }; - - // *********************************BUTTONS********************************** - function toggle_loop() { - if (socket.readyState != WebSocket.OPEN) { - loop = false; - open_socket(); - } else { - loop = !loop; - set_status({true: 1, false: 2}[loop]); - } - }; - - // *********************************RECEVING********************************* - function ws_receive(message_event) { - set_status(1); - recv_packets += 1; - caught_timeout = false; - - var blob = message_event.data; - reader.readAsArrayBuffer(blob); - }; - reader.onload = function() { - var result = reader.result; - var bytes_u8_array = new Uint8Array(result); - var bytes = Array.from(bytes_u8_array); - // read packet header - var msg_padding = bytes.splice(0, 3); - var chat_id_byte = bytes.splice(0, 1); - var message_id = unpack_number(bytes.splice(0, 3)); - var message_count_and_direction = bytes.splice(0, 1)[0]; - var message_count = message_count_and_direction & 0x7f; - var build_upwards = (message_count_and_direction & 0x80) == 0; - - if (msg_padding[0] != rcv_pad[0]) {shutdown("incorrect smrt pad 1");} - if (msg_padding[1] != rcv_pad[1]) {shutdown("incorrect smrt pad 2");} - if (msg_padding[2] != rcv_pad[2]) {shutdown("incorrect smrt pad 3");} - if (chat_id_byte != chat_id_hash[0]) {return;} - if (message_count*message_byte_size != bytes.length) {return} - - if (message_count === 0) {return;} - - if (build_upwards) { - max_message_id = Math.max(max_message_id, message_id + message_count-1); - min_message_id = message_id; - } else { - max_message_id = message_id + message_count - 1; - min_message_id = Math.min(min_message_id, message_id); - } - - read_message_bytes(bytes, build_upwards); - }; - - function read_message_bytes(bytes, build_upwards) { - if (bytes == null || bytes == []) {console.log("Received empty");return;} - // Checks current scroll height BEFORE the message is added - var scroll_pos = message_list_div.scrollTop+message_list_div.clientHeight; - var scroll_down = scroll_pos > (message_list_div.scrollHeight * 0.90); - var scroll_up = message_list_div.scrollTop < 10; - var scroll_target = null; - - if (build_upwards) { // insert at top; read messages backwards - scroll_target = message_list_div.children[0]; - while (bytes.length > 0) { - var single_message = bytes.splice(-message_byte_size); - new_message_div = bytes_to_message(single_message); - message_list_div.prepend(new_message_div); - } - } else { // insert at bottom; read messages normally - while (bytes.length > 0) { - var single_message = bytes.splice(0, message_byte_size); - new_message_div = bytes_to_message(single_message); - message_list_div.appendChild(new_message_div); - scroll_target = new_message_div; - } - } - // scroll to bottom if user is already at bottom - if ((scroll_down || scroll_up) && scroll_target != null) { - scroll_target.scrollIntoView(); - } - }; - function verify_signature(pub_key_bytes, hash, signature) { - var ec = new elliptic.eddsa('ed25519'); - var key = ec.keyFromPublic(pub_key_bytes, 'bytes'); - try { - return key.verify(hash, signature); - } catch(e) { - return false; - } - }; - function verify_time(server_time, client_time) { - // server & client time stamp can have a max of 10 seconds difference - var res = Math.abs(server_time-client_time) < 1000*10; - return res; - }; - function verify_chat_key(chat_key_4bytes) { - var expected = get_chat_key().splice(0,4); - var res = true; - for (let i = 0; i < chat_key_4bytes.length; i++) { - res = res && (expected[i] == chat_key_4bytes[i]); - } - return res; - }; - function unpad_message(padded_message, chat_key) { - var padding_marker = chat_key[0]; - for (var i = padded_message.length - 1; i >= 0; i--) { - if (padded_message[i] == padding_marker) { break; } - } - if (i <= 0) { - // message 0 length or no pad charachter => error - console.log("Warning: Message with invalid padding.") - return []; - } - return padded_message.slice(0, i); - } - function bytes_to_message(bytes) { - // Break message server side - var server_time = unpack_number(bytes.splice(0, 8)); // 8 bytes - var cypher_block = bytes.splice(0, cypher_length); // 440 needs splicing - var signature = bytes.splice(0, 64); - var bytes_hash = sha3_256.array(cypher_block); - - // decrypt message - var cnt = new aesjs.Counter(1); - var aes_cnt = new aesjs.ModeOfOperation.ctr(get_chat_key(), cnt); - var decrypted_bytes = Array.from(aes_cnt.decrypt(cypher_block)); - - // Break message client side - var chat_key_4bytes = decrypted_bytes.splice(0, 4); // 4 bytes - var client_time = unpack_number(decrypted_bytes.splice(0, 8)); // 8 bytes - var public_key = decrypted_bytes.splice(0, 32); // 32 bytes - var padded_bytes = decrypted_bytes.splice(0, message_content_length);// 396 - // username string - var username_colour = aesjs.utils.hex.fromBytes(public_key.slice(29, 32)); - var username_str = btoa(String.fromCharCode(...public_key)); - // date string - var date = new Date(Number(server_time)); - var today = new Date(); - if (date.toDateString() === today.toDateString()) { // sent today - var date_str = ""; - } else if (date.getFullYear() === today.getFullYear()) { // sent this year - var date_str = date.toLocaleString().slice(0, -15); - } else { - var date_str = date.toLocaleString().slice(0, -10); // date < this year - } - date_str += " " + date.toLocaleTimeString().slice(0, -3); - // message string remove padding - var message_bytes = unpad_message(padded_bytes, chat_key_4bytes); - var message_str = utf8decoder.decode(new Uint8Array(message_bytes)); - - var [msg_div, sig_div] = build_msg( - username_str, - username_colour, - date_str, - message_str - ); - setTimeout( - ()=>verify_message( - sig_div, public_key, bytes_hash, signature, - server_time, client_time, chat_key_4bytes - ), 0 - ); // this is to make the signature checking async to the building of msg - return msg_div; - }; - function build_msg(username_str, user_colour, date_str, message_str) { - var msg_div = document.createElement("div"); - var usr_div = document.createElement("div"); - var time_div = document.createElement("div"); - var content_div = document.createElement("div"); - - msg_div.className = "message"; - usr_div.className = "username"; - time_div.className = "time"; - content_div.className = "content"; - - var bg_colour = "#" + user_colour; - usr_div.style.background = bg_colour; - usr_div.style.color = white_or_black(bg_colour); // selects best contrast - usr_div.textContent = username_str.substring(0, 15).replaceAll('=', ''); - time_div.textContent = date_str; - content_div.textContent = message_str; - msg_div.appendChild(usr_div); - msg_div.appendChild(time_div); - msg_div.appendChild(content_div); - return [msg_div, time_div]; - }; - function verify_message( - time_div, public_key, bytes_hash, signature, - server_time, client_time, chat_key_4bytes - ) { - // Verifies the time, sign., and chat id - // also adds check mark to each message - let reason = "Message verified!"; - let chat_verified = verify_chat_key(chat_key_4bytes); - let time_verified = verify_time(server_time, client_time); - let sig_verified = verify_signature(public_key, bytes_hash, signature); - var verified = chat_verified && time_verified && sig_verified; - if (!verified) { - console.log( - "Message from: ", aesjs.utils.hex.fromBytes(public_key).slice(0, 20), - "\nCould not be verified because:", - "\nChat check: ", chat_verified, - "\nTime check: ", time_verified, - "\nSignature check: ", sig_verified, - ); - reason = "Possible attack, take caution!" - if (!chat_verified) { - reason += "\n- Message sent to wrong chat.\n" + - "An impersonator may have copied this va lid message from a different chat.\n" + - "May have happened if you switched chats too quickly."; - } - if (!time_verified) { - reason += "\n- Message sent at strange time.\n" + - "An impersonator may have resent an old message from this chat.\n" + - "May have happened due to poor connection or strange time settings."; - } - if (!sig_verified) { - reason += "\n- Message signed incorrectly.\n" + - "This message may have been alterd and cannot be trusted."; - } - } - time_div.appendChild(make_verify_mark(verified, reason)); - }; - function make_verify_mark(is_verified, reason) { - var main_div = document.createElement("div"); - var circle = document.createElement("div"); - var stem = document.createElement("div"); - var kick = document.createElement("div"); - main_div.className = "checkmark"; - circle.className = "checkmark_circle"; - stem.className = "checkmark_stem"; - kick.className = "checkmark_kick"; - - var checkmark_colour = "--status_ok"; - if (!is_verified) { - checkmark_colour = "--status_err"; - } - circle.style.background = style.getPropertyValue(checkmark_colour); - main_div.appendChild(circle); - main_div.appendChild(stem); - main_div.appendChild(kick); - - main_div.title = reason; - return main_div; - }; - - // *********************************MAINLOOP********************************* - function mainloop(old_title) { - var title = get_title(); - if (title == "") { - landing_page(); - setTimeout(function() {mainloop(title);}, 1000); - return; - } - if (loop == false) { - setTimeout(function() {mainloop(title);}, 1000); - return; - } - // check if chat title has changed (and we have received essages) - if (title == old_title && max_message_id >= min_message_id) { - query_messages(false); // false means new messages - } else { - // update chat list to new title - reset_chat(); - fetch_messages(); - } - setTimeout(function() {mainloop(title);}, 500); - }; - - function landing_page() { - reset_chat(); - for (let msg_str of landing_page_str) { - var [msg_div, time_div] = build_msg( - "Admin", - "991133", - "2022-03-01 13:37", - msg_str - ); - - time_div.appendChild(make_verify_mark(true)); - message_list_div.appendChild(msg_div); - } - } - // *********************************QUERY/FETCH****************************** - function fetch_messages() { - var chat_id = get_chat_id(); - chat_id_hash = chat_id; - ws_send([].concat(fch_pad, chat_id, end_pad)); - expect_response("fetch"); - }; - function query_messages(up) { - var chat_id = get_chat_id(); - if (up) { // query messages upward (old messages) - var query = [0x7f].concat(pack_number(min_message_id, 3)); - } else { // query messages downward (new messages) - var query = [0xff].concat(pack_number(max_message_id, 3)); - } - ws_send([].concat(qry_pad, chat_id, query, end_pad)); - expect_response("query"); - }; - function top_scroll_query(e) { - if (message_list_div.scrollTop == 0 && max_message_id > min_message_id) { - query_messages(get_title(), true); - } - }; - - // *********************************SENDING********************************** - function send_message() { - var chat_id = get_chat_id(); - var cypher = create_cypher_block(); - if (cypher == null) {return;} - // counter_div.textContent = "0/" + message_content_length; - - var signature = sign(cypher); - outbound_bytes = [].concat(snd_pad, chat_id, cypher, signature, end_pad); - - ws_send(outbound_bytes); - message_entry.value = ""; - }; - function pad_message(message, chat_key) { - var message = utf8encoder.encode(message); // make message into array - - var padding_marker = chat_key[0]; - var padding_length = message_content_length - message.length; - var padding = new Uint8Array(padding_length); - - var possible_bytes = new Uint8Array(256 - 1); // each byte 0-254 (no 255) - for (var i = 0; i < possible_bytes.length; i++) {possible_bytes[i] = i;} // [0, 1, 2, ... 254] - possible_bytes[padding_marker] = 255; // exclude padding_marker - - padding[0] = padding_marker; // First byte should be the padding marker - for (var i = 1; i < padding_length; i++) { - var rand_byte = Math.floor(Math.random() * 256) // evenly distributed on ints [0, 255] (inclusive) - padding[i] = possible_bytes[rand_byte]; // on [0, 255] \ {marker} - } - - // GRISHA'S BETTER SOLUTION - // for (var i = 0; i < padding_length; i++) { - // var rand_byte = Math.floor(Math.random() * 255) // evenly distributed on ints [0, 254] - // padding[i] = (rand_byte + (255-padding_marker)) % 256; // on [0, 255] \ {marker} - // } - - // concatinate the arrays - var padded_message = new Uint8Array(message_content_length); - padded_message.set(message); - padded_message.set(padding, message.length); - return padded_message; - }; - function sign(cypher) { - //var EdDSA = require('elliptic').eddsa; - var ec = new elliptic.eddsa('ed25519'); - var secret = get_password(); - var hashed_secret = sha3_256.array(utf8encoder.encode(secret)) - var key_pair = ec.keyFromSecret(hashed_secret); - var cypher_hash = sha3_256.array(cypher); - var signature = key_pair.sign(cypher_hash).toBytes(); - return signature; - }; - function get_public_key() { - var ec = new elliptic.eddsa('ed25519'); - var secret = get_password(); - var hashed_secret = sha3_256.array(utf8encoder.encode(secret)) - var key_pair = ec.keyFromSecret(hashed_secret); - return key_pair.pubBytes(); - }; - function get_time_array() { - const time = Date.now(); - return pack_number(time, 8); - }; - function get_chat_key() { - var title = get_title(); - return sha3_256.array(title); - } - function get_chat_id() { - return sha3_256.array(get_chat_key()); - }; - function create_cypher_block() { - var message = get_message(); // known by peers - if (message == "") {return null;} - var chat_key = get_chat_key(); // known by peers - var message_data = [].concat( - chat_key.slice(0,4), // 4 bytes - get_time_array(), // 8 bytes - get_public_key(), // 32 bytes - ...pad_message(message, chat_key) // 396 bytes - ); - var cnt = new aesjs.Counter(1); - var aes_cnt = new aesjs.ModeOfOperation.ctr(chat_key, cnt); - return Array.from(aes_cnt.encrypt(message_data)); - }; - - // *******************************CHAR_COUNTER******************************* - //var counter_div = document.getElementById("content_counter"); - function keystroke_input(event) { - // send with enter (enter == 13) - if(event.keyCode === 13) {send_message();} - // update colour and value of message length counter - var textLength = message_entry.value.length; - //counter_div.textContent = textLength + "/" + (message_content_length-1); - if(textLength >= message_content_length - 10){ - //sending_div.style.borderColor = style.getPropertyValue("--status_err"); - send_button.style.background = style.getPropertyValue("--status_err"); - //counter_div.style.color = "#ff2851"; - } else { - //sending_div.style.borderColor = style.getPropertyValue("--borders1"); - send_button.style.background = style.getPropertyValue("--borders1"); - //counter_div.style.color = "#757575"; - } - }; - - mainloop(""); -}; diff --git a/page/favicon.ico b/page/favicon.ico deleted file mode 100644 index b27e0f7..0000000 Binary files a/page/favicon.ico and /dev/null differ diff --git a/page/index.html b/page/index.html deleted file mode 100644 index 2885990..0000000 --- a/page/index.html +++ /dev/null @@ -1,266 +0,0 @@ - - - - Publichat - - - - - - - - - - - - - -
-
- -
- -
- -
-
-
Admin
-
13:37 - 3 March 2022
-
- Publichat is an end to end encrypted chat website where users can enter a chatroom based on a title. The messages are encrypted with the title as the key using AES and the title is hashed with SHA3 before it is all send to the server. As a result the server cannot read the chat content or know what the shared title was. - - Enter a title above and press play to join a chat. -
-
-
-
Admin
-
13:37 - 3 March 2022
-
- Enter "about" in the title field to learn more about this project. - You can also find the source code on GitHub. -
-
-
- -
- -
- -
-
-
-
-
-
-
- -
- - diff --git a/page/mobile.html b/page/mobile.html deleted file mode 100644 index f3ea899..0000000 --- a/page/mobile.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - Publichat - - - - - - - - - - - - - - -
-
- -
- -
- -
-
-
Admin
-
13:37 - 3 March 2022
-
- Publichat is an end to end encrypted chat website where users can enter a chatroom based on a title. The messages are encrypted with the title as the key using AES and the title is hashed with SHA3 before it is all send to the server. As a result the server cannot read the chat content or know what the shared title was. - - Enter a title above and press play to join a chat. -
-
-
-
Admin
-
13:37 - 3 March 2022
-
- Enter "about" in the title field to learn more about this project. - You can also find the source code on GitHub. -
-
-
- -
- -
- -
-
-
-
-
-
-
- -
- - diff --git a/page/tools.html b/page/tools.html deleted file mode 100644 index 4d50685..0000000 --- a/page/tools.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - Publichat - - - - - - - - - - - Title: -
chat key: -
chat id : -
base 64 : -

- Username: -
private: -
public : -
display: -

- - - diff --git a/src/bin/client/comm.rs b/src/bin/client/comm.rs deleted file mode 100644 index c71f841..0000000 --- a/src/bin/client/comm.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::net::TcpStream; - -use publichat::helpers::*; -use publichat::buffers::{ - cypher::Buf as CypherBuf, - hash::Buf as HashBuf, - msg_in_c as msg_in, - fetch, - query, -}; - -use crate::crypt::ed25519::SigBuf; - -pub fn send_msg( - stream: &mut TcpStream, - chat: &HashBuf, - cypher: &CypherBuf, - signature: &SigBuf, -) -> Res { - // TODO: rename constants to something direction-agnostic - // TODO: create global buffer builder functions - let mut buf = msg_in::PREPAD; - let (cid_buf, cy_buf, sig_buf) = msg_in::pad_split_mut(&mut buf); - - cid_buf.copy_from_slice(chat); - cy_buf.copy_from_slice(cypher); - sig_buf.copy_from_slice(signature); - - full_write(stream, &buf, "Failed to send message") -} - -pub fn send_fetch(stream: &mut TcpStream, chat: &HashBuf) -> Res { - let mut buf = fetch::PREPAD; - let (cid_buf,) = fetch::pad_split_mut(&mut buf); - - cid_buf.copy_from_slice(chat); - - full_write(stream, &buf, "Failed to send fetch") -} - -pub fn send_query( - stream: &mut TcpStream, - chat: &HashBuf, - forwards: bool, - count: u8, - id: u32, -) -> Res { - if count > 0x7f || id > 0xffffff { return Err("Query input too large") } - let mut buf = query::PREPAD; - let (cid_buf, args_buf, mid_buf) = query::pad_split_mut(&mut buf); - - cid_buf.copy_from_slice(chat); - args_buf[0] = if forwards {count | 0x80} else {count}; - mid_buf.copy_from_slice(&id.to_be_bytes()[1..]); - - full_write(stream, &buf, "Failed to send query") -} diff --git a/src/bin/client/common.rs b/src/bin/client/common.rs deleted file mode 100644 index ffc5b28..0000000 --- a/src/bin/client/common.rs +++ /dev/null @@ -1,20 +0,0 @@ -use std::{time::Duration, collections::VecDeque}; - -use publichat::buffers::hash::Buf as HashBuf; -use crate::msg::Message; - -pub const FQ_DELAY: Duration = Duration::from_millis(200); - -const DISP_FPS: u64 = 100; -pub const _DISP_DELAY: Duration = Duration::from_millis(1000 / DISP_FPS); - -pub struct GlobalState { - pub queue: VecDeque, - pub chat_key: HashBuf, - pub chat_id: HashBuf, - pub min_id: u32, - pub max_id: u32, // inclusive -} - -pub const VERIFY_TOLERANCE_MS: u64 = 10 * 1000; // time between server and client -pub const USER_ID_CHAR_COUNT: usize = 15; // how many b64 chars are displayed diff --git a/src/bin/client/crypt.rs b/src/bin/client/crypt.rs deleted file mode 100644 index c64b2a9..0000000 --- a/src/bin/client/crypt.rs +++ /dev/null @@ -1,74 +0,0 @@ -pub mod sha { - use sha3::{Sha3_256, Digest}; - use publichat::buffers::hash; - - pub fn hash(data: &[u8]) -> hash::Buf { - let mut res = hash::DEFAULT; - - let mut hasher = Sha3_256::new(); - hasher.update(data); - res.copy_from_slice(&hasher.finalize()); - - res - } -} - -pub mod aes { - use aes::{Aes256, cipher::{KeyIvInit, StreamCipher}}; - use ctr::Ctr128BE; - use publichat::buffers::{hash::Buf as HashBuf, cypher::Buf as CypherBuf}; - - pub fn apply(key: &HashBuf, buf: &mut CypherBuf) { - // applies AES in-place on buf as side-effect - const IV: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; - - // TODO: key is always the same; generate decrypter once in main? - let mut cypher = Ctr128BE::::new(key.into(), &IV.into()); - cypher.apply_keystream(buf); - } -} - -pub mod ed25519 { - pub use ed25519_dalek::Keypair; // allow use outside - use ed25519_dalek::{ - SecretKey, - PublicKey, - Signature, - SIGNATURE_LENGTH, - Signer, - Verifier, - }; - use publichat::buffers::{hash::Buf as HashBuf, cypher::Buf as CypherBuf}; - - pub type SigBuf = [u8; SIGNATURE_LENGTH]; - - pub fn make_keypair(input: &[u8]) -> Result { - // hash input data to get a neat 32 bytes - let hash = super::sha::hash(input); - - let secret = SecretKey::from_bytes(&hash) - .map_err(|_| "Failed to make private key")?; - let public = PublicKey::from(&secret); - - Ok(Keypair{secret, public}) - } - - pub fn sign(cypher: &CypherBuf, keypair: &Keypair) -> SigBuf { - let hash = super::sha::hash(cypher); - keypair.sign(&hash).to_bytes() - } - - pub fn verify( - cypher_hash: &HashBuf, - pub_key: &HashBuf, - signature: &SigBuf, - ) -> Result { - let pub_key = PublicKey::from_bytes(pub_key) - .map_err(|_| "Failed to make pub key")?; - - let signature = Signature::from_bytes(signature) - .map_err(|_| "Failed to make signature")?; - - Ok(pub_key.verify(cypher_hash, &signature).is_ok()) - } -} diff --git a/src/bin/client/display.rs b/src/bin/client/display.rs deleted file mode 100644 index 36bde64..0000000 --- a/src/bin/client/display.rs +++ /dev/null @@ -1,419 +0,0 @@ -use std::io::{self, Write}; -use std::sync::{Arc, Mutex, mpsc}; -use std::mem; - -use crossterm::QueueableCommand; -use crossterm::cursor; -use crossterm::style::{ - style, - SetAttribute, - Stylize, - Attribute, - PrintStyledContent, - Color, -}; -use crossterm::terminal::{ - self, - ClearType, -}; -use crossterm::event; - -use crate::common::*; - -const BG_COLOUR: Color = Color::Rgb{r: 0xd0, g: 0xd0, b: 0xd0}; -const FG_COLOUR: Color = Color::Rgb{r: 0x66, g: 0x00, b: 0x33}; - -const HEADER_HEIGHT: u16 = 2; -const FOOTER_HEIGHT: u16 = 2; -const MIN_HEIGHT: u16 = HEADER_HEIGHT + 1 + FOOTER_HEIGHT; -const MIN_WIDTH: u16 = 25; - -enum ViewPos { - Last, // "most recent message on bottom" - Index{msg_id: u16, chr_id: u8}, // id of TOP message, index of its first char -} - -pub struct Display<'a> { - state: Arc>, - msg_tx: mpsc::Sender, - stdout: std::io::Stdout, - size: (u16, u16), // size of terminal (w, h) - user_msg: String, // stuff the user is typing - view: ViewPos, - chat_name: &'a str, - user_name: &'a str, - known_count: usize, - hidden: bool, -} - -// WARNING: this file is very OO; proceed with your own risk! -impl<'a> Display<'a> { - pub fn start( - state: Arc>, - msg_tx: mpsc::Sender, - chat_name: &str, - user_name: &str, - ) -> crossterm::Result<()> { - // setup - let mut stdout = std::io::stdout(); - terminal::enable_raw_mode()?; - stdout.queue(event::EnableMouseCapture)?; - stdout.queue(terminal::EnterAlternateScreen)?; - stdout.queue(terminal::Clear(ClearType::All))?; - stdout.queue(cursor::DisableBlinking)?; - stdout.queue(cursor::Hide)?; - stdout.queue(terminal::SetTitle("PubliChat"))?; - stdout.flush()?; - - // set up struct - let mut disp = Display { - state, - msg_tx, - stdout: std::io::stdout(), - size: terminal::size()?, - user_msg: String::with_capacity(50), - view: ViewPos::Index { msg_id: 0, chr_id: 0 }, - chat_name, - user_name, - known_count: 0, - hidden: true, - }; - - // draw first frame then start mainloop - // preserve errors for returning, but don't return yet - // (cleanup will still be needed) - let res = disp.refresh().and(disp.mainloop()); - - // clean up - stdout.queue(cursor::Show)?; - stdout.queue(cursor::EnableBlinking)?; - stdout.queue(terminal::LeaveAlternateScreen)?; - stdout.queue(SetAttribute(Attribute::Reset))?; - stdout.queue(event::DisableMouseCapture)?; - stdout.flush()?; - terminal::disable_raw_mode()?; - - res - } - - fn mainloop(&mut self) -> crossterm::Result<()> { - use crossterm::event::Event::*; - use crossterm::event::KeyEvent as KE; - use crossterm::event::KeyCode::{Char, Esc}; - use crossterm::event::KeyModifiers as Mod; - loop { - // wait for events to come in. Update instantly if they do, otherwise - // update at defined FPS when a new message comes in. - match event::poll(_DISP_DELAY) { - Ok(true) => match event::read()? { - Key(KE{code: Char('c'), modifiers: Mod::CONTROL, .. }) => break Ok(()), - Key(KE{code: Char('d'), modifiers: Mod::CONTROL, .. }) => break Ok(()), - Key(KE{code: Char('z'), modifiers: Mod::CONTROL, .. }) => break Ok(()), - Key(KE{code: Esc, modifiers: Mod::NONE, .. }) => break Ok(()), - Key(e) => self.handle_keyboard_event(e)?, - Mouse(e) => self.handle_mouse_event(e)?, - Resize(x, y) => self.handle_resize(x, y)?, - Paste(s) => self.handle_paste(s)?, - FocusGained => {}, // TODO - FocusLost => {}, // TODO - }, - Ok(false) => {}, // No events to be processed - Err(e) => break Err(e), // Failed to read, clean up and exit - } - - let queue_len = self.state.lock().map_err(|_| { - use std::io::{Error, ErrorKind::Other}; - Error::new(Other, "Failed to lock state") - })?.queue.len(); - - // re-draw if there are new messages - if self.known_count != queue_len { - self.known_count = queue_len; - self.draw_messages()?; - self.draw_footer()?; - } - } - } - - fn draw_header(&mut self) -> crossterm::Result<()> { - let w = self.size.0 as usize; - - let mut stdout = std::io::stdout(); - stdout.queue(cursor::MoveTo(0, 0))?; - - // TODO: cache with each size change? - let header_text = format!( - "{:^w$}", - format!( - "chat: {}, user: {}", - if self.hidden {"******"} else {self.chat_name}, - if self.hidden {"******"} else {self.user_name}, - ) - ); - - let header = style(header_text) - .with(FG_COLOUR) - .on(BG_COLOUR) - .attribute(Attribute::Bold); - - stdout.queue(PrintStyledContent(header))?; - - let coloured_line = style(" ".repeat(w)).on(FG_COLOUR); - - stdout.queue(cursor::MoveToNextLine(1))?; - stdout.queue(PrintStyledContent(coloured_line))?; - - stdout.flush() - } - - fn draw_footer(&mut self) -> crossterm::Result<()> { - // TODO: notify when message too long - let mut stdout = std::io::stdout(); - let (w, h) = terminal::size()?; - let max_text_len = w as usize - 2; - - // draw purple separator - stdout.queue(cursor::MoveTo(0, h - 2))?; - stdout.queue(PrintStyledContent( - style(" ".repeat(w as usize)).on(FG_COLOUR) - ))?; - - // draw current input text - stdout.queue(cursor::MoveToNextLine(1))?; - stdout.queue(terminal::Clear(ClearType::CurrentLine))?; // del line only - - // print blinker - stdout.queue(PrintStyledContent( - style("> ").bold().rapid_blink().with(FG_COLOUR).on(BG_COLOUR) - ))?; - - // print user's typed message - let vis_text = match self.user_msg.char_indices().rev().nth(max_text_len - 1) { - // slice of last max_text_len charachters of typed message - None => self.user_msg.as_str(), // shorter than max => show whole - Some((i, _)) => &self.user_msg[i..], - }; - stdout.queue(PrintStyledContent( - style(vis_text).with(FG_COLOUR).on(BG_COLOUR) - ))?; - if vis_text.len() == self.user_msg.len() { - // vis and text have same length => text wasn't shortened - let spaces_len = max_text_len - self.user_msg.chars().count(); - stdout.queue(PrintStyledContent( - style(" ".repeat(spaces_len)).on(BG_COLOUR) - ))?; - } - stdout.flush() - } - - fn draw_messages(&mut self) -> crossterm::Result<()> { - // SIDE EFFECT: DELETES FOOTER!!! - let state = self.state.lock().map_err(|_| { - use std::io::{Error, ErrorKind::Other}; - Error::new(Other, "Failed to lock state") - })?; - if state.queue.is_empty() {return Ok(())} - - let mut stdout = std::io::stdout(); - - let (w, h) = self.size; - let mut remaining_lines = h - (HEADER_HEIGHT + FOOTER_HEIGHT); - - // TODO: find a way of changing backgroud nicely - // stdout.queue(SetForegroundColor(Color::Black))?; - // stdout.queue(SetBackgroundColor(Color::Grey))?; - - // TODO: this can just be u16::div_ceil once that stabilises. rust#88581 - let req_lines = |len| if len % w > 0 { len / w + 1 } else { len / w }; - - // clear current screen (THIS DELETES FOOTER!) - stdout.queue(cursor::MoveTo(0, HEADER_HEIGHT))?; // TODO: terminal too small? - stdout.queue(terminal::Clear(ClearType::FromCursorDown))?; - - if state.queue.len() <= remaining_lines as usize && - // it's possible all messages fit on the screen - // do more expensive check to see if it's true - state.queue.iter().map(|m| req_lines(m.len)).sum::() <= remaining_lines - { - // if it is true, just print with no checks - for msg in state.queue.iter() { - write!(stdout, "{msg}\r\n")?; - } - return stdout.flush(); - } - - // Not all messages fit on screen - match self.view { - ViewPos::Index { msg_id, .. } => { // TODO: use chr_id - // cursor already at right position, draw one msg at a time - // TODO: print start.msg partial - if msg_id >= state.queue.len() as u16 { return Ok(()) } // too far down - for msg in state.queue.range(1 + usize::from(msg_id)..) { - let msg_height = req_lines(msg.len); - if msg_height <= remaining_lines { - // normal situation, whole message fits on screen - write!(stdout, "{msg}\r\n")?; - remaining_lines -= msg_height; // TODO: check cursor pos? - } else { - // TODO: think about dealing with unicode graphemes - // let printable_chars = remaining_lines * w; - // write!(stdout, "{}", &msg.to_string()[..usize::from(printable_chars)])?; - break; // finished drawing - } - } - } - ViewPos::Last => { - // draw from bottom up - // last message shown in full, but top message might be cut off - stdout.queue(cursor::MoveTo(0, h - FOOTER_HEIGHT))?; - for msg in state.queue.iter().rev() { - let msg_height = req_lines(msg.len); - if msg_height <= remaining_lines { - // message fits no problemo. Move up to fit it: - stdout.queue(cursor::MoveToPreviousLine(msg_height))?; - - // print, then return to starting point: - stdout.queue(cursor::SavePosition)?; - write!(stdout, "{msg}")?; - stdout.queue(cursor::RestorePosition)?; - - remaining_lines -= msg_height; - if remaining_lines == 0 { break } - } else { - // only bottom half of top msg fits - // note: the message prefix will NOT be visible - // stdout.queue(cursor::MoveTo(0, HEADER_HEIGHT))?; - // let skipped_lines = msg_height - remaining_lines; - - // TODO: think about dealing with unicode graphemes - // write!(stdout, "{}", &msg.repr[(w*skipped_lines) as usize..])?; - break; // finished drawing - } - } - } - } - stdout.flush() - } - - fn refresh(&mut self) -> crossterm::Result<()> { - // Draw the full frame - self.draw_header()?; - self.draw_messages()?; - self.draw_footer()?; - self.stdout.flush() - } - - fn move_pos(&mut self, up: bool) { - // positive is scolling up - self.view = match self.view { - ViewPos::Last => ViewPos::Last, - ViewPos::Index{mut msg_id, chr_id} => if up { - msg_id = msg_id.checked_sub(1).unwrap_or(0); - ViewPos::Index{ msg_id, chr_id } - } else { - // TODO: possible ViewPos::Last - ViewPos::Index{ msg_id: msg_id+1, chr_id } - }, - }; - } - - fn handle_keyboard_event(&mut self, event: event::KeyEvent) -> crossterm::Result<()> { - use crossterm::event::KeyCode::*; - use crossterm::event::KeyModifiers as Mod; - - match (event.modifiers, event.code) { - (Mod::NONE, Char(c)) | (Mod::SHIFT, Char(c)) => { // add char - self.user_msg.push(c); - self.draw_footer() - }, - (Mod::NONE, Backspace) => { // remove char - self.user_msg.pop(); - self.draw_footer() - }, - (Mod::CONTROL, Backspace) | (Mod::CONTROL, Char('h')) => { - // remove word - let pos = self.user_msg - .trim_end() // remove trailing spaces - .trim_end_matches(|c: char| - !c.is_whitespace() - && !c.is_ascii_punctuation() - ) // remove last word - .trim_end() // remove more spaces (probably just one) - .len(); - - self.user_msg.truncate(pos); - self.draw_footer() - } - (Mod::NONE, Enter) => { // send message - use std::io::{Error, ErrorKind::Other}; - self.msg_tx.send(mem::take(&mut self.user_msg)) - .map_err(|_| Error::new(Other, "msg_rx closed"))?; - self.draw_footer() - }, - // (Mod::NONE, Delete) => Ok(()), // remove char - // (Mod::CONTROL, Delete) => Ok(()), // remove word - (Mod::CONTROL, Char('s')) => { // toggle hide secret - self.hidden ^= true; - self.draw_header() - } - (Mod::NONE, Up) => { // scroll up - self.move_pos(true); - self.draw_messages()?; - self.draw_footer() - }, - (Mod::NONE, Down) => { // scroll down - self.move_pos(false); - self.draw_messages()?; - self.draw_footer() - }, - (Mod::NONE, PageUp) => Ok(()), // scroll way up - (Mod::NONE, PageDown) => Ok(()), // scroll way down - (Mod::NONE, Home) => { // scroll way way up - self.view = ViewPos::Index{msg_id: 0, chr_id: 0}; - self.draw_messages()?; - self.draw_footer() - }, - (Mod::NONE, End) => { // scroll way way down - self.view = ViewPos::Last; - self.draw_messages()?; - self.draw_footer() - }, - (Mod::CONTROL, Char('r')) => self.refresh(), // redraw everything - (Mod::CONTROL, Char('c')) | (Mod::NONE, Esc) => unreachable!(), - _ => Ok(()), - } - } - - fn handle_mouse_event(&mut self, event: event::MouseEvent) -> crossterm::Result<()> { - use crossterm::event::MouseEventKind::*; - match event.kind { - ScrollUp => { - self.move_pos(true); - self.draw_messages()?; - self.draw_footer() - }, - ScrollDown => { - self.move_pos(false); - self.draw_messages()?; - self.draw_footer() - }, - _ => Ok(()), - } - } - - fn handle_resize(&mut self, x: u16, y: u16) -> crossterm::Result<()> { - if y < MIN_HEIGHT || x < MIN_WIDTH { - return Err(io::Error::new( - io::ErrorKind::Other, - "Terminal size not supported! Too small :(", - )) - } - self.size = (x, y); - self.refresh() - } - - fn handle_paste(&mut self, s: String) -> crossterm::Result<()> { - self.user_msg.push_str(&s); - self.draw_footer() - } -} diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs deleted file mode 100644 index eba0227..0000000 --- a/src/bin/client/main.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::collections::VecDeque; -use std::error::Error; -use std::io::Write; -use std::net::TcpStream; -use std::net::ToSocketAddrs; -use std::sync::{Arc, Mutex, mpsc}; -use std::thread; -use std::mem; - -use publichat::helpers::*; -use publichat::buffers::{ - msg_head, - msg_out_c as msg_out, - cypher::Buf as CypherBuf, -}; - -mod msg; -use msg::Message; - -mod common; -use common::*; - -mod display; -use display::Display; - -mod crypt; -use crypt::{sha, ed25519}; - -mod comm; - -// mutex lock shortuct -macro_rules! lock { ($s:tt) => { $s.lock().map_err(|_| "Failed to lock state") } } - -fn parse_header(header: &msg_head::Buf) -> Result<(u8, u32, u8, bool), &'static str> { - // returns (chat id byte, message id, message count, forward) - let (pad_buf, cid_buf, mid_buf, count_buf) = msg_head::split(header); - let mut msg_id = [0; 4]; // TODO: this is ugly. Consider combining cid and mid - msg_id[1..].copy_from_slice(mid_buf); // can't fail - if pad_buf == msg_head::PAD { - Ok(( - cid_buf[0], // can't fail - u32::from_be_bytes(msg_id), - count_buf[0] & 0b0111_1111, // can't fail - count_buf[0] & 0b1000_0000 > 0, - )) - } else { - println!("{header:?}"); - Err("Received invalid header padding") - } -} - - -// Listener thread handles parsing data received from server -// - Receive message packets; parse; break up into messages -// - Insert into queue in correct place -fn listener(mut stream: TcpStream, state: Arc>) -> Res { - let mut hed_buf = msg_head::DEFAULT; - loop { - read_exact(&mut stream, &mut hed_buf, "Failed to read head buffer")?; - // TODO: what should happen when this fails? - // I guess thread closes and require reconnect - - let (chat, first_id, count, forward) = parse_header(&hed_buf)?; - if count == 0 { continue } // skip no messages - - // read messages expected from header - let mut buf = vec![0; count as usize * msg_out::SIZE]; // TODO: consider array - read_exact(&mut stream, &mut buf, "Failed to bulk read fetch")?; - - let mut s = lock!(state)?; - if chat != s.chat_id[0] { continue } // skip wrong chat - - let last_id = first_id + count as u32 - 1; // inclusive. Can't undeflow - - if s.min_id > s.max_id { // initial fetch - // handle initial fetch separately; skip all checks - for msg in buf.chunks_exact(msg_out::SIZE) { - let msg = Message::new(msg.try_into().unwrap(), &s.chat_key)?; - s.queue.push_back(msg); - } - s.min_id = first_id; - s.max_id = last_id; - continue; // initial fetch finished, move to next packet - } - - if s.max_id + 1 < first_id // disconnected ahead - || s.min_id > last_id + 1 // disconnected behind - || (s.min_id <= first_id && last_id <= s.max_id) // already have this - || (first_id < s.min_id && s.max_id < last_id) // overflow on both sides - { continue } // skip all these - - if forward { - if last_id > s.max_id { // good proper data here - let i = if first_id <= s.max_id {s.max_id-first_id+1} else {0}; - assert_eq!(s.max_id + 1, first_id + i); - for msg in buf.chunks_exact(msg_out::SIZE).skip(i as usize) { - let msg = Message::new(msg.try_into().unwrap(), &s.chat_key)?; - s.queue.push_back(msg); - } - // buf.chunks_exact(MSG_OUT_SIZE) - // .skip(i as usize) - // .map(|msg| Message::new(msg.try_into().unwrap(), &s.chat_key)?) - // .for_each(|msg| s.queue.push_back(msg)); - s.max_id = last_id; - } else { // points forwards but behind our data - continue; - } - } else { // not forwards (for scrolling up) - todo!() - } - } -} - - -// Requester thread handles sending requests (fetch & query) to server -fn requester(mut stream: TcpStream, state: Arc>) -> Res { - let chat_id = lock!(state)?.chat_id; - - // Fetch until we get first message packet - while lock!(state)?.queue.is_empty() { - comm::send_fetch(&mut stream, &chat_id)?; - thread::sleep(FQ_DELAY); - } - - // Query for scroll or fetch for more - loop { - comm::send_query( - &mut stream, - &chat_id, - true, - 50, - state.lock().unwrap().max_id, // TODO: edit ID - )?; - thread::sleep(FQ_DELAY); - } -} - - -// Sender threads sends messages to server as they come in from snd_rx -fn sender( - mut stream: TcpStream, - state: Arc>, - snd_rx: mpsc::Receiver, - keypair: ed25519::Keypair, -) -> Res { - let chat_id = lock!(state)?.chat_id; - let chat_key = lock!(state)?.chat_key; - - let mut cypher_buf: CypherBuf; - let mut signature_buf: ed25519::SigBuf; - - loop { - let msg = snd_rx.recv().map_err(|_| "Message sender hung up")?; // blocks - if msg.split_whitespace().next().is_none() { continue; } // empty msg - - cypher_buf = Message::make_cypher(&msg, &chat_key, keypair.public.as_bytes())?; - signature_buf = ed25519::sign(&cypher_buf, &keypair); - comm::send_msg(&mut stream, &chat_id, &cypher_buf, &signature_buf)?; - } -} - - -fn main() -> Result<(), Box> { // TODO: return Res instead? - eprintln!("Starting client..."); - // arguments: addr:port title user - - let mut args = std::env::args().skip(1).collect::>(); - - let server_addr = args.get(0).ok_or("No addr given")? - .to_socket_addrs()? - .next().ok_or("Zero addrs received?")?; - - let chat = mem::take(args.get_mut(1).ok_or("No title given")?); - let chat_key = sha::hash(chat.as_bytes()); - let chat_id = sha::hash(&chat_key); - - let user = mem::take(args.get_mut(2).ok_or("No username given")?); - let keypair = ed25519::make_keypair(user.as_bytes())?; - - eprintln!("Connecting to server {:?}...", server_addr); - let mut stream = TcpStream::connect(server_addr)?; - eprintln!("Connected!"); - - stream.write_all(b"SMRT")?; - - let queue = VecDeque::with_capacity(500); - let state = GlobalState { - queue, - chat_key, // TODO: this doesn't change; store somewhere else? - chat_id, - min_id: 1, - max_id: 0, - }; - let state = Arc::new(Mutex::new(state)); - - // mpsc for sending messages - let (msg_tx, msg_rx) = mpsc::channel::(); - - // start listener thread - let stream_c = stream.try_clone()?; - let state_c = state.clone(); - eprintln!("Starting listener thread..."); - thread::spawn(|| { - match listener(stream_c, state_c) { - Ok(_) => eprintln!("Listener thread finished"), - Err(e) => eprintln!("Listener thread crashed: {e}"), - } - }); - - // start requester thread - let stream_c = stream.try_clone()?; - let state_c = state.clone(); - eprintln!("Starting requester thread..."); - thread::spawn(|| { - match requester(stream_c, state_c) { - Ok(_) => eprintln!("Requester loop finished"), - Err(e) => eprintln!("Requester loop crashed: {e}"), - }; - }); - - // start sender thread - let stream_c = stream.try_clone()?; - let state_c = state.clone(); - eprintln!("Starting requester thread..."); - thread::spawn(|| { - match sender(stream_c, state_c, msg_rx, keypair) { - Ok(_) => eprintln!("Sender loop finished"), - Err(e) => eprintln!("Sender loop crashed: {e}"), - }; - }); - - // start drawer thread - eprintln!("Starting drawer..."); - match Display::start(state, msg_tx, chat.as_str(), user.as_str()) { - Ok(_) => eprintln!("Drawer finished"), - Err(e) => eprintln!("Drawer crashed: {e}"), - } - - Ok(()) -} diff --git a/src/bin/client/msg.rs b/src/bin/client/msg.rs deleted file mode 100644 index bfbc723..0000000 --- a/src/bin/client/msg.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::{str, fmt, time::{Duration, SystemTime, UNIX_EPOCH}}; - -use crossterm::style::{Stylize, Color}; -use rand::Rng; - -use publichat::buffers::{hash::Buf as HashBuf, cypher, msg_out_c as msg_out}; -use crate::crypt::*; -use crate::common::{ - VERIFY_TOLERANCE_MS, - USER_ID_CHAR_COUNT, -}; - -#[derive(Debug)] -pub struct Message { - // time: Duration, - // user: Hash, - // text: Contents, - // verified: bool, - pub len: u16, - pub repr: String, // TODO: duplicate storage? - // TODO: consider just having strings instead of this struct - // TODO: what do I do when the time needs to be displayed differently? -} - -impl Message { - pub fn new( // parse server's bytes into message text - mut bytes: msg_out::Buf, - chat_key: &HashBuf, - ) -> Result { - // deconstruct bytes - let (st_buf, c_buf, s_buf) = msg_out::split_mut(&mut bytes); - - // shadow to change types (unwraps CANNOT fail here; len check skipped!) - let server_time = u64::from_be_bytes(st_buf.try_into().unwrap()); - let cypher: &mut cypher::Buf = c_buf.try_into().unwrap(); - let signature: &ed25519::SigBuf = (&*s_buf).try_into().unwrap(); - // TODO: the &* casts the `&mut [u8]` into a `&[u8]`. Ugly! - - // prepare for signature check before cypher gets decrypted - let hashed_cypher = sha::hash(cypher.as_slice()); - - // decrypt chat in-place - aes::apply(chat_key, cypher); - let cypher_data = cypher; // rename variable for clarity - - // deconstruct msg_data - let (ck_buf, ct_buf, pk_buf, msg_buf) = cypher::split(cypher_data); - - // shadow to change types (unwraps CANNOT fail here; len check skipped!) - let client_time = u64::from_be_bytes(ct_buf.try_into().unwrap()); - let pub_key: &HashBuf = pk_buf.try_into().unwrap(); - - // find padding - let pad_start = msg_buf.iter() - .rposition(|&b| b == chat_key[0]) - .ok_or("Invalid pad: indicator not found")?; - let message = &msg_buf[..pad_start]; - - // verify message, prep verification mark - let verified = - chat_key.starts_with(ck_buf) - && server_time.abs_diff(client_time) < VERIFY_TOLERANCE_MS - && ed25519::verify(&hashed_cypher, pub_key, signature)?; - let v_mark = if verified { '✔'.green() } else { '✗'.red().rapid_blink() }; - - // prep username string - let user = &base64::encode(pub_key)[..USER_ID_CHAR_COUNT]; - let colour = Color::from({ - // user colour taken from last three bytes of public key - // 3 is an unavoidable magic number of colours in RGB, - // lets hope humans don't evolve more cone cell types - let c = &pub_key[pub_key.len()-3..]; - (c[0], c[1], c[2]) - }); - let user_c = user.on(colour).with(w_or_b(&colour)); - - // prep time string - let time = Duration::from_millis(server_time); - let (hour, min, sec) = { // TODO: use date/time-related crate (?) - let time_sec = time.as_secs(); - ( - (time_sec / 3600) % 24, - (time_sec / 60) % 60, - time_sec % 60, - ) - }; - - // prep message string: check utf8 and sanitise for ansi - let msg = str::from_utf8(message).map_err(|_| "Non-utf8 message!")?; - let msg = msg.chars() - .map(|c| if c.is_ascii_control() {'�'} else {c}) - .collect::(); - - // build string - let cached_str_repr = format!( - "{v_mark} {user_c} {hour:0>2}:{min:0>2}:{sec:0>2} {msg}" - ); - - const PREFIX_LEN: u16 = 1 + 1 + USER_ID_CHAR_COUNT as u16 + 1 + 8 + 1; - - Ok(Self { - // time, - // user, - // text: cypher, - // verified, - // length, - len: PREFIX_LEN + msg.chars().count() as u16, - repr: cached_str_repr, - }) - } - - pub fn make_cypher( - text: &str, - chat_key: &HashBuf, - pub_key: &HashBuf, - ) -> Result { - let time: u64 = SystemTime::now() - .duration_since(UNIX_EPOCH).expect("Woah, get with the times!") - .as_millis().try_into().expect("Alright, futureboy"); - - let mut res = cypher::DEFAULT; - let (ck_buf, t_buf, pk_buf, msg_buf) = cypher::split_mut(&mut res); - - if text.len() > msg_buf.len() - 1 { return Err("Can't make cypher; msg too long") } - - // copy in basic data - ck_buf.copy_from_slice(&chat_key[..ck_buf.len()]); - t_buf.copy_from_slice(&time.to_be_bytes()); - pk_buf.copy_from_slice(pub_key); - msg_buf[..text.len()].copy_from_slice(text.as_bytes()); - - // padding - let mut rng = rand::thread_rng(); - msg_buf[text.len()] = chat_key[0]; // pad indicator - msg_buf[text.len()+1..].fill_with(|| - rng.gen_range(1u8..=0xff).wrapping_add(chat_key[0]) - ); - - // AES - aes::apply(chat_key, &mut res); - - Ok(res) - } -} - -impl fmt::Display for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.repr) - } -} - -fn w_or_b(colour: &Color) -> Color { // TODO: where should this function be? - // Return white for dark colours, black for light colours - return if let Color::Rgb{r, g, b} = colour { - let is_dark = ( - 0.299 * f32::from(*r) - + 0.587 * f32::from(*g) - + 0.114 * f32::from(*b) - ) < 150.0; - if is_dark { Color::White } else { Color::Black } - } else { unreachable!("w_or_b called on non-rgb colour") } -} diff --git a/src/bin/server/http.rs b/src/bin/server/http.rs index a4bc06a..675cf31 100644 --- a/src/bin/server/http.rs +++ b/src/bin/server/http.rs @@ -13,36 +13,6 @@ fn send_code(code: u16, stream: &mut TcpStream) -> Res { ) } -fn send_data(code: u16, data: &[u8], stream: &mut TcpStream) -> Res { - let header_string = format!( - "HTTP/1.1 {}\r\nContent-Length: {}\r\n\r\n", - code, - data.len(), - ); - - full_write( - stream, - &[header_string.as_bytes(), data].concat(), - "Failed to send file", - ) -} - -fn handle_robots(stream: &mut TcpStream) -> Res { - const RESP_ROBOTS: &[u8] = b"\ - HTTP/1.1 200\r\n\ - Content-Length: 25\r\n\r\n\ - User-agent: *\nDisallow: / - "; - full_write(stream, RESP_ROBOTS, "Failed to send robots") -} - -fn handle_version(stream: &mut TcpStream, globals: &Arc) -> Res { - // TODO: avoid allocations to pre-building packet? - // TODO: should functions like this map_err to show where the issue is? - send_data(200, &globals.git_hash, stream) - .map_err(|_| "Failed to send version") -} - fn handle_ws(req: &str, mut stream: TcpStream, globals: &Arc) -> Res { // handshake let key_in = match req.split("Sec-WebSocket-Key: ").nth(1) { @@ -77,18 +47,7 @@ pub fn handle(mut stream: TcpStream, globals: &Arc) -> Res { }; match path { - "/" | "" => send_data(200, FILE_INDEX_HTML, &mut stream) - .map_err(|_| "Failed to send index.html"), - "/favicon.ico" => send_data(200, FILE_FAVICON_ICO, &mut stream) - .map_err(|_| "Failed to send favicon"), - "/mobile" | "/m" => send_data(200, FILE_MOBILE_HTML, &mut stream) - .map_err(|_| "Failed to send mobile.html"), - "/ws" => handle_ws(req, stream, globals), // start WS - "/robots.txt" => handle_robots(&mut stream), - "/tools" => send_data(200, FILE_TOOLS_HTML, &mut stream) - .map_err(|_| "Failed to send 404"), - "/version" => handle_version(&mut stream, globals), - _ => send_data(404, FILE_404_HTML, &mut stream) - .map_err(|_| "Failed to send 404"), + "/ws" => handle_ws(req, stream, globals), // start WS + _ => Err("Invalid HTTP path (path!=/ws)"), } } diff --git a/src/helpers.rs b/src/helpers.rs index da912ec..9c0d21d 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -20,18 +20,6 @@ pub struct Globals { // owns all its data! pub git_hash: [u8; 40], } -pub const FILE_FAVICON_ICO: &[u8] = include_bytes!("../page/favicon.ico"); -pub const FILE_TOOLS_HTML: &[u8] = include_bytes!("../page/tools.html"); // TODO: minify? - -// I believe there is no nice way of doing this -// no minify, no tls -#[cfg(all(not(feature = "minify"), not(feature = "tls")))] -pub const FILE_INDEX_HTML: &[u8] = include_bytes!("../target/index.html"); -#[cfg(all(not(feature = "minify"), not(feature = "tls")))] -pub const FILE_MOBILE_HTML: &[u8] = include_bytes!("../target/mobile.html"); -#[cfg(all(not(feature = "minify"), not(feature = "tls")))] -pub const FILE_404_HTML: &[u8] = include_bytes!("../target/404.html"); - // only tls #[cfg(all(not(feature = "minify"), feature = "tls"))] pub const FILE_INDEX_HTML: &[u8] = include_bytes!("../target/index-tls.html"); @@ -39,19 +27,3 @@ pub const FILE_INDEX_HTML: &[u8] = include_bytes!("../target/index-tls.html"); pub const FILE_MOBILE_HTML: &[u8] = include_bytes!("../target/mobile-tls.html"); #[cfg(all(not(feature = "minify"), feature = "tls"))] pub const FILE_404_HTML: &[u8] = include_bytes!("../target/404.html"); - -// only minify -#[cfg(all(feature = "minify", not(feature = "tls")))] -pub const FILE_INDEX_HTML: &[u8] = include_bytes!("../target/index.min.html"); -#[cfg(all(feature = "minify", not(feature = "tls")))] -pub const FILE_MOBILE_HTML: &[u8] = include_bytes!("../target/mobile.min.html"); -#[cfg(all(feature = "minify", not(feature = "tls")))] -pub const FILE_404_HTML: &[u8] = include_bytes!("../target/404.min.html"); - -// minify and tls -#[cfg(all(feature = "minify", feature = "tls"))] -pub const FILE_INDEX_HTML: &[u8] = include_bytes!("../target/index-tls.min.html"); -#[cfg(all(feature = "minify", feature = "tls"))] -pub const FILE_MOBILE_HTML: &[u8] = include_bytes!("../target/mobile-tls.min.html"); -#[cfg(all(feature = "minify", feature = "tls"))] -pub const FILE_404_HTML: &[u8] = include_bytes!("../target/404.min.html");