Compare commits

...

31 Commits

Author SHA1 Message Date
Matéo Duparc 0bd49c092d
Update dependencies 2022-05-04 13:31:02 +02:00
Matéo Duparc ede41e5715
Add AIRA logo 2021-09-02 19:45:32 +02:00
Matéo Duparc 6c5bbc3f64
Better concurrency management 2021-08-29 21:19:59 +02:00
Matéo Duparc 5ae61222f4
Make cargo clippy happy 2021-08-18 16:09:52 +02:00
Matéo Duparc dce526922e
Log websocket messages in debug build 2021-08-02 21:22:56 +02:00
Matéo Duparc 8c8ede431c
Bug fix: set not seen before notifying UI 2021-08-02 21:19:50 +02:00
Matéo Duparc c4417cf802
Bug fixes: load old messages on scroll & show right avatar in pending messages 2021-08-02 21:18:06 +02:00
Matéo Duparc 0e1c17973b
Update to PSEC v0.4 2021-07-25 16:02:17 +02:00
Matéo Duparc f2cfb6bda0
Refactoring ui_interface.rs 2021-07-23 12:36:37 +02:00
Matéo Duparc dda30bf5af
Pending messages 2021-07-23 12:04:29 +02:00
Matéo Duparc 5081f9dbf4
Update screenshot 2021-06-20 15:32:40 +02:00
Matéo Duparc 6b81cec83a
Decrease max message size to 16MB 2021-06-18 20:02:48 +02:00
Matéo Duparc 40cab75f96
Add avatar as notifications icon 2021-06-16 16:19:40 +02:00
Matéo Duparc 03a9df81ec
Message timestamps 2021-06-16 16:17:08 +02:00
Matéo Duparc 3543ef2824
Handle websocket disconnection on UI 2021-06-04 13:14:29 +02:00
Matéo Duparc 496c115fa9
Hide chat scrollbar when not needed & Always optimize scrypt crate 2021-06-04 12:40:31 +02:00
Matéo Duparc dc9fde39be
Adjust avatar sizes & Fix horizontal overflow in chat history 2021-05-31 15:45:24 +02:00
Matéo Duparc 2673fb2341
Update async-psec to v0.3 2021-05-30 18:43:47 +02:00
Matéo Duparc e953b7b335
Display file transfer index 2021-05-30 17:32:37 +02:00
Matéo Duparc bd795ca995
Responsive pop-up 2021-05-28 19:15:34 +02:00
Matéo Duparc 0eee053b5e
Secure avatar generation 2021-05-28 19:02:24 +02:00
Matéo Duparc 10e496e36f
Improve cache cleaning when contacts table doesn't exist 2021-05-27 21:07:12 +02:00
Matéo Duparc 541fe10f1a
Allow setting avatar when creating identity 2021-05-26 15:01:48 +02:00
Matéo Duparc b3ae7ba703
Remove avatar button 2021-05-26 12:35:12 +02:00
Matéo Duparc 8cc6f6b50f
Avatars 2021-05-25 21:59:16 +02:00
Matéo Duparc da4cc4995f
Add contact info on session info pop-up 2021-05-21 19:19:15 +02:00
Matéo Duparc 1b175b81e9
Add config.yml & Set AIRA_VERSION in build.rs 2021-05-18 19:23:26 +02:00
Matéo Duparc 275c2972df
Switching to async-psec 2021-05-17 14:37:04 +02:00
Matéo Duparc 8adcef8852
Better messages loading 2021-05-14 15:52:54 +02:00
Matéo Duparc 1c584112b3
Optional PSEC padding 2021-05-14 11:39:44 +02:00
Matéo Duparc 861e072537
Refresh name button 2021-05-11 16:11:24 +02:00
27 changed files with 5051 additions and 1675 deletions

5
.gitignore vendored
View File

@ -1,3 +1,2 @@
Cargo.lock
target
local
/target
local

2866
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,49 @@
[package]
name = "aira"
version = "0.0.2"
version = "0.1.1"
authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"]
edition = "2018"
exclude = ["src/frontend"]
[dependencies]
rand-8 = {package = "rand", version = "0.8.3"}
rand-7 = {package = "rand", version = "0.7.3"}
tokio = {version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-util"]}
rand = "0.8"
rand-7 = { package = "rand", version = "0.7.3" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-util"] }
async-psec = { version = "0.4", features = ["split"] }
lazy_static = "1.4"
socket2 = "0.4.0"
rusqlite = {version = "0.25.1", features = ["bundled"]}
ed25519-dalek = "1" #for singing
x25519-dalek = "1.1" #for shared secret
sha2 = "0.9.3"
hkdf = "0.11.0"
aes-gcm = "0.9.0" #PSEC
aes-gcm-siv = "0.10.0" #Database
hmac = "0.11.0"
hex = "0.4.3"
strum_macros = "0.20.1" #display enums
actix-web = "3"
actix-multipart = "0.3"
time = "0.2.25" #needed for actix cookies
socket2 = "0.4"
rusqlite = { version = "0.27", features = ["bundled"] }
ed25519-dalek = "1" #for singatures
sha2 = "0.10"
aes-gcm = "0.9"
aes-gcm-siv = "0.10" #database encryption
hkdf = "0.12"
hex = "0.4"
actix-web = "4"
env_logger = "0.9"
actix-multipart = "0.4"
time = "0.3" #needed for actix cookies
futures = "0.3"
tungstenite = "0.13.0" #websocket
serde = "1.0.124" #serialization
html-escape = "0.2.7"
tungstenite = "0.17" #websocket
serde = { version = "1.0", features = ["derive"] } #serialization
html-escape = "0.2"
sanitize-filename = "0.3"
platform-dirs = "0.3.0"
uuid = {version = "0.8", features = ["v4"]}
webbrowser = "0.5.5"
platform-dirs = "0.3"
uuid = { version = "1.0", features = ["v4"] }
webbrowser = "0.7"
libmdns = "0.6" #mDNS advertiser
multicast_dns = "0.5" #mDNS browser
if-addrs = "0.6"
base64 = "0.13.0"
scrypt = "0.7.0"
zeroize = "1.2.0"
if-addrs = "0.7"
base64 = "0.13"
scrypt = "0.10"
zeroize = "1.5"
image = "0.24"
yaml-rust = "0.4" #only in debug mode
[build-dependencies]
html-minifier = "3.0.11"
html-minifier = "3.0"
yaml-rust = "0.4"
linked-hash-map = "0.5"
[profile.dev.package.scrypt]
opt-level = 3

View File

@ -1,7 +1,7 @@
# AIRA
AIRA is peer-to-peer encrypted communication tool for local networks built on the [PSEC protocol](https://forge.chapril.org/hardcoresushi/PSEC). It allows to securely send text messages and files without any server or Internet access. AIRA automatically discovers and connects to other peers on your network, so you don't need any prior configuration to start communicating.
![Screenshot of a conversation between Alice and Bob on AIRA](/screenshot.png)
![Screenshot of a conversation between Alice and Bob on AIRA](https://forge.chapril.org/hardcoresushi/AIRA/raw/branch/master/screenshot.png)
# Rationale
When people want to send a file from one computer to another located only meters apart, they usually send it via mail. This mail usually goes through many servers around the world before reaching its final destination.
@ -62,4 +62,4 @@ cargo build --release
```
## What does AIRA stand for ?
AIRA Is a Recursive Acronym.
AIRA Is a Recursive Acronym.

View File

@ -1,6 +1,11 @@
use std::{env, fs::{File, read_to_string, create_dir}, path::Path, io::{Write, ErrorKind}};
#[cfg(not(debug_assertions))]
use {
std::{env, fs::{File, read_to_string, create_dir}, path::Path, io::{Write, ErrorKind}},
yaml_rust::{YamlLoader, Yaml},
linked_hash_map::LinkedHashMap,
};
#[allow(dead_code)]
#[cfg(not(debug_assertions))]
fn minify_content(content: &str, language: &str) -> Option<String> {
match language {
"html" => Some(html_minifier::minify(content).unwrap()),
@ -10,8 +15,15 @@ fn minify_content(content: &str, language: &str) -> Option<String> {
}
}
#[allow(dead_code)]
fn minify_web_files() {
#[cfg(not(debug_assertions))]
fn replace_fields(content: &mut String, fields: &LinkedHashMap<Yaml, Yaml>) {
fields.into_iter().for_each(|field| {
*content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap());
});
}
#[cfg(not(debug_assertions))]
fn generate_web_files() {
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir);
let src_dir = Path::new("src/frontend");
@ -22,23 +34,41 @@ fn minify_web_files() {
}
}
let config = &YamlLoader::load_from_str(&read_to_string("config.yml").unwrap()).unwrap()[0];
let fields = config.as_hash().unwrap();
[
"login.html",
"index.html",
"index.css",
"index.js",
"commons/style.css",
"commons/script.js"
"commons/script.js",
].iter().for_each(|file_name| {
let file_name = Path::new(file_name);
let content = read_to_string(src_dir.join(file_name)).unwrap();
let minified_content = minify_content(&content, file_name.extension().unwrap().to_str().unwrap()).unwrap();
let mut dst = File::create(out_dir.join(file_name)).unwrap();
let path = Path::new(file_name);
let src_path = src_dir.join(path);
println!("cargo:rerun-if-changed={}", src_path.to_str().unwrap());
let extension = path.extension().unwrap().to_str().unwrap();
let mut content = read_to_string(src_path).unwrap();
if extension == "css" || file_name == &"login.html" {
replace_fields(&mut content, fields);
}
if file_name == &"index.html" {
content = content.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION"));
}
let minified_content = minify_content(&content, extension).unwrap();
let mut dst = File::create(out_dir.join(path)).unwrap();
dst.write(minified_content.as_bytes()).unwrap();
});
const TEXT_AVATAR_PATH: &str = "src/frontend/imgs/text_avatar.svg";
let mut text_avatar = read_to_string(TEXT_AVATAR_PATH).unwrap();
println!("cargo:rerun-if-changed={}", TEXT_AVATAR_PATH);
replace_fields(&mut text_avatar, fields);
File::create(out_dir.join("text_avatar.svg")).unwrap().write(text_avatar.as_bytes()).unwrap();
}
fn main() {
#[cfg(not(debug_assertions))]
minify_web_files();
generate_web_files();
}

1
config.yml Normal file
View File

@ -0,0 +1 @@
ACCENT_COLOR: "19a52c"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -4,4 +4,5 @@ pub const APPLICATION_FOLDER: &str = "AIRA";
pub const DB_NAME: &str = "AIRA.db";
pub const HTTP_COOKIE_NAME: &str = "aira_auth";
pub const MSG_LOADING_COUNT: usize = 20;
pub const FILE_CHUNK_SIZE: usize = 1023996;
pub const FILE_CHUNK_SIZE: usize = 1_023_996;
pub const MAX_RECV_SIZE: usize = 16_383_996;

View File

@ -1,148 +1,18 @@
use std::convert::TryInto;
use std::{convert::TryInto, fmt::Display};
use hkdf::Hkdf;
use sha2::Sha384;
use hmac::{Hmac, Mac, NewMac};
use scrypt::{scrypt, Params};
use rand_8::{RngCore, rngs::OsRng};
use rand::{RngCore, rngs::OsRng};
use aes_gcm::{aead::Aead, NewAead, Nonce};
use aes_gcm_siv::Aes256GcmSiv;
use zeroize::Zeroize;
use strum_macros::Display;
use crate::utils::*;
pub const HASH_OUTPUT_LEN: usize = 48; //SHA384
const KEY_LEN: usize = 16;
pub const IV_LEN: usize = 12;
pub const AES_TAG_LEN: usize = 16;
pub const SALT_LEN: usize = 32;
const PASSWORD_HASH_LEN: usize = 32;
pub const MASTER_KEY_LEN: usize = 32;
pub fn iv_to_nonce(iv: &[u8], counter: &mut usize) -> Vec<u8> {
let mut counter_bytes = vec![0; 4];
counter_bytes.extend_from_slice(&counter.to_be_bytes());
let r: Vec<u8> = iv.iter().zip(counter_bytes.iter()).map(|(a, b)| a^b).collect();
*counter += 1;
r
}
fn hkdf_expand_label(key: &[u8], label: &str, context: Option<&[u8]>, okm: &mut [u8]) {
let hkdf = Hkdf::<Sha384>::from_prk(key).unwrap();
//can't set info conditionnally because of different array size
match context {
Some(context) => {
let info = [&(label.len() as u32).to_be_bytes(), label.as_bytes(), &(context.len() as u32).to_be_bytes(), context];
hkdf.expand_multi_info(&info, okm).unwrap();
}
None => {
let info = [&(label.len() as u32).to_be_bytes(), label.as_bytes()];
hkdf.expand_multi_info(&info, okm).unwrap();
}
};
}
pub struct HandshakeKeys {
pub local_key: [u8; KEY_LEN],
pub local_iv: [u8; IV_LEN],
pub local_handshake_traffic_secret: [u8; HASH_OUTPUT_LEN],
pub peer_key: [u8; KEY_LEN],
pub peer_iv: [u8; IV_LEN],
pub peer_handshake_traffic_secret: [u8; HASH_OUTPUT_LEN],
pub handshake_secret: [u8; HASH_OUTPUT_LEN],
}
impl HandshakeKeys {
pub fn derive_keys(shared_secret: [u8; 32], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> HandshakeKeys {
let (handshake_secret, _) = Hkdf::<Sha384>::extract(None, &shared_secret);
let local_label = "handshake".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"};
let mut local_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(handshake_secret.as_slice(), &local_label, Some(&handshake_hash), &mut local_handshake_traffic_secret);
let peer_label = "handshake".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
let mut peer_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(handshake_secret.as_slice(), &peer_label, Some(&handshake_hash), &mut peer_handshake_traffic_secret);
let mut local_handshake_key = [0; KEY_LEN];
hkdf_expand_label(&local_handshake_traffic_secret, "key", None, &mut local_handshake_key);
let mut local_handshake_iv = [0; IV_LEN];
hkdf_expand_label(&local_handshake_traffic_secret, "iv", None, &mut local_handshake_iv);
let mut peer_handshake_key = [0; KEY_LEN];
hkdf_expand_label(&peer_handshake_traffic_secret, "key", None, &mut peer_handshake_key);
let mut peer_handshake_iv = [0; IV_LEN];
hkdf_expand_label(&peer_handshake_traffic_secret,"iv", None, &mut peer_handshake_iv);
HandshakeKeys {
local_key: local_handshake_key,
local_iv: local_handshake_iv,
local_handshake_traffic_secret: local_handshake_traffic_secret,
peer_key: peer_handshake_key,
peer_iv: peer_handshake_iv,
peer_handshake_traffic_secret: peer_handshake_traffic_secret,
handshake_secret: to_array_48(handshake_secret.as_slice())
}
}
}
pub struct ApplicationKeys {
pub local_key: [u8; KEY_LEN],
pub local_iv: [u8; IV_LEN],
pub peer_key: [u8; KEY_LEN],
pub peer_iv: [u8; IV_LEN],
}
impl ApplicationKeys {
pub fn derive_keys(handshake_secret: [u8; HASH_OUTPUT_LEN], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> ApplicationKeys {
let mut derived_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&handshake_secret, "derived", None, &mut derived_secret);
let (master_secret, _) = Hkdf::<Sha384>::extract(Some(&derived_secret), b"");
let local_label = "application".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"};
let mut local_application_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&master_secret, &local_label, Some(&handshake_hash), &mut local_application_traffic_secret);
let peer_label = "application".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
let mut peer_application_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&master_secret, &peer_label, Some(&handshake_hash), &mut peer_application_traffic_secret);
let mut local_application_key = [0; KEY_LEN];
hkdf_expand_label(&local_application_traffic_secret, "key", None, &mut local_application_key);
let mut local_application_iv = [0; IV_LEN];
hkdf_expand_label(&local_application_traffic_secret, "iv", None, &mut local_application_iv);
let mut peer_application_key = [0; KEY_LEN];
hkdf_expand_label(&peer_application_traffic_secret, "key", None, &mut peer_application_key);
let mut peer_application_iv = [0; IV_LEN];
hkdf_expand_label(&peer_application_traffic_secret,"iv", None, &mut peer_application_iv);
ApplicationKeys {
local_key: local_application_key,
local_iv: local_application_iv,
peer_key: peer_application_key,
peer_iv: peer_application_iv,
}
}
}
pub fn compute_handshake_finished(local_handshake_traffic_secret: [u8; HASH_OUTPUT_LEN], handshake_hash: [u8; HASH_OUTPUT_LEN]) -> [u8; HASH_OUTPUT_LEN] {
let mut finished_key = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&local_handshake_traffic_secret, "finished", None, &mut finished_key);
let mut hmac = Hmac::<Sha384>::new_from_slice(&finished_key).unwrap();
hmac.update(&handshake_hash);
hmac.finalize().into_bytes().as_slice().try_into().unwrap()
}
pub fn verify_handshake_finished(peer_handshake_finished: [u8; HASH_OUTPUT_LEN], peer_handshake_traffic_secret: [u8; HASH_OUTPUT_LEN], handshake_hash: [u8; HASH_OUTPUT_LEN]) -> bool {
let mut peer_finished_key = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&peer_handshake_traffic_secret, "finished", None, &mut peer_finished_key);
let mut hmac = Hmac::<Sha384>::new_from_slice(&peer_finished_key).unwrap();
hmac.update(&handshake_hash);
hmac.verify(&peer_handshake_finished).is_ok()
}
pub fn generate_fingerprint(public_key: &[u8]) -> String {
let mut raw_fingerprint = [0; 16];
Hkdf::<Sha384>::new(None, public_key).expand(&[], &mut raw_fingerprint).unwrap();
@ -150,7 +20,6 @@ pub fn generate_fingerprint(public_key: &[u8]) -> String {
}
pub fn generate_master_key() -> [u8; MASTER_KEY_LEN] {
let mut master_key = [0; MASTER_KEY_LEN];
OsRng.fill_bytes(&mut master_key);
@ -169,12 +38,21 @@ pub fn encrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoErr
Ok(cipher_text)
}
#[derive(Display, Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub enum CryptoError {
DecryptionFailed,
InvalidLength
}
impl Display for CryptoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
CryptoError::DecryptionFailed => "Decryption failed",
CryptoError::InvalidLength => "Invalid length",
})
}
}
pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
if data.len() <= IV_LEN || master_key.len() != MASTER_KEY_LEN {
return Err(CryptoError::InvalidLength);

View File

@ -1,13 +1,67 @@
function generateAvatar(name) {
let span = document.createElement("span");
if (typeof name == "undefined") {
span.appendChild(document.createTextNode("?"));
} else if (name.length > 0) {
span.appendChild(document.createTextNode(name[0].toUpperCase()));
function generateImgAvatar() {
let img = document.createElement("img");
img.classList.add("avatar");
return img;
}
function generateSelfAvatar(timestamp) {
let img = generateImgAvatar();
img.src = "/avatar/self?"+timestamp;
return img;
}
function generateAvatar(sessionId, name, timestamp) {
let img = generateImgAvatar();
img.src = "/avatar/"+sessionId+"/"+name+"?"+timestamp;
return img;
}
function removePopup() {
let popups = document.querySelectorAll(".popup_background");
if (popups.length > 0) {
popups[popups.length-1].remove();
}
}
function showPopup(content, cancelable = true) {
let popup_background = document.createElement("div");
popup_background.classList.add("popup_background");
let popup = document.createElement("div");
popup.classList.add("popup");
if (cancelable) {
popup_background.onclick = function(e) {
if (e.target == popup_background) {
removePopup();
}
};
let close = document.createElement("button");
close.classList.add("close");
close.onclick = removePopup;
popup.appendChild(close);
}
popup.appendChild(content);
popup_background.appendChild(popup);
let main = document.querySelector("main");
main.appendChild(popup_background);
}
function uploadAvatar(event, onUploaded) {
let file = event.target.files[0];
if (file.size < 10000000) {
let formData = new FormData();
formData.append("avatar", file);
fetch("/set_avatar", {method: "POST", body: formData}).then(response => {
if (response.ok) {
onUploaded();
} else {
console.log(response);
}
});
} else {
let mainDiv = document.createElement("div");
mainDiv.appendChild(generatePopupWarningTitle());
let p = document.createElement("p");
p.textContent = "Avatar cannot be larger than 10MB.";
mainDiv.appendChild(p);
showPopup(mainDiv);
}
let div = document.createElement("div");
div.classList.add("avatar");
div.appendChild(span);
div.appendChild(document.createElement("div")); //used for background
return div;
}

View File

@ -4,7 +4,7 @@
}
:root {
--accent: #19a52c;
--accent: #ACCENT_COLOR;
--transparent: #00000000;
}
@ -24,26 +24,112 @@ input[type="text"], input[type="password"] {
width: 100%;
margin: 0;
}
input[type="file"] {
display: none;
}
label {
cursor: pointer;
}
.avatar {
position: relative;
margin-right: .5em;
width: 1.5em;
height: 1.5em;
width: 2.5em;
height: 2.5em;
border-radius: 50%;
}
.avatar div {
width: 100%;
height: 100%;
background-color: var(--accent);
border-radius: 100%;
}
.avatar span {
main.card {
max-width: 500px;
background-color: #2B2F31;
border-radius: 10px;
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
}
.popup {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
width: 40vw;
max-height: 90vh;
overflow: auto;
box-sizing: border-box;
padding: 20px 70px 10px;
background-color: #2B2F31;
border-radius: 10px;
font-size: 1.2em;
}
@media (max-width: 1700px) {
.popup {
width: 50vw;
}
}
@media (max-width: 1400px) {
.popup {
width: 60vw;
}
}
@media (max-width: 1100px) {
.popup {
width: 70vw;
}
}
.popup:last-child::after {
content: "";
display: block;
height: 20px;
width: 100%;
}
.popup_background {
height: 100%;
width: 100%;
position: absolute;
background-color: rgba(0, 0, 0, .5);
z-index: 2;
}
.popup .close {
background-color: unset;
position: absolute;
right: 0;
top: 6px;
}
.popup .close::after {
content: url("/static/imgs/icons/cancel");
background-color: unset;
}
#avatarContainer {
display: flex;
justify-content: center;
padding-bottom: 1.5em;
}
#avatarContainer .avatar {
margin-right: unset;
}
#avatarContainer label:hover .avatar {
opacity: .4;
}
#avatarContainer label {
position: relative;
}
#avatarContainer .avatar + p {
display: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: 0;
text-align: center;
}
#avatarContainer label:hover p {
display: block;
}

View File

@ -0,0 +1,79 @@
<svg width="191.7mm" height="168.39mm" version="1.1" viewBox="0 0 191.7 168.39" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-9.1475 -20.806)">
<path d="m81.908 189.19c14.507-12.545 13.641-67.907 13.641-67.907l18.56 2e-5s-0.86524 55.362 13.641 67.907z" fill="#803300"/>
<g fill="#19f52c">
<g fill-rule="evenodd">
<circle cx="19.053" cy="67.31" r="8.6808"/>
<circle cx="40.006" cy="88.061" r="8.6808"/>
<circle cx="79.997" cy="99.739" r="8.6808"/>
<circle cx="68.203" cy="133.44" r="8.6808"/>
<circle cx="105.86" cy="74.321" r="8.6808"/>
<circle cx="38.754" cy="131.9" r="8.6808"/>
<circle cx="127.11" cy="124.1" r="8.6808"/>
<circle cx="97.819" cy="119.49" r="8.6808"/>
<circle cx="19.67" cy="109.42" r="8.6808"/>
<circle cx="48.138" cy="59.715" r="8.6808"/>
<circle cx="95.703" cy="44.581" r="8.6808"/>
<circle cx="77.134" cy="67.671" r="8.6808"/>
<circle cx="121.04" cy="29.486" r="8.6808"/>
<circle cx="119.73" cy="98.423" r="8.6808"/>
<circle cx="51.945" cy="108.83" r="8.6808"/>
</g>
<g stroke="#19f52c" stroke-width="1.8939">
<path d="m38.754 131.9-19.084-22.481"/>
<path d="m19.67 109.42 20.336-21.356"/>
<path d="m19.053 67.31 20.953 20.75"/>
<path d="m51.945 108.83 16.258 24.602"/>
<path d="m51.945 108.83 28.052-9.0944"/>
<path d="m48.138 59.715-8.132 28.346"/>
<path d="m48.138 59.715 28.996 7.956"/>
<path d="m77.134 67.671 28.73 6.6502"/>
<path d="m77.134 67.671 18.569-23.089"/>
<path d="m95.703 44.581 25.333-15.095"/>
<path d="m105.86 74.321 13.863 24.103"/>
<path d="m73.582 133.41 24.237-13.916"/>
<path d="m97.819 119.49 21.907-21.071"/>
<path d="m97.819 119.49 29.289 4.6056"/>
<path d="m17.828 153.33 20.926-21.434"/>
</g>
<circle cx="17.828" cy="153.33" r="8.6808" fill-rule="evenodd"/>
<circle cx="53.865" cy="159.2" r="8.6808" fill-rule="evenodd"/>
<path d="m38.754 131.9 15.111 27.303" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m127.11 124.1 13.863 24.103" stroke="#19f52c" stroke-width="1.8939"/>
<circle cx="140.97" cy="148.2" r="8.6808" fill-rule="evenodd"/>
<path d="m140.97 148.2 20.559-17.279" stroke="#19f52c" stroke-width="1.8939"/>
<g fill-rule="evenodd">
<circle cx="161.53" cy="130.92" r="8.6808"/>
<circle cx="148.77" cy="105.42" r="8.6808"/>
<circle cx="161.93" cy="80.961" r="8.6808"/>
<circle cx="134.44" cy="73.351" r="8.6808"/>
<circle cx="172.46" cy="44.584" r="8.6808"/>
<circle cx="192.17" cy="132.75" r="8.6808"/>
<circle cx="175.39" cy="155.03" r="8.6808"/>
<circle cx="68.229" cy="33.867" r="8.6808"/>
<circle cx="143.95" cy="45.376" r="8.6808"/>
</g>
<g stroke="#19f52c" stroke-width="1.8939">
<path d="m161.53 130.92 13.863 24.103"/>
<path d="m175.39 155.03 16.778-22.271"/>
<path d="m119.73 98.423 29.041 6.9934"/>
<path d="m148.77 105.42 12.763 25.506"/>
<path d="m105.86 74.321 28.58-0.96983"/>
<path d="m148.77 105.42 29.455-1.4191"/>
<path d="m143.95 45.376 28.511-0.79158"/>
<path d="m121.04 29.486 22.914 15.89"/>
<path d="m38.754 131.9 29.449 1.5377"/>
<path d="m77.134 67.671 2.8632 32.068"/>
<path d="m68.229 33.867 27.474 10.715"/>
<path d="m48.138 59.715 20.091-25.848"/>
<path d="m143.95 45.376-9.5064 27.975"/>
</g>
<circle cx="178.22" cy="104" r="8.6808" fill-rule="evenodd"/>
<path d="m172.46 44.584 16.663 26.606" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m134.44 73.351 27.488 7.6103" stroke="#19f52c" stroke-width="1.8939"/>
<circle cx="189.12" cy="71.19" r="8.6808" fill-rule="evenodd"/>
<path d="m189.12 71.19-27.193 9.7712" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m161.93 80.961 16.292 23.036" stroke="#19f52c" stroke-width="1.8939"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FILL_COLOR"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@ -0,0 +1,4 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill="#ACCENT_COLOR"/>
<text x="50" y="50" text-anchor="middle" dominant-baseline="middle" fill="white" font-weight="bold" font-size="60" dy=".1em" font-family="Arial,Helvetica,Sans-Serif">LETTER</text>
</svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@ -37,74 +37,7 @@ button::after {
button:hover::after {
background-color: var(--accent);
}
input[type="file"] {
display: none;
}
.file_picker {
display: flex;
align-items: center;
cursor: pointer;
}
.file_picker::after {
content: url("/static/imgs/icons/attach/19a52c");
width: 2em;
}
.popup {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
width: 40%;
max-height: 90vh;
overflow: auto;
box-sizing: border-box;
padding: 20px 70px 10px;
background-color: #2B2F31;
border-radius: 10px;
font-size: 1.2em;
}
.popup:last-child::after {
content: "";
display: block;
height: 20px;
width: 100%;
}
.popup_background {
height: 100%;
width: 100%;
position: absolute;
background-color: rgba(0, 0, 0, .5);
z-index: 2;
}
.popup .close {
background-color: unset;
position: absolute;
right: 0;
top: 6px;
}
.popup .close:hover {
background-color: unset;
}
.popup .close::after {
content: url("/static/imgs/icons/cancel");
background-color: unset;
}
.popup .avatar {
font-size: 4em;
margin: auto;
}
.popup section {
font-weight: bold;
display: block;
margin-bottom: 20px;
}
.popup input {
display: block;
margin: 10px;
}
.popup button {
.classic_button {
background-color: var(--button-background);
color: white;
cursor: pointer;
@ -112,31 +45,134 @@ input[type="file"] {
border-radius: 8px;
font-weight: bold;
}
.popup button:hover {
.classic_button:hover {
background-color: var(--accent);
}
.popup span {
font-weight: bold;
.file_picker {
display: flex;
align-items: center;
cursor: pointer;
}
.popup>div>div p {
font-weight: normal;
font-size: 0.9em;
.file_picker::after {
content: url("/static/imgs/icons/attach/ACCENT_COLOR");
width: 2em;
}
.popup h2.warning::before {
content: url("/static/imgs/icons/warning/19a52c");
width: 9%;
content: url("/static/imgs/icons/warning/ACCENT_COLOR");
width: 2em;
display: inline-block;
vertical-align: middle;
}
.popup .session_info h2 {
text-align: center;
.switch_preference {
display: flex;
align-items: center;
}
.popup .session_info p:first-of-type, .popup .session_info pre {
.preference_description {
flex-grow: 1;
width: 0; /*fix unknown display bug of .switch*/
margin-right: 20px;
}
.preference_description p:last-of-type {
font-size: .8em;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.popup .session_info p:nth-of-type(2) {
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch span {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
border-radius: 34px;
transition: .3s;
}
.switch span::before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
border-radius: 50%;
transition: .3s;
}
.switch input:checked + span {
background-color: var(--accent);
}
.switch input:focus + span {
box-shadow: 0 0 1px var(--accent);
}
.switch input:checked + span:before {
transform: translateX(26px);
}
#avatarContainer {
position: relative;
}
#avatarContainer .avatar {
font-size: 2.5em;
}
#removeAvatar {
position: absolute;
bottom: 0;
cursor: pointer;
}
#removeAvatar:hover {
color: var(--accent);
}
#profile_info section {
display: block;
margin-bottom: 20px;
border-top: 1px solid black;
}
#profile_info section:first-of-type {
border-top: unset;
}
#profile_info section:first-of-type h3 {
margin: 0;
}
#profile_info input {
margin: 10px;
}
#profile_info>div>div p {
font-weight: normal;
font-size: 0.9em;
}
#session_info .avatar {
font-size: 2.5em;
display: block;
margin: auto;
}
#session_info .name {
display: flex;
justify-content: center;
}
#session_info .name button::after {
content: url("/static/imgs/icons/refresh");
}
#session_info .session_field {
display: flex;
gap: .3em;
}
#session_info .session_field p {
margin-top: 0;
}
#session_info .session_field p:first-child {
color: #34db4a;
}
#session_info .session_field p:last-child {
font-weight: bold;
}
.button_row {
display: flex;
gap: 15px;
@ -154,8 +190,6 @@ input[type="file"] {
}
#right_panel {
background-color: #15191E;
display: flex;
flex-direction: column;
overflow: hidden;
}
#me {
@ -163,7 +197,6 @@ input[type="file"] {
padding: 10px;
display: flex;
align-items: center;
font-size: 1.7em;
cursor: pointer;
}
#me>div {
@ -172,6 +205,7 @@ input[type="file"] {
flex-grow: 1;
}
#me p {
font-size: 1.7em;
margin: 0;
font-weight: bold;
display: inline;
@ -179,9 +213,8 @@ input[type="file"] {
#me>div:hover p {
color: var(--accent);
}
#identity_fingerprint {
text-align: center;
margin-bottom: 0;
#me .avatar {
font-size: 1.2em;
}
#left_panel ul:last-of-type, #msg_log {
flex-grow: 1;
@ -204,14 +237,13 @@ input[type="file"] {
flex-grow: 1;
}
#left_panel ul li .avatar {
width: 2em;
height: 2em;
font-size: .9em;
}
#left_panel ul li:hover, #left_panel ul li.current {
background-color: #333940;
}
#left_panel ul li p::after {
content: url("/static/imgs/icons/warning/19a52c");
content: url("/static/imgs/icons/warning/ACCENT_COLOR");
display: inline-block;
width: 1em;
margin-left: 5px;
@ -221,9 +253,10 @@ input[type="file"] {
content: unset;
}
#left_panel ul li.is_verified p::after {
content: url("/static/imgs/icons/verified/19a52c");
content: url("/static/imgs/icons/verified/ACCENT_COLOR");
}
#left_panel ul li .not_seen_marker {
#left_panel ul li.not_seen::after {
content: "";
width: 12px;
height: 12px;
background-color: var(--accent);
@ -255,7 +288,7 @@ input[type="file"] {
}
#show_local_ips:hover::after {
background-color: unset;
content: url("/static/imgs/icons/info/19a52c");
content: url("/static/imgs/icons/info/ACCENT_COLOR");
}
.popup ul {
list-style-type: unset;
@ -264,7 +297,6 @@ input[type="file"] {
flex-direction: row;
align-items: center;
padding: 20px 20px;
font-size: 1.5em;
}
#chat_header>div {
display: flex;
@ -276,11 +308,12 @@ input[type="file"] {
color: var(--accent);
}
#chat_header>div>p { /*name*/
font-size: 1.5em;
font-weight: bold;
margin: 0;
}
#chat_header p::after {
content: url("/static/imgs/icons/warning/19a52c");
content: url("/static/imgs/icons/warning/ACCENT_COLOR");
display: inline-block;
width: 1.2em;
vertical-align: middle;
@ -290,7 +323,7 @@ input[type="file"] {
content: unset;
}
#chat_header.is_verified p::after {
content: url("/static/imgs/icons/verified/19a52c");
content: url("/static/imgs/icons/verified/ACCENT_COLOR");
}
#chat_header.is_contact #delete_conversation::after {
content: url("/static/imgs/icons/delete_conversation");
@ -323,7 +356,6 @@ input[type="file"] {
}
#file_transfer {
border-top: 2px solid var(--accent);
display: none;
position: relative;
}
#file_transfer.active {
@ -342,7 +374,7 @@ input[type="file"] {
#file_cancel::after {
background-color: unset;
width: 20px;
content: url("/static/imgs/icons/cancel/19a52c");
content: url("/static/imgs/icons/cancel/ACCENT_COLOR");
}
#file_progress {
display: none;
@ -370,31 +402,46 @@ input[type="file"] {
height: 100%;
background-color: var(--accent);
}
#message_box {
border-top: 2px solid var(--accent);
margin-bottom: 0;
}
#msg_log {
font-size: 1.1em;
overflow-y: scroll;
white-space: pre;
overflow-y: auto;
white-space: pre-wrap;
}
#msg_log li {
display: flex;
align-items: end;
gap: 10px;
margin-bottom: 10px;
padding-right: 10px;
}
#msg_log li>div {
flex-grow: 1;
}
#msg_log li .timestamp {
opacity: .5;
font-family: "Liberation Sans", Arial, sans-serif;
font-size: .8em;
}
#msg_log p {
font-size: 1.1em;
margin: 0;
}
#msg_log .avatar {
font-size: .8em;
}
#msg_log li .header {
display: flex;
align-items: center;
margin-top: 15px;
}
#msg_log li .header p {
color: var(--accent);
font-weight: bold;
margin-left: .5em;
}
#msg_log li .content {
margin-left: 2em;
margin-top: 5px;
margin-bottom: 10px;
margin-left: 3em;
}
#msg_log li .content p {
word-break: break-word;
}
#msg_log a {
color: #238cf5;
@ -416,11 +463,154 @@ input[type="file"] {
color: var(--accent);
}
#msg_log .file a::after {
content: url("/static/imgs/icons/download/19a52c");
content: url("/static/imgs/icons/download/ACCENT_COLOR");
display: block;
width: 2em;
margin-left: 15px;
}
#message_box, #chat_header, #msg_log {
#message_box, #message_box.online #offline_warning, #chat_header, #msg_log, #file_transfer {
display: none;
}
#message_box.active {
display: block;
}
#message_box {
border-top: 2px solid red;
margin-bottom: 0;
}
#message_box>div:nth-child(2) {
display: flex;
}
#message_box.online {
border-top-color: var(--accent);
}
#offline_warning {
margin-left: 20px;
display: flex;
align-items: center;
gap: 25px;
}
#offline_warning::before {
content: url("/static/imgs/icons/warning/ff0000");
display: block;
width: 2em;
}
#offline_warning h3 {
color: red;
display: inline-block;
margin-bottom: .3em;
}
#offline_warning p {
margin-top: 0;
}
#msg_log li.pending_msgs_divider {
border-top: 1px solid grey;
padding-top: 10px;
margin-top: 30px;
margin-left: 100px;
margin-right: 100px;
}
#msg_log li.pending_msgs_divider h4 {
margin: auto;
opacity: .5;
}
.lds-spinner {
color: official;
position: relative;
width: 82px;
height: 82px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #fff;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#pending_msgs_indicator {
display: none;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
#pending_msgs_indicator.sending {
display: flex;
}
#disconnected {
display: none;
height: 100%;
align-items: center;
justify-content: center;
}
#disconnected.disconnected {
display: flex;
}
#disconnected img {
width: 70px;
height: 70px;
}

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<link rel="stylesheet" href="/static/index.css">
</head>
@ -41,6 +42,10 @@
</div>
<ul id="msg_log">
</ul>
<div id="pending_msgs_indicator">
<div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
<h3>Sending pending messages...</h3>
</div>
<div id="file_transfer">
<div id="file_control">
<button id="file_cancel" title="Cancel"></button>
@ -56,10 +61,22 @@
</div>
</div>
<div id="message_box">
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file" multiple>
</label>
<div id="offline_warning">
<div>
<h3>Your contact seems to be offline.</h3>
<p>Sent messages will be stored until a connection is established.</p>
</div>
</div>
<div>
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file" multiple>
</label>
</div>
</div>
<div id="disconnected">
<img src="/static/imgs/icons/warning/ff0000">
<h1>Websocket connection closed</h1>
</div>
</div>
</main>
@ -68,6 +85,7 @@
let identityFingerprint = "IDENTITY_FINGERPRINT";
let isIdentityProtected = IS_IDENTITY_PROTECTED;
let websocketPort = WEBSOCKET_PORT;
let usePadding = PSEC_PADDING;
</script>
<script src="/static/libs/linkify.min.js"></script>
<script src="/static/libs/linkify-element.min.js"></script>
@ -75,4 +93,3 @@
<script src="/static/index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA - Login</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<style>
body {
@ -14,14 +15,6 @@
background-color: black;
}
main {
max-width: 500px;
background-color: #2B2F31;
border-radius: 10px;
position: absolute;
top: 20vh;
left: 0;
right: 0;
margin: auto;
padding-bottom: 20px;
}
h1, main>h3, #error_msg {
@ -40,6 +33,9 @@
font-weight: bold;
margin-bottom: 0;
}
.action_page>h2 {
margin-top: 0;
}
input[type="text"], input[type="password"] {
margin-bottom: 20px;
}
@ -63,15 +59,12 @@
padding: 10px 50px;
}
.avatar {
width: 2em;
height: 2em;
font-size: 3em;
margin: auto;
}
#identity h2 {
text-align: center;
}
p {
#error_msg {
color: red;
}
label.checkbox {
@ -110,24 +103,41 @@
label.checkbox input:checked ~ .checkmark:after {
display: block;
}
#avatarContainer .avatar.unset {
border: 2px solid var(--accent);
}
#identity .avatar {
display: block;
margin: auto;
}
</style>
</head>
<body>
<main>
<main class="card">
<h1>AIRA</h1>
<h3>Local network secure P2P communications</h3>
<p id="error_msg">ERROR_MSG</p>
<div id="login_page" class="action_page">
<h2>Login:</h2>
<form id="login_form" method="POST" action="/login">
<div id="identity"></div>
<div id="identity">
<img class="avatar" src="/avatar/self"/>
<h2 id="identityName"></h2>
</div>
<input name="password" type="password" placeholder="Password">
<button type="submit">Login</button>
</form>
</div>
<div id="create_page" class="action_page">
<h2>Create New Identity:</h2>
<h2>Create a new identity:</h2>
<form method="POST">
<div id="avatarContainer">
<label>
<input type="file" accept="image/*">
<img class="avatar unset" src="/static/imgs/icons/profile/ACCENT_COLOR"/>
<p>Upload</p>
</label>
</div>
<input type="text" name="name" placeholder="Name" required>
<label class="checkbox">
<input id="enable_password" type="checkbox">
@ -142,12 +152,20 @@
</main>
<script src="/static/commons/script.js"></script>
<script>
let identity_name = IDENTITY_NAME;
if (identity_name == null) {
let identityName = IDENTITY_NAME;
if (identityName == null) {
document.getElementById("login_page").style.display = "none";
document.querySelector("#avatarContainer input").onchange = function(event) {
uploadAvatar(event, function() {
let img = document.querySelector("#avatarContainer .avatar");
img.src = "/avatar/self?"+Date.now();
img.classList.remove("unset");
});
};
let passwordInputs = document.querySelectorAll("#create_page input[type=\"password\"]");
let enable_password = document.getElementById("enable_password");
enable_password.onchange = function() {
document.querySelectorAll("#create_page input[type=\"password\"]").forEach(function(i) {
passwordInputs.forEach(function(i) {
if (enable_password.checked) {
i.style.display = "block";
} else {
@ -156,11 +174,8 @@
});
}
} else {
let identity = document.getElementById("identity");
identity.appendChild(generateAvatar(identity_name));
let h2 = document.createElement("h2");
h2.textContent = identity_name;
identity.appendChild(h2);
let h2Name = document.getElementById("identityName");
h2Name.textContent = identityName;
document.getElementById("create_page").style.display = "none";
}
</script>

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA - Logged out</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<style>
body {
@ -14,15 +15,6 @@
background-color: black;
}
main {
max-width: 500px;
background-color: #2B2F31;
border-radius: 10px;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
padding: 50px;
}
img {
@ -37,7 +29,7 @@
</style>
</head>
<body>
<main>
<main class="card">
<img src="/static/imgs/frog">
<p>You've been successfully logged out.</p>
</main>

View File

@ -1,6 +1,6 @@
use std::convert::TryInto;
use crypto::CryptoError;
use ed25519_dalek::{Keypair, Signer, SIGNATURE_LENGTH, PUBLIC_KEY_LENGTH};
use ed25519_dalek::{Keypair, PUBLIC_KEY_LENGTH};
use rusqlite::{Connection, params};
use platform_dirs::AppDirs;
use utils::to_uuid_bytes;
@ -11,6 +11,7 @@ use crate::{constants, crypto, key_value_table::KeyValueTable, print_error, util
const MAIN_TABLE: &str = "main";
const CONTACTS_TABLE: &str = "contacts";
const FILES_TABLE: &str = "files";
const AVATARS_TABLE: &str = "avatars";
const DATABASE_CORRUPED_ERROR: &str = "Database corrupted";
@ -20,6 +21,8 @@ impl<'a> DBKeys {
pub const KEYPAIR: &'a str = "keypair";
pub const SALT: &'a str = "salt";
pub const MASTER_KEY: &'a str = "master_key";
pub const USE_PADDING: &'a str = "use_padding";
pub const AVATAR: &'a str = "avatar";
}
fn bool_to_byte(b: bool) -> u8 {
@ -40,50 +43,60 @@ fn get_database_path() -> String {
AppDirs::new(Some(constants::APPLICATION_FOLDER), false).unwrap().data_dir.join(constants::DB_NAME).to_str().unwrap().to_owned()
}
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>
#[derive(Debug, Clone)]
pub struct Message {
pub outgoing: bool,
pub timestamp: u64,
pub data: Vec<u8>,
}
pub struct Contact {
pub uuid: Uuid,
pub public_key: [u8; PUBLIC_KEY_LENGTH],
pub name: String,
pub avatar: Option<Uuid>,
pub verified: bool,
pub seen: bool,
}
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>,
encrypted_use_padding: Vec<u8>,
}
pub struct Identity {
pub name: String,
keypair: Keypair,
pub keypair: Keypair,
pub master_key: [u8; crypto::MASTER_KEY_LEN],
pub use_padding: bool,
}
impl Identity {
pub fn sign(&self, input: &[u8]) -> [u8; SIGNATURE_LENGTH] {
self.keypair.sign(input).to_bytes()
}
pub fn get_public_key(&self) -> [u8; PUBLIC_KEY_LENGTH] {
self.keypair.public.to_bytes()
}
pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
pub fn add_contact(&self, name: String, avatar_uuid: Option<Uuid>, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?;
db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, avatar BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?;
let contact_uuid = Uuid::new_v4();
let encrypted_name = crypto::encrypt_data(name.as_bytes(), &self.master_key).unwrap();
let encrypted_public_key = crypto::encrypt_data(&public_key, &self.master_key).unwrap();
let encrypted_verified = crypto::encrypt_data(&[bool_to_byte(false)], &self.master_key).unwrap();
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(true)], &self.master_key).unwrap();
db.execute(&("INSERT INTO ".to_owned()+CONTACTS_TABLE+" (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)"), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?;
match avatar_uuid {
Some(avatar_uuid) => db.execute(&format!("INSERT INTO {} (uuid, name, avatar, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, &avatar_uuid.as_bytes()[..], encrypted_public_key, encrypted_verified, encrypted_seen])?,
None => db.execute(&format!("INSERT INTO {} (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?
};
Ok(Contact {
uuid: contact_uuid,
public_key: public_key,
name: name,
public_key,
name,
avatar: avatar_uuid,
verified: false,
seen: true,
})
@ -92,7 +105,7 @@ impl Identity {
pub fn remove_contact(uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
Identity::delete_conversation(uuid)?;
db.execute(&("DELETE FROM ".to_owned()+CONTACTS_TABLE+" WHERE uuid=?"), [&uuid.as_bytes()[..]])
db.execute(&format!("DELETE FROM {} WHERE uuid=?", CONTACTS_TABLE), [&uuid.as_bytes()[..]])
}
pub fn set_verified(&self, uuid: &Uuid) -> Result<usize, rusqlite::Error> {
@ -107,6 +120,17 @@ impl Identity {
db.execute(&format!("UPDATE {} SET name=?1 WHERE uuid=?2", CONTACTS_TABLE), [encrypted_name.as_slice(), uuid.as_bytes()])
}
pub fn set_contact_avatar(&self, contact_uuid: &Uuid, avatar_uuid: Option<&Uuid>) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
match avatar_uuid {
Some(avatar_uuid) => db.execute(&format!("UPDATE {} SET avatar=?1 WHERE uuid=?2", CONTACTS_TABLE), params![&avatar_uuid.as_bytes()[..], &contact_uuid.as_bytes()[..]]),
None => {
db.execute(&format!("DELETE FROM {} WHERE uuid=(SELECT avatar FROM {} WHERE uuid=?)", AVATARS_TABLE, CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..]])?;
db.execute(&format!("UPDATE {} SET avatar=NULL WHERE uuid=?", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..]])
}
}
}
pub fn set_contact_seen(&self, uuid: &Uuid, seen: bool) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(seen)], &self.master_key).unwrap();
@ -116,107 +140,99 @@ impl Identity {
pub fn load_contacts(&self) -> Option<Vec<Contact>> {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&("SELECT uuid, name, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
let mut contacts = Vec::new();
while let Some(row) = rows.next().unwrap() {
let encrypted_public_key: Vec<u8> = row.get(2).unwrap();
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
Ok(public_key) => {
if public_key.len() == PUBLIC_KEY_LENGTH {
let encrypted_name: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
Ok(name) => {
let encrypted_verified: Vec<u8> = row.get(3).unwrap();
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
Ok(verified) => {
let encrypted_seen: Vec<u8> = row.get(4).unwrap();
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
Ok(seen) => {
let uuid: Vec<u8> = row.get(0).unwrap();
match to_uuid_bytes(&uuid) {
Some(uuid_bytes) => {
contacts.push(Contact {
uuid: Uuid::from_bytes(uuid_bytes),
public_key: public_key.try_into().unwrap(),
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
verified: byte_to_bool(verified[0]).unwrap(),
seen: byte_to_bool(seen[0]).unwrap(),
})
}
None => {}
}
}
Err(e) => print_error!(e)
}
if let Ok(mut stmt) = db.prepare(&("SELECT uuid, name, avatar, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
let mut rows = stmt.query([]).unwrap();
let mut contacts = Vec::new();
while let Ok(Some(row)) = rows.next() {
let encrypted_public_key: Vec<u8> = row.get(3).unwrap();
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
Ok(public_key) => {
let encrypted_name: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
Ok(name) => {
let encrypted_verified: Vec<u8> = row.get(4).unwrap();
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
Ok(verified) => {
let encrypted_seen: Vec<u8> = row.get(5).unwrap();
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
Ok(seen) => {
let contact_uuid: Vec<u8> = row.get(0).unwrap();
let avatar_result: Result<Vec<u8>, rusqlite::Error> = row.get(2);
let avatar = match avatar_result {
Ok(avatar_uuid) => Some(Uuid::from_bytes(to_uuid_bytes(&avatar_uuid).unwrap())),
Err(_) => None
};
contacts.push(Contact {
uuid: Uuid::from_bytes(to_uuid_bytes(&contact_uuid).unwrap()),
public_key: public_key.try_into().unwrap(),
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
avatar,
verified: byte_to_bool(verified[0]).unwrap(),
seen: byte_to_bool(seen[0]).unwrap(),
})
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
} else {
print_error!("Invalid public key length: database corrupted");
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
Some(contacts)
}
Err(e) => {
print_error!(e);
None
}
}
}
Err(e) => {
print_error!(e);
None
return Some(contacts);
}
}
Err(e) => print_error!(e)
}
None
}
pub fn clear_temporary_files() -> Result<usize, rusqlite::Error> {
pub fn clear_cache() -> Result<(), rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), [])
let mut stmt = db.prepare(&format!("SELECT name FROM sqlite_master WHERE type='table' AND name='{}'", CONTACTS_TABLE))?;
let mut rows = stmt.query([])?;
let contact_table_exists = rows.next()?.is_some();
if contact_table_exists {
#[allow(unused_must_use)]
{
db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), []);
db.execute(&format!("DELETE FROM {} WHERE uuid NOT IN (SELECT avatar FROM {})", AVATARS_TABLE, CONTACTS_TABLE), []);
}
} else {
db.execute(&format!("DROP TABLE IF EXISTS {}", FILES_TABLE), [])?;
db.execute(&format!("DROP TABLE IF EXISTS {}", AVATARS_TABLE), [])?;
}
Ok(())
}
pub fn load_file(&self, uuid: Uuid) -> Option<Vec<u8>> {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
while let Some(row) = rows.next().unwrap() {
let encrypted_uuid: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
Ok(test_uuid) => {
if test_uuid == uuid.as_bytes() {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => return Some(data),
Err(e) => print_error!(e)
}
}
let mut stmt = db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)).unwrap();
let mut rows = stmt.query([]).unwrap();
while let Ok(Some(row)) = rows.next() {
let encrypted_uuid: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
Ok(test_uuid) => {
if test_uuid == uuid.as_bytes() {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => return Some(data),
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
None
}
Err(e) => {
print_error!(e);
None
Err(e) => print_error!(e)
}
}
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
None
}
pub fn store_file(&self, contact_uuid: Option<Uuid>, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
@ -233,84 +249,63 @@ impl Identity {
Ok(file_uuid)
}
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<usize, rusqlite::Error> {
pub fn store_msg(&self, contact_uuid: &Uuid, message: &Message) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(outgoing);
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, timestamp BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(message.outgoing);
let encrypted_outgoing = crypto::encrypt_data(&[outgoing_byte], &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(data, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO \"{}\" (outgoing, data) VALUES (?1, ?2)", contact_uuid), params![encrypted_outgoing, encrypted_data])
let encrypted_timestamp = crypto::encrypt_data(&message.timestamp.to_be_bytes(), &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(&message.data, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO \"{}\" (outgoing, timestamp, data) VALUES (?1, ?2, ?3)", contact_uuid), params![encrypted_outgoing, encrypted_timestamp, encrypted_data])
}
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<Message>> {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
let row = rows.next().unwrap();
if row.is_some() {
let total: usize = row.unwrap().get(0).unwrap();
if offset >= total {
print_error!("Offset larger than total numbers of rows");
None
} else {
if offset+count >= total {
count = total-offset;
}
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
let mut msgs = Vec::new();
while let Some(row) = rows.next().unwrap() {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
Ok(outgoing) => {
match byte_to_bool(outgoing[0]) {
Ok(outgoing) => {
let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => {
msgs.push(
(
outgoing,
data
)
)
},
Err(e) => print_error!(e)
}
}
Err(_) => {}
if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
let mut rows = stmt.query([]).unwrap();
if let Ok(Some(row)) = rows.next() {
let total: usize = row.get(0).unwrap();
if offset < total {
if offset+count >= total {
count = total-offset;
}
let mut stmt = db.prepare(&format!("SELECT outgoing, timestamp, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
let mut rows = stmt.query([]).unwrap();
let mut msgs = Vec::new();
while let Ok(Some(row)) = rows.next() {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
Ok(outgoing) => {
if let Ok(outgoing) = byte_to_bool(outgoing[0]) {
let encrypted_timestamp: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(&encrypted_timestamp, &self.master_key) {
Ok(timestamp) => {
let encrypted_data: Vec<u8> = row.get(2).unwrap();
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(data) => msgs.push(Message {
outgoing,
timestamp: u64::from_be_bytes(timestamp.try_into().unwrap()),
data,
}),
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
}
Some(msgs)
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
}
} else {
None
return Some(msgs);
}
}
Err(e) => {
print_error!(e);
None
}
}
}
Err(e) => {
print_error!(e);
None
}
Err(e) => print_error!(e)
}
None
}
#[allow(unused_must_use)]
@ -329,6 +324,36 @@ impl Identity {
result
}
pub fn set_use_padding(&mut self, use_padding: bool) -> Result<usize, rusqlite::Error> {
self.use_padding = use_padding;
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
let encrypted_use_padding = crypto::encrypt_data(&[bool_to_byte(use_padding)], &self.master_key).unwrap();
db.update(DBKeys::USE_PADDING, &encrypted_use_padding)
}
pub fn store_avatar(&self, avatar: &[u8]) -> Result<Uuid, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (uuid BLOB PRIMARY KEY, data BLOB)", AVATARS_TABLE), [])?;
let uuid = Uuid::new_v4();
let encrypted_avatar = crypto::encrypt_data(avatar, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO {} (uuid, data) VALUES (?1, ?2)", AVATARS_TABLE), params![&uuid.as_bytes()[..], encrypted_avatar])?;
Ok(uuid)
}
pub fn get_avatar(&self, avatar_uuid: &Uuid) -> Option<Vec<u8>> {
let db = Connection::open(get_database_path()).ok()?;
let mut stmt = db.prepare(&format!("SELECT data FROM {} WHERE uuid=?", AVATARS_TABLE)).unwrap();
let mut rows = stmt.query(params![&avatar_uuid.as_bytes()[..]]).unwrap();
let encrypted_avatar: Vec<u8> = rows.next().ok()??.get(0).unwrap();
match crypto::decrypt_data(&encrypted_avatar, &self.master_key) {
Ok(avatar) => Some(avatar),
Err(e) => {
print_error!(e);
None
}
}
}
pub fn zeroize(&mut self){
self.master_key.zeroize();
self.keypair.secret.zeroize();
@ -340,25 +365,21 @@ impl Identity {
let encrypted_keypair = db.get(DBKeys::KEYPAIR)?;
let salt = db.get(DBKeys::SALT)?;
let encrypted_master_key = db.get(DBKeys::MASTER_KEY)?;
let encrypted_use_padding = db.get(DBKeys::USE_PADDING)?;
Ok(EncryptedIdentity {
name: std::str::from_utf8(&name).unwrap().to_owned(),
encrypted_keypair,
salt,
encrypted_master_key,
encrypted_use_padding,
})
}
pub fn load_identity(password: Option<&[u8]>) -> Result<Identity, String> {
match Identity::load_encrypted_identity() {
Ok(encrypted_identity) => {
let master_key: [u8; crypto::MASTER_KEY_LEN] = if password.is_none() {
if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
} else {
match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password.unwrap(), &encrypted_identity.salt) {
let master_key: [u8; crypto::MASTER_KEY_LEN] = match password {
Some(password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, password, &encrypted_identity.salt) {
Ok(master_key) => master_key,
Err(e) => return Err(
match e {
@ -367,14 +388,28 @@ impl Identity {
}
)
}
None => if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
};
match crypto::decrypt_data(&encrypted_identity.encrypted_keypair, &master_key) {
Ok(keypair) => {
Ok(Identity{
name: encrypted_identity.name,
keypair: Keypair::from_bytes(&keypair[..]).unwrap(),
master_key: master_key,
})
match crypto::decrypt_data(&encrypted_identity.encrypted_use_padding, &master_key) {
Ok(use_padding) => {
Ok(Identity{
name: encrypted_identity.name,
keypair: Keypair::from_bytes(&keypair[..]).unwrap(),
master_key,
use_padding: byte_to_bool(use_padding[0]).unwrap(),
})
}
Err(e) => {
print_error!(e);
Err(String::from(DATABASE_CORRUPED_ERROR))
}
}
}
Err(e) => {
print_error!(e);
@ -403,31 +438,40 @@ impl Identity {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.set(DBKeys::NAME, name.as_bytes())?;
db.set(DBKeys::KEYPAIR, &encrypted_keypair)?;
let salt = if password.is_none() { //no password
db.set(DBKeys::MASTER_KEY, &master_key)?; //storing master_key in plaintext
[0; crypto::SALT_LEN]
} else {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password.unwrap());
db.set(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
let salt = match password {
Some(password) => {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password);
db.set(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
}
None => {
db.set(DBKeys::MASTER_KEY, &master_key)?; //storing master_key in plaintext
[0; crypto::SALT_LEN]
}
};
db.set(DBKeys::SALT, &salt)?;
let encrypted_use_padding = crypto::encrypt_data(&[bool_to_byte(true)], &master_key).unwrap();
db.set(DBKeys::USE_PADDING, &encrypted_use_padding)?;
Ok(Identity {
name: name.to_owned(),
keypair,
master_key,
use_padding: true,
})
}
fn update_master_key(master_key: [u8; crypto::MASTER_KEY_LEN], new_password: Option<&[u8]>) -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
let salt = if new_password.is_none() { //no password
db.update(DBKeys::MASTER_KEY, &master_key)?;
[0; crypto::SALT_LEN]
} else {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, new_password.unwrap());
db.update(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
let salt = match new_password {
Some(new_password) => {
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, new_password);
db.update(DBKeys::MASTER_KEY, &encrypted_master_key)?;
salt
}
None => {
db.update(DBKeys::MASTER_KEY, &master_key)?;
[0; crypto::SALT_LEN]
}
};
db.update(DBKeys::SALT, &salt)
}
@ -435,20 +479,19 @@ impl Identity {
pub fn change_password(old_password: Option<&[u8]>, new_password: Option<&[u8]>) -> Result<bool, String> {
match Identity::load_encrypted_identity() {
Ok(encrypted_identity) => {
let master_key: [u8; crypto::MASTER_KEY_LEN] = if old_password.is_none() {
if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
} else {
match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password.unwrap(), &encrypted_identity.salt) {
let master_key: [u8; crypto::MASTER_KEY_LEN] = match old_password {
Some(old_password) => match crypto::decrypt_master_key(&encrypted_identity.encrypted_master_key, old_password, &encrypted_identity.salt) {
Ok(master_key) => master_key,
Err(e) => return match e {
CryptoError::DecryptionFailed => Ok(false),
CryptoError::InvalidLength => Err(String::from(DATABASE_CORRUPED_ERROR))
}
}
None => if encrypted_identity.encrypted_master_key.len() == crypto::MASTER_KEY_LEN {
encrypted_identity.encrypted_master_key.try_into().unwrap()
} else {
return Err(String::from(DATABASE_CORRUPED_ERROR))
}
};
match Identity::update_master_key(master_key, new_password) {
Ok(_) => Ok(true),
@ -459,6 +502,21 @@ impl Identity {
}
}
pub fn set_identity_avatar(avatar: &[u8]) -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.upsert(DBKeys::AVATAR, avatar)
}
pub fn remove_identity_avatar() -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.del(DBKeys::AVATAR)
}
pub fn get_identity_avatar() -> Result<Vec<u8>, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.get(DBKeys::AVATAR)
}
pub fn delete_identity() -> Result<(), std::io::Error> {
std::fs::remove_file(get_database_path())
}
@ -470,6 +528,7 @@ impl Clone for Identity {
name: self.name.clone(),
keypair: Keypair::from_bytes(&self.keypair.to_bytes()).unwrap(),
master_key: self.master_key,
use_padding: self.use_padding,
}
}
}

View File

@ -12,7 +12,7 @@ impl<'a> KeyValueTable<'a> {
Ok(KeyValueTable {db, table_name})
}
pub fn set(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
Ok(self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])?)
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES (?1, ?2)", self.table_name), params![key, value])
}
pub fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
let mut stmt = self.db.prepare(&format!("SELECT value FROM {} WHERE key=\"{}\"", self.table_name, key))?;
@ -22,10 +22,13 @@ impl<'a> KeyValueTable<'a> {
None => Err(rusqlite::Error::QueryReturnedNoRows)
}
}
/*pub fn del(&self, key: &str) -> Result<usize, Error> {
self.db.execute(&format!("DELETE FROM {} WHERE key=\"{}\"", self.table_name, key), NO_PARAMS)
}*/
pub fn del(&self, key: &str) -> Result<usize, Error> {
self.db.execute(&format!("DELETE FROM {} WHERE key=\"{}\"", self.table_name, key), [])
}
pub fn update(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value])
}
pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
self.db.execute(&format!("INSERT INTO {} (key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value=?3", self.table_name), params![key, value, value])
}
}

View File

@ -2,62 +2,59 @@ mod key_value_table;
mod identity;
mod crypto;
mod session_manager;
mod protocol;
mod utils;
mod ui_interface;
mod constants;
mod discovery;
use std::{env, fs, io, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}};
use tokio::{net::TcpListener, runtime::Handle, sync::mpsc};
use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, HttpServer, http::{header, CookieBuilder}, web, web::Data};
use actix_multipart::Multipart;
use std::{env, fs, io::{self, Cursor}, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}, cmp::Ordering};
use image::GenericImageView;
use tokio::{net::TcpListener, runtime::Handle, sync::mpsc, task::JoinError};
use tungstenite::Message;
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, http::header, cookie::CookieBuilder, web, web::Data};
use actix_multipart::Multipart;
use futures::{StreamExt, TryStreamExt};
use rand_8::{RngCore, rngs::OsRng};
use rand::{RngCore, rngs::OsRng};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use platform_dirs::AppDirs;
use zeroize::Zeroize;
use utils::escape_double_quote;
use identity::Identity;
use session_manager::{SessionManager, SessionCommand, protocol};
use session_manager::{SessionManager, SessionCommand};
use ui_interface::UiConnection;
async fn start_websocket_server(global_vars: Arc<RwLock<GlobalVars>>) -> u16 {
let websocket_bind_addr = env::var("AIRA_WEBSOCKET_ADDR").unwrap_or("127.0.0.1".to_owned());
let websocket_port = env::var("AIRA_WEBSOCKET_PORT").unwrap_or("0".to_owned());
async fn start_websocket_server(ui_auth_token: Arc<RwLock<Option<String>>>, session_manager: Arc<SessionManager>) -> u16 {
let websocket_bind_addr = env::var("AIRA_WEBSOCKET_ADDR").unwrap_or_else(|_| "127.0.0.1".to_owned());
let websocket_port = env::var("AIRA_WEBSOCKET_PORT").unwrap_or_else(|_| "0".to_owned());
let server = TcpListener::bind(websocket_bind_addr+":"+&websocket_port).await.unwrap();
let websocket_port = server.local_addr().unwrap().port();
tokio::spawn(async move {
let worker_done = Arc::new(RwLock::new(true));
loop {
let (stream, _addr) = server.accept().await.unwrap();
if *worker_done.read().unwrap() {
let ui_auth_token = {
global_vars.clone().read().unwrap().ui_auth_token.clone()
};
if let Some(ui_auth_token) = ui_auth_token {
let stream = stream.into_std().unwrap();
stream.set_nonblocking(false).unwrap();
match tungstenite::accept(stream.try_clone().unwrap()) {
Ok(mut websocket) => {
if let Ok(message) = websocket.read_message() { //waiting for auth token
match message.into_text() {
Ok(token) => {
if token == ui_auth_token {
let ui_connection = UiConnection::new(websocket);
let global_vars = global_vars.clone();
global_vars.read().unwrap().session_manager.set_ui_connection(ui_connection.clone());
*worker_done.write().unwrap() = false;
websocket_worker(ui_connection, global_vars, worker_done.clone()).await;
}
let ui_auth_token = {
ui_auth_token.read().unwrap().clone()
};
if let Some(ui_auth_token) = ui_auth_token {
let stream = stream.into_std().unwrap();
stream.set_nonblocking(false).unwrap();
match tungstenite::accept(stream) {
Ok(mut websocket) => {
if let Ok(message) = websocket.read_message() { //waiting for auth token
match message.into_text() {
Ok(token) => {
if token == ui_auth_token {
let ui_connection = UiConnection::new(websocket);
session_manager.set_ui_connection(ui_connection.clone());
websocket_worker(ui_connection, session_manager.clone()).await.unwrap();
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
Err(e) => print_error!(e)
}
}
}
@ -68,7 +65,6 @@ async fn start_websocket_server(global_vars: Arc<RwLock<GlobalVars>>) -> u16 {
fn discover_peers(session_manager: Arc<SessionManager>) {
tokio::spawn(async move {
discovery::discover_peers(move |discovery_manager, ip| {
println!("New peer discovered: {}", ip);
let session_manager = session_manager.clone();
if session_manager.is_identity_loaded() {
tokio::spawn( async move {
@ -83,19 +79,18 @@ fn discover_peers(session_manager: Arc<SessionManager>) {
});
}
fn load_msgs(session_manager: Arc<SessionManager>, ui_connection: &mut UiConnection, session_id: &usize) {
fn load_msgs(session_manager: &SessionManager, ui_connection: &mut UiConnection, session_id: &usize) {
if let Some(msgs) = session_manager.load_msgs(session_id, constants::MSG_LOADING_COUNT) {
ui_connection.load_msgs(session_id, &msgs);
}
}
async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLock<GlobalVars>>, worker_done: Arc<RwLock<bool>>) {
let session_manager = global_vars.read().unwrap().session_manager.clone();
ui_connection.set_name(&session_manager.get_my_name());
async fn websocket_worker(mut ui_connection: UiConnection, session_manager: Arc<SessionManager>) -> Result<(), JoinError> {
ui_connection.set_name(&session_manager.identity.read().unwrap().as_ref().unwrap().name);
session_manager.list_contacts().into_iter().for_each(|contact|{
ui_connection.set_as_contact(contact.0, &contact.1, contact.2, &crypto::generate_fingerprint(&contact.3));
session_manager.last_loaded_msg_offsets.write().unwrap().insert(contact.0, 0);
load_msgs(session_manager.clone(), &mut ui_connection, &contact.0);
load_msgs(&session_manager, &mut ui_connection, &contact.0);
});
session_manager.sessions.read().unwrap().iter().for_each(|session| {
ui_connection.on_new_session(
@ -110,11 +105,27 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
{
let not_seen = session_manager.not_seen.read().unwrap();
if not_seen.len() > 0 {
ui_connection.set_not_seen(not_seen.clone());
ui_connection.set_not_seen(&not_seen);
}
}
session_manager.get_saved_msgs().into_iter().for_each(|msgs| {
ui_connection.load_msgs(&msgs.0, &msgs.1);
if !msgs.1.is_empty() {
ui_connection.load_msgs(&msgs.0, &msgs.1);
}
});
session_manager.pending_msgs.lock().unwrap().iter().for_each(|entry| {
entry.1.iter().for_each(|buff| {
match buff[0] {
protocol::Headers::MESSAGE => match from_utf8(&buff[1..]) {
Ok(msg) => ui_connection.new_pending_msg(entry.0, false, msg),
Err(e) => print_error!(e)
}
protocol::Headers::FILE => if let Some(filename) = protocol::get_file_name(buff) {
ui_connection.new_pending_msg(entry.0, true, filename);
}
_ => {}
}
});
});
let mut ips = Vec::new();
match if_addrs::get_if_addrs() {
@ -125,10 +136,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
Err(e) => print_error!(e)
}
ui_connection.set_local_ips(ips);
ui_connection.set_local_ips(&ips);
discover_peers(session_manager.clone());
let handle = Handle::current();
std::thread::spawn(move || { //new thread needed to block on read_message() without blocking tokio tasks
tokio::task::spawn_blocking(move || {
loop {
match ui_connection.websocket.read_message() {
Ok(msg) => {
@ -136,10 +147,12 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
ui_connection.write_message(Message::Pong(Vec::new())); //not sure if I'm doing this right
} else if msg.is_text() {
let msg = msg.into_text().unwrap();
#[cfg(debug_assertions)]
println!("Message: {}", msg);
let mut ui_connection = ui_connection.clone();
let session_manager = session_manager.clone();
handle.spawn(async move {
let args: Vec<&str> = msg.split(" ").collect();
let args: Vec<&str> = msg.split_whitespace().collect();
match args[0] {
"set_seen" => {
let session_id: usize = args[1].parse().unwrap();
@ -156,12 +169,14 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
"refresh" => discover_peers(session_manager.clone()),
"send" => {
let session_id: usize = args[1].parse().unwrap();
let buffer = protocol::new_message(msg[args[0].len()+args[1].len()+2..].to_string());
match session_manager.send_command(&session_id, SessionCommand::Send {
buff: buffer.clone()
}).await {
Ok(_) => session_manager.store_msg(&session_id, true, buffer),
Err(e) => print_error!(e)
let msg_content = &msg[args[0].len()+args[1].len()+2..];
let buffer = protocol::new_message(msg_content);
#[allow(unused_must_use)] {
if let Ok(sent) = session_manager.send_or_add_to_pending(&session_id, buffer).await {
if !sent {
ui_connection.new_pending_msg(&session_id, false, msg_content);
}
}
}
}
"large_files" => {
@ -170,48 +185,40 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
for n in (2..args.len()).step_by(2) {
file_info.push((args[n].parse::<u64>().unwrap(), base64::decode(args[n+1]).unwrap()));
}
if let Err(e) = session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::ask_large_files(file_info)
}).await {
print_error!(e);
#[allow(unused_must_use)] {
session_manager.send_or_add_to_pending(&session_id, protocol::ask_large_files(file_info)).await;
}
}
"download" => {
let session_id: usize = args[1].parse().unwrap();
if let Err(e) = session_manager.send_command(&session_id, SessionCommand::Send {
session_manager.send_command(&session_id, SessionCommand::Send {
buff: vec![protocol::Headers::ACCEPT_LARGE_FILES]
}).await {
print_error!(e);
}
}).await;
}
"abort" => {
let session_id: usize = args[1].parse().unwrap();
if let Err(e) = session_manager.send_command(&session_id, SessionCommand::Send {
session_manager.send_command(&session_id, SessionCommand::Send {
buff: vec![protocol::Headers::ABORT_FILES_TRANSFER]
}).await {
print_error!(e);
}
}).await;
}
"sending_ended" => {
let session_id: usize = args[1].parse().unwrap();
if let Err(e) = session_manager.send_command(&session_id, SessionCommand::SendingEnded).await {
print_error!(e);
}
session_manager.send_command(&session_id, SessionCommand::SendingEnded).await;
}
"load_msgs" => {
let session_id: usize = args[1].parse().unwrap();
load_msgs(session_manager.clone(), &mut ui_connection, &session_id);
load_msgs(&session_manager, &mut ui_connection, &session_id);
}
"contact" => {
let session_id: usize = args[1].parse().unwrap();
match session_manager.add_contact(session_id, msg[args[0].len()+args[1].len()+2..].to_string()) {
match session_manager.add_contact(session_id) {
Ok(_) => {},
Err(e) => print_error!(e)
}
}
"uncontact" => {
let session_id: usize = args[1].parse().unwrap();
match session_manager.remove_contact(session_id) {
match session_manager.remove_contact(&session_id) {
Ok(_) => {},
Err(e) => print_error!(e)
}
@ -230,12 +237,28 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
Err(e) => print_error!(e)
}
}
"refresh_profile" => {
let session_id: usize = args[1].parse().unwrap();
session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::ask_profile_info()
}).await;
}
"remove_avatar" => {
match session_manager.remove_avatar().await {
Ok(_) => ui_connection.on_avatar_changed(None),
Err(e) => print_error!(e)
}
}
"set_use_padding" => {
let use_padding: bool = args[1].parse().unwrap();
if let Err(e) = session_manager.identity.write().unwrap().as_mut().unwrap().set_use_padding(use_padding) {
print_error!(e);
}
}
"change_name" => {
let new_name = &msg[args[0].len()+1..];
match session_manager.change_name(new_name.to_string()).await {
Ok(_) => {
ui_connection.set_name(new_name)
}
Ok(_) => ui_connection.set_name(new_name),
Err(e) => print_error!(e)
};
}
@ -248,11 +271,11 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
(None, Some(base64::decode(args[1]).unwrap()))
};
let result = Identity::change_password(old_password.as_deref(), new_password.as_deref());
if old_password.is_some() {
old_password.unwrap().zeroize();
if let Some(mut old_password) = old_password {
old_password.zeroize();
}
let is_identity_protected = if new_password.is_some() {
new_password.unwrap().zeroize();
let is_identity_protected = if let Some(mut new_password) = new_password {
new_password.zeroize();
true
} else {
false
@ -276,7 +299,6 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
Err(e) => {
match e {
tungstenite::Error::ConnectionClosed => {
*worker_done.write().unwrap() = true;
break;
}
_ => print_error!(e)
@ -284,35 +306,113 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
}
}
}
});
}).await
}
fn is_authenticated(req: &HttpRequest) -> bool {
if let Some(cookie) = req.cookie(constants::HTTP_COOKIE_NAME) {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
if let Some(token) = &global_vars.read().unwrap().ui_auth_token {
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if let Some(token) = global_vars.ui_auth_token.read().unwrap().as_ref() {
return token == cookie.value();
}
}
false
}
async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResponse {
if let Ok(Some(mut field)) = payload.try_next().await {
let content_disposition = field.content_disposition();
if let Some(name) = content_disposition.get_name() {
if name == "avatar" {
let mut buffer = Vec::new();
while let Some(Ok(chunk)) = field.next().await {
buffer.extend(chunk);
}
match image::guess_format(&buffer) {
Ok(format) => {
match image::load_from_memory_with_format(&buffer, format) {
Ok(image) => {
let (width, height) = image.dimensions();
let image = match width.cmp(&height) {
Ordering::Greater => image.crop_imm((width-height)/2, 0, height, height),
Ordering::Less => image.crop_imm(0, (height-width)/2, width, width),
Ordering::Equal => image,
};
let mut avatar = Vec::new();
image.write_to(&mut Cursor::new(&mut avatar), format).unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
let session_manager = &global_vars.session_manager;
let is_authenticated = is_authenticated(&req);
let is_running = session_manager.is_identity_loaded();
if is_authenticated || !is_running {
match Identity::set_identity_avatar(&avatar) {
Ok(_) => {
if is_authenticated && is_running {
session_manager.send_avatar(&avatar).await;
}
return HttpResponse::Ok().finish();
}
Err(e) => {
print_error!(e);
return HttpResponse::InternalServerError().finish();
}
}
}
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
}
}
}
HttpResponse::BadRequest().finish()
}
fn reply_with_avatar(avatar: Option<Vec<u8>>, name: Option<&str>) -> HttpResponse {
match avatar {
Some(avatar) => HttpResponse::Ok().content_type("image/*").body(avatar),
None => match name {
Some(name) => {
#[cfg(not(debug_assertions))]
let svg = include_str!(concat!(env!("OUT_DIR"), "/text_avatar.svg"));
#[cfg(debug_assertions)]
let svg = replace_fields("src/frontend/imgs/text_avatar.svg");
HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().next().unwrap_or('?').to_string()))
}
None => HttpResponse::InternalServerError().finish()
}
}
}
async fn handle_avatar(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split('/').collect();
if splits.len() == 2 {
if splits[1] == "self" {
return reply_with_avatar(Identity::get_identity_avatar().ok(), Identity::get_identity_name().ok().as_deref());
}
} else if splits.len() == 3 && is_authenticated(&req) {
if let Ok(session_id) = splits[1].parse() {
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
return reply_with_avatar(global_vars.session_manager.get_avatar(&session_id), Some(splits[2]));
}
}
HttpResponse::BadRequest().finish()
}
#[derive(Deserialize, Serialize, Debug)]
struct FileInfo {
uuid: String,
file_name: String,
}
fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
async fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
if is_authenticated(&req) {
match Uuid::from_str(&file_info.uuid) {
Ok(uuid) => {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
match global_vars.read().unwrap().session_manager.load_file(uuid) {
Some(buffer) => {
return HttpResponse::Ok().header("Content-Disposition", format!("attachment; filename=\"{}\"", escape_double_quote(html_escape::decode_html_entities(&file_info.file_name).to_string()))).content_type("application/octet-stream").body(buffer);
}
None => {}
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if let Some(buffer) = global_vars.session_manager.identity.read().unwrap().as_ref().unwrap().load_file(uuid) {
return HttpResponse::Ok().append_header(("Content-Disposition", format!("attachment; filename=\"{}\"", escape_double_quote(html_escape::decode_html_entities(&file_info.file_name).to_string())))).content_type("application/octet-stream").body(buffer);
}
}
Err(e) => print_error!(e)
@ -325,36 +425,27 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
if is_authenticated(&req) {
let mut session_id: Option<usize> = None;
while let Ok(Some(mut field)) = payload.try_next().await {
let content_disposition = field.content_disposition().unwrap();
let content_disposition = field.content_disposition();
if let Some(name) = content_disposition.get_name() {
if name == "session_id" {
if let Some(Ok(raw_id)) = field.next().await {
session_id = Some(from_utf8(&raw_id).unwrap().parse().unwrap());
}
} else if session_id.is_some() {
let filename = content_disposition.get_filename().unwrap();
let filename = content_disposition.get_filename().unwrap().to_owned();
let session_id = session_id.unwrap();
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars_read = global_vars.read().unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if req.path() == "/send_file" {
let mut buffer = Vec::new();
while let Some(Ok(chunk)) = field.next().await {
buffer.extend(chunk);
}
match global_vars_read.session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::file(filename, &buffer)
}).await {
Ok(_) => {
match global_vars_read.session_manager.store_file(&session_id, &buffer) {
Ok(file_uuid) => {
let msg = [&[protocol::Headers::FILE][..], file_uuid.as_bytes(), filename.as_bytes()].concat();
global_vars_read.session_manager.store_msg(&session_id, true, msg);
return HttpResponse::Ok().body(file_uuid.to_string());
}
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
if let Ok(sent) = global_vars.session_manager.send_or_add_to_pending(&session_id, protocol::file(&filename, &buffer)).await {
return if sent {
HttpResponse::Ok().finish()
} else {
HttpResponse::Ok().body("pending")
};
}
} else {
let (ack_sender, mut ack_receiver) = mpsc::channel(1);
@ -378,22 +469,20 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
break;
}
}
if let Err(e) = global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
if !global_vars.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
plain_text: chunk_buffer.clone()
}).await {
print_error!(e);
return HttpResponse::InternalServerError().finish();
}
if !match ack_receiver.recv().await {
Some(should_continue) => {
//send previous encrypted chunk even if transfert is aborted to keep PSEC nonces syncrhonized
if let Err(e) = global_vars_read.session_manager.send_command(&session_id, SessionCommand::SendEncryptedFileChunk {
if global_vars.session_manager.send_command(&session_id, SessionCommand::SendEncryptedFileChunk {
ack_sender: ack_sender.clone()
}).await {
print_error!(e);
false
} else {
should_continue
} else {
false
}
}
None => false
@ -417,29 +506,31 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
async fn handle_logout(req: HttpRequest) -> HttpResponse {
if is_authenticated(&req) {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let mut global_vars_write = global_vars.write().unwrap();
if global_vars_write.session_manager.is_identity_loaded() {
global_vars_write.ui_auth_token = None;
global_vars_write.session_manager.stop().await;
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if global_vars.session_manager.is_identity_loaded() {
*global_vars.ui_auth_token.write().unwrap() = None;
global_vars.session_manager.stop().await;
}
if Identity::is_protected().unwrap_or(true) {
HttpResponse::Found().header(header::LOCATION, "/").finish()
HttpResponse::Found().append_header((header::LOCATION, "/")).finish()
} else {
HttpResponse::Ok().body(include_str!("frontend/logout.html"))
#[cfg(debug_assertions)]
let html = fs::read_to_string("src/frontend/logout.html").unwrap();
#[cfg(not(debug_assertions))]
let html = include_str!("frontend/logout.html");
HttpResponse::Ok().body(html)
}
} else {
HttpResponse::Unauthorized().finish()
}
}
fn login(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
let mut global_vars_write = global_vars.write().unwrap();
let session_manager = global_vars_write.session_manager.clone();
fn login(identity: Identity, global_vars: &GlobalVars) -> HttpResponse {
let session_manager = global_vars.session_manager.clone();
if !session_manager.is_identity_loaded() {
global_vars_write.session_manager.set_identity(Some(identity));
global_vars_write.tokio_handle.clone().spawn(async move {
if SessionManager::start_listener(session_manager.clone()).await.is_err() {
session_manager.set_identity(Some(identity));
global_vars.tokio_handle.spawn(async move {
if SessionManager::start_listener(session_manager).await.is_err() {
print_error!("You won't be able to receive incomming connections from other peers.");
}
});
@ -447,29 +538,33 @@ fn login(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpRespo
let mut raw_cookie = [0; 32];
OsRng.fill_bytes(&mut raw_cookie);
let cookie_value = base64::encode(raw_cookie);
global_vars_write.ui_auth_token = Some(cookie_value.clone());
*global_vars.ui_auth_token.write().unwrap() = Some(cookie_value.clone());
let cookie = CookieBuilder::new(constants::HTTP_COOKIE_NAME, cookie_value).max_age(time::Duration::hours(4)).finish();
HttpResponse::Found()
.header(header::LOCATION, "/")
.set_header(header::SET_COOKIE, cookie.to_string())
.append_header((header::LOCATION, "/"))
.insert_header((header::SET_COOKIE, cookie.to_string()))
.finish()
}
fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
match Identity::clear_temporary_files() {
fn on_identity_loaded(identity: Identity, global_vars: &Arc<GlobalVars>) -> HttpResponse {
match Identity::clear_cache() {
Ok(_) => {},
Err(e) => print_error!(e)
}
login(identity, global_vars)
}
fn handle_login(req: HttpRequest, mut params: web::Form<LoginParams>) -> HttpResponse {
#[derive(Serialize, Deserialize)]
struct LoginParams {
password: String,
}
async fn handle_login(req: HttpRequest, mut params: web::Form<LoginParams>) -> HttpResponse {
let response = match Identity::load_identity(Some(params.password.as_bytes())) {
Ok(identity) => {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
on_identity_loaded(identity, global_vars)
}
Err(e) => generate_login_response(Some(&e.to_string()))
Err(e) => generate_login_response(Some(&e))
};
params.password.zeroize();
response
@ -477,7 +572,7 @@ fn handle_login(req: HttpRequest, mut params: web::Form<LoginParams>) -> HttpRes
fn get_login_body(error_msg: Option<&str>) -> Result<String, rusqlite::Error> {
#[cfg(debug_assertions)]
let html = fs::read_to_string("src/frontend/login.html").unwrap();
let html = replace_fields("src/frontend/login.html");
#[cfg(not(debug_assertions))]
let html = include_str!(concat!(env!("OUT_DIR"), "/login.html"));
Ok(html
@ -487,8 +582,10 @@ fn get_login_body(error_msg: Option<&str>) -> Result<String, rusqlite::Error> {
})
.replace("IDENTITY_NAME", &match Identity::get_identity_name() {
Ok(name) => format!("\"{}\"", html_escape::encode_double_quoted_attribute(&name)),
Err(e) => {
print_error!(e);
Err(_) => {
if let Err(e) = Identity::remove_identity_avatar() {
print_error!(e);
}
"null".to_owned()
}
}
@ -506,19 +603,25 @@ fn generate_login_response(error_msg: Option<&str>) -> HttpResponse {
}
}
#[derive(Serialize, Deserialize)]
struct CreateParams {
name: String,
password: String,
password_confirm: String,
}
async fn handle_create(req: HttpRequest, mut params: web::Form<CreateParams>) -> HttpResponse {
let response = if params.password == params.password_confirm {
match Identity::create_identidy(
&params.name,
if params.password.len() == 0 { //no password
if params.password.is_empty() { //no password
None
} else {
Some(params.password.as_bytes())
}
) {
Ok(identity) => {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
login(identity, global_vars.get_ref())
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
login(identity, global_vars)
}
Err(e) => {
print_error!(e);
@ -533,7 +636,7 @@ async fn handle_create(req: HttpRequest, mut params: web::Form<CreateParams>) ->
response
}
fn index_not_logged_in(global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
fn index_not_logged_in(global_vars: &Arc<GlobalVars>) -> HttpResponse {
if Identity::is_protected().unwrap_or(true) {
generate_login_response(None)
} else {
@ -545,19 +648,21 @@ fn index_not_logged_in(global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
}
async fn handle_index(req: HttpRequest) -> HttpResponse {
let global_vars = req.app_data::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
let global_vars = req.app_data::<Data<GlobalVars>>().unwrap();
if is_authenticated(&req) {
let global_vars_read = global_vars.read().unwrap();
#[cfg(debug_assertions)]
let html = fs::read_to_string("src/frontend/index.html").unwrap();
let html = fs::read_to_string("src/frontend/index.html").unwrap()
.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION"));
#[cfg(not(debug_assertions))]
let html = include_str!(concat!(env!("OUT_DIR"), "/index.html"));
let identity = global_vars.session_manager.identity.read().unwrap();
let identity = identity.as_ref().unwrap();
HttpResponse::Ok().body(
html
.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION"))
.replace("IDENTITY_FINGERPRINT", &crypto::generate_fingerprint(&global_vars_read.session_manager.get_my_public_key()))
.replace("WEBSOCKET_PORT", &global_vars_read.websocket_port.to_string())
.replace("IDENTITY_FINGERPRINT", &crypto::generate_fingerprint(&identity.get_public_key()))
.replace("WEBSOCKET_PORT", &global_vars.websocket_port.to_string())
.replace("IS_IDENTITY_PROTECTED", &Identity::is_protected().unwrap().to_string())
.replace("PSEC_PADDING", &identity.use_padding.to_string())
)
} else {
index_not_logged_in(global_vars)
@ -566,8 +671,20 @@ async fn handle_index(req: HttpRequest) -> HttpResponse {
const JS_CONTENT_TYPE: &str = "text/javascript";
fn handle_static(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split("/").collect();
#[cfg(debug_assertions)]
fn replace_fields(file_path: &str) -> String {
use yaml_rust::YamlLoader;
let mut content = fs::read_to_string(file_path).unwrap();
let config = &YamlLoader::load_from_str(&fs::read_to_string("config.yml").unwrap()).unwrap()[0];
let fields = config.as_hash().unwrap();
fields.into_iter().for_each(|field| {
content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap());
});
content
}
async fn handle_static(req: HttpRequest) -> HttpResponse {
let splits: Vec<&str> = req.path()[1..].split('/').collect();
if splits[0] == "static" {
let mut response_builder = HttpResponse::Ok();
match splits[1] {
@ -580,7 +697,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
}
"index.css" => {
#[cfg(debug_assertions)]
return response_builder.body(fs::read_to_string("src/frontend/index.css").unwrap());
return response_builder.body(replace_fields("src/frontend/index.css"));
#[cfg(not(debug_assertions))]
return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/index.css")));
}
@ -591,7 +708,8 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
} else {
"none"
};
match match splits[3] {
if let Some(body) = match splits[3] {
"logo" => Some(include_str!("frontend/imgs/icons/logo.svg")),
"verified" => Some(include_str!("frontend/imgs/icons/verified.svg")),
"add_contact" => Some(include_str!("frontend/imgs/icons/add_contact.svg")),
"remove_contact" => Some(include_str!("frontend/imgs/icons/remove_contact.svg")),
@ -603,13 +721,11 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
"refresh" => Some(include_str!("frontend/imgs/icons/refresh.svg")),
"info" => Some(include_str!("frontend/imgs/icons/info.svg")),
"delete_conversation" => Some(include_str!("frontend/imgs/icons/delete_conversation.svg")),
"profile" => Some(include_str!("frontend/imgs/icons/profile.svg")),
_ => None
} {
Some(body) => {
response_builder.content_type("image/svg+xml");
return response_builder.body(body.replace("FILL_COLOR", color))
}
None => {}
response_builder.content_type("image/svg+xml");
return response_builder.body(body.replace("FILL_COLOR", color))
}
} else if splits.len() == 3 {
match splits[2] {
@ -639,7 +755,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
}
"style.css" => {
#[cfg(debug_assertions)]
return response_builder.body(fs::read_to_string("src/frontend/commons/style.css").unwrap());
return response_builder.body(replace_fields("src/frontend/commons/style.css"));
#[cfg(not(debug_assertions))]
return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/commons/style.css")));
}
@ -649,13 +765,12 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
}
"libs" => {
if splits.len() == 3 {
match match splits[2] {
if let Some(body) = match splits[2] {
"linkify.min.js" => Some(include_str!("frontend/libs/linkify.min.js")),
"linkify-element.min.js" => Some(include_str!("frontend/libs/linkify-element.min.js")),
_ => None
} {
Some(body) => return response_builder.content_type(JS_CONTENT_TYPE).body(body),
None => {}
return response_builder.content_type(JS_CONTENT_TYPE).body(body);
}
}
}
@ -665,17 +780,15 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
HttpResponse::NotFound().finish()
}
#[actix_web::main]
async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<()> {
let http_addr = env::var("AIRA_HTTP_ADDR").unwrap_or("127.0.0.1".to_owned()).parse().expect("AIRA_HTTP_ADDR invalid");
async fn start_http_server(global_vars: GlobalVars) -> io::Result<()> {
let http_addr = env::var("AIRA_HTTP_ADDR").unwrap_or_else(|_| "127.0.0.1".to_owned()).parse().expect("AIRA_HTTP_ADDR invalid");
let http_port = match env::var("AIRA_HTTP_PORT") {
Ok(port) => port.parse().expect("AIRA_HTTP_PORT invalid"),
Err(_) => constants::UI_PORT
};
let server = HttpServer::new(move || {
let global_vars_clone = global_vars.clone();
App::new()
.data(global_vars_clone)
.app_data(Data::new(global_vars.clone()))
.service(web::resource("/")
.route(web::get().to(handle_index))
.route(web::post().to(handle_create))
@ -684,7 +797,9 @@ async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<(
.route("/send_file", web::post().to(handle_send_file))
.route("/send_large_file", web::post().to(handle_send_file))
.route("/load_file", web::get().to(handle_load_file))
.route("/static/.*", web::get().to(handle_static))
.route("/set_avatar", web::post().to(handle_set_avatar))
.route("/avatar/{_}*", web::get().to(handle_avatar))
.route("/static/{_}*", web::get().to(handle_static))
.route("/logout", web::get().to(handle_logout))
}
).bind(SocketAddr::new(http_addr, http_port))?;
@ -696,22 +811,11 @@ async fn start_http_server(global_vars: Arc<RwLock<GlobalVars>>) -> io::Result<(
server.run().await
}
#[derive(Serialize, Deserialize)]
struct LoginParams {
password: String,
}
#[derive(Serialize, Deserialize)]
struct CreateParams {
name: String,
password: String,
password_confirm: String,
}
#[derive(Clone)]
struct GlobalVars {
session_manager: Arc<SessionManager>,
websocket_port: u16,
ui_auth_token: Option<String>,
ui_auth_token: Arc<RwLock<Option<String>>>,
tokio_handle: Handle,
}
@ -722,13 +826,13 @@ async fn main() {
print_error!(e);
}
}
let global_vars = Arc::new(RwLock::new(GlobalVars {
session_manager: Arc::new(SessionManager::new()),
websocket_port: 0,
ui_auth_token: None,
let ui_auth_token = Arc::new(RwLock::new(None));
let session_manager = Arc::new(SessionManager::new());
let websocket_port = start_websocket_server(ui_auth_token.clone(), session_manager.clone()).await;
start_http_server(GlobalVars {
session_manager,
websocket_port,
ui_auth_token,
tokio_handle: Handle::current(),
}));
let websocket_port = start_websocket_server(global_vars.clone()).await;
global_vars.write().unwrap().websocket_port = websocket_port;
start_http_server(global_vars).unwrap();
}).await.unwrap();
}

View File

@ -5,43 +5,49 @@ pub struct Headers;
impl Headers {
pub const MESSAGE: u8 = 0x00;
pub const ASK_NAME: u8 = 0x01;
pub const TELL_NAME: u8 = 0x02;
pub const FILE: u8 = 0x03;
pub const ASK_LARGE_FILES: u8 = 0x04;
pub const ACCEPT_LARGE_FILES: u8 = 0x05;
pub const LARGE_FILE_CHUNK: u8 = 0x06;
pub const ACK_CHUNK: u8 = 0x07;
pub const ABORT_FILES_TRANSFER: u8 = 0x08;
pub const FILE: u8 = 0x01;
pub const ASK_PROFILE_INFO: u8 = 0x02;
pub const NAME: u8 = 0x03;
pub const AVATAR: u8 = 0x04;
pub const REMOVE_AVATAR: u8 = 0x05;
pub const ASK_LARGE_FILES: u8 = 0x06;
pub const ACCEPT_LARGE_FILES: u8 = 0x07;
pub const LARGE_FILE_CHUNK: u8 = 0x08;
pub const ACK_CHUNK: u8 = 0x09;
pub const ABORT_FILES_TRANSFER: u8 = 0x0a;
}
pub fn new_message(message: String) -> Vec<u8> {
pub fn new_message(message: &str) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat()
}
pub fn ask_name() -> Vec<u8> {
vec![Headers::ASK_NAME]
pub fn ask_profile_info() -> Vec<u8> {
vec![Headers::ASK_PROFILE_INFO]
}
pub fn tell_name(name: &str) -> Vec<u8> {
[&[Headers::TELL_NAME], name.as_bytes()].concat()
pub fn name(name: &str) -> Vec<u8> {
[&[Headers::NAME], name.as_bytes()].concat()
}
pub fn file(file_name: &str, buffer: &[u8]) -> Vec<u8> {
[&[Headers::FILE], &(file_name.len() as u16).to_be_bytes()[..], file_name.as_bytes(), buffer].concat()
}
pub fn parse_file<'a>(buffer: &'a [u8]) -> Option<(&'a [u8], &'a [u8])> {
pub fn get_file_name(buffer: &[u8]) -> Option<&str> {
if buffer.len() > 3 {
let file_name_len = u16::from_be_bytes([buffer[1], buffer[2]]) as usize;
if buffer.len() > 3+file_name_len {
let file_name = &buffer[3..3+file_name_len];
return Some((file_name, &buffer[3+file_name_len..]));
return from_utf8(&buffer[3..3+file_name_len]).ok();
}
}
None
}
pub fn parse_file(buffer: &[u8]) -> Option<(&str, &[u8])> {
let file_name = get_file_name(buffer)?;
Some((file_name, &buffer[3+file_name.len()..]))
}
pub fn ask_large_files(file_info: Vec<(u64, Vec<u8>)>) -> Vec<u8> {
let mut buff = vec![Headers::ASK_LARGE_FILES];
file_info.into_iter().for_each(|info| {
@ -79,4 +85,12 @@ pub fn parse_ask_files(buffer: &[u8]) -> Option<Vec<(u64, String)>> {
}
}
Some(files_info)
}
pub fn avatar(avatar: &[u8]) -> Vec<u8> {
[&[Headers::AVATAR], avatar].concat()
}
pub fn remove_avatar() -> Vec<u8> {
vec![Headers::REMOVE_AVATAR]
}

View File

@ -1,28 +1,10 @@
mod session;
pub mod protocol;
use std::{collections::HashMap, fs::OpenOptions, io::{self, Write}, net::{IpAddr, SocketAddr}, path::PathBuf, str::from_utf8, sync::{Mutex, RwLock, Arc}};
use tokio::{net::{TcpListener, TcpStream}, sync::mpsc::{self, Sender, Receiver}};
use libmdns::Service;
use strum_macros::Display;
use session::Session;
use ed25519_dalek::PUBLIC_KEY_LENGTH;
use uuid::Uuid;
use platform_dirs::UserDirs;
use crate::{constants, crypto, discovery, identity::{Contact, Identity}, print_error, utils::{get_unix_timestamp, get_not_used_path}};
use crate::ui_interface::UiConnection;
use self::session::SessionWrite;
#[derive(Display, Debug, PartialEq, Eq)]
pub enum SessionError {
ConnectionReset,
BrokenPipe,
TransmissionCorrupted,
BufferTooLarge,
InvalidSessionId,
Unknown,
}
use async_psec::{PUBLIC_KEY_LENGTH, Session, SessionWriteHalf, PsecWriter, PsecReader, PsecError};
use crate::{constants, crypto, discovery, identity::{Contact, Identity, Message}, ui_interface::UiConnection, print_error, protocol, utils::{get_not_used_path, get_unix_timestamp_ms, get_unix_timestamp_sec}};
pub enum SessionCommand {
Send {
@ -57,6 +39,7 @@ pub struct LargeFilesDownload {
#[derive(Clone)]
pub struct SessionData {
pub name: String,
avatar: Option<Uuid>,
pub outgoing: bool,
pub peer_public_key: [u8; PUBLIC_KEY_LENGTH],
pub ip: IpAddr,
@ -67,11 +50,12 @@ pub struct SessionData {
pub struct SessionManager {
session_counter: RwLock<usize>,
pub sessions: RwLock<HashMap<usize, SessionData>>,
identity: RwLock<Option<Identity>>,
pub identity: RwLock<Option<Identity>>,
ui_connection: Mutex<Option<UiConnection>>,
loaded_contacts: RwLock<HashMap<usize, Contact>>,
pub last_loaded_msg_offsets: RwLock<HashMap<usize, usize>>,
pub saved_msgs: Mutex<HashMap<usize, Vec<(bool, Vec<u8>)>>>,
saved_msgs: RwLock<HashMap<usize, Vec<Message>>>,
pub pending_msgs: Mutex<HashMap<usize, Vec<Vec<u8>>>>,
pub not_seen: RwLock<Vec<usize>>,
mdns_service: Mutex<Option<Service>>,
listener_stop_signal: Mutex<Option<Sender<()>>>,
@ -81,55 +65,79 @@ impl SessionManager {
fn with_ui_connection<F>(&self, f: F) where F: FnOnce(&mut UiConnection) {
let mut ui_connection_opt = self.ui_connection.lock().unwrap();
match ui_connection_opt.as_mut() {
Some(ui_connection) => if ui_connection.is_valid {
if let Some(ui_connection) = ui_connection_opt.as_mut() {
if ui_connection.is_valid {
f(ui_connection);
}
None => {}
}
}
fn get_all_senders(&self) -> Vec<Sender<SessionCommand>> {
self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect()
}
async fn encrypt_and_send<T: PsecWriter>(&self, writer: &mut T, buff: &[u8]) -> Result<(), PsecError> {
let use_padding = self.identity.read().unwrap().as_ref().unwrap().use_padding;
writer.encrypt_and_send(buff, use_padding).await
}
pub async fn connect_to(session_manager: Arc<SessionManager>, ip: IpAddr) -> io::Result<()> {
let stream = TcpStream::connect(SocketAddr::new(ip, constants::PORT)).await?;
SessionManager::handle_new_session(session_manager, Session::new(stream), true);
SessionManager::handle_new_session(session_manager, Session::from(stream), true);
Ok(())
}
pub fn store_msg(&self, session_id: &usize, outgoing: bool, buffer: Vec<u8>) {
pub fn store_msg(&self, session_id: &usize, message: Message) {
let mut msg_saved = false;
if self.is_contact(session_id) {
match self.identity.read().unwrap().as_ref().unwrap().store_msg(&self.loaded_contacts.read().unwrap().get(session_id).unwrap().uuid, outgoing, &buffer) {
if let Some(contact) = self.loaded_contacts.read().unwrap().get(session_id) {
let mut offsets = self.last_loaded_msg_offsets.write().unwrap(); //locking mutex before modifying the DB to prevent race conditions with load_msgs()
match self.identity.read().unwrap().as_ref().unwrap().store_msg(&contact.uuid, &message) {
Ok(_) => {
*offsets.get_mut(session_id).unwrap() += 1;
msg_saved = true;
*self.last_loaded_msg_offsets.write().unwrap().get_mut(session_id).unwrap() += 1;
},
Err(e) => print_error!(e),
}
}
if !msg_saved {
self.saved_msgs.lock().unwrap().get_mut(&session_id).unwrap().push((false, buffer));
//can be None if session disconnected
if let Some(saved_msgs) = self.saved_msgs.write().unwrap().get_mut(&session_id) {
saved_msgs.push(message)
}
}
}
fn get_session_sender(&self, session_id: &usize) -> Option<Sender<SessionCommand>> {
let mut sessions = self.sessions.write().unwrap();
match sessions.get_mut(session_id) {
Some(session_data) => Some(session_data.sender.clone()),
None => None
}
let sessions = self.sessions.read().unwrap();
sessions.get(session_id).map(|session_data| session_data.sender.clone())
}
pub async fn send_command(&self, session_id: &usize, session_command: SessionCommand) -> Result<(), SessionError> {
pub async fn send_command(&self, session_id: &usize, session_command: SessionCommand) -> bool {
if let Some(sender) = self.get_session_sender(session_id) {
match sender.send(session_command).await {
Ok(_) => Ok(()),
Ok(_) => true,
Err(e) => {
print_error!(e);
Err(SessionError::BrokenPipe)
false
}
}
} else {
Err(SessionError::InvalidSessionId)
false
}
}
pub async fn send_or_add_to_pending(&self, session_id: &usize, buff: Vec<u8>) -> Result<bool, ()> {
if let Some(sender) = self.get_session_sender(session_id) {
match sender.send(SessionCommand::Send { buff }).await {
Ok(_) => Ok(true),
Err(e) => {
print_error!(e);
Err(())
}
}
} else {
self.pending_msgs.lock().unwrap().get_mut(session_id).unwrap().push(buff);
Ok(false)
}
}
@ -138,27 +146,85 @@ impl SessionManager {
ui_connection.on_disconnected(&session_id);
});
self.sessions.write().unwrap().remove(session_id);
self.saved_msgs.lock().unwrap().remove(session_id);
self.saved_msgs.write().unwrap().remove(session_id);
self.not_seen.write().unwrap().retain(|x| x != session_id);
}
async fn send_msg(&self, session_id: usize, session_write: &mut SessionWrite, buff: &[u8], is_sending: &mut bool, file_ack_sender: &mut Option<Sender<bool>>) -> Result<(), SessionError> {
session_write.encrypt_and_send(&buff).await?;
if buff[0] == protocol::Headers::ACCEPT_LARGE_FILES {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download.as_mut().unwrap().accepted = true;
} else if buff[0] == protocol::Headers::ABORT_FILES_TRANSFER {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
*is_sending = false;
if let Some(ack_sender) = file_ack_sender {
if let Err(e) = ack_sender.send(false).await {
print_error!(e);
}
*file_ack_sender = None;
fn set_avatar_uuid(&self, session_id: &usize, avatar_uuid: Option<Uuid>) {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
if let Some(contact) = loaded_contacts.get_mut(session_id) {
contact.avatar = avatar_uuid;
if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().set_contact_avatar(&contact.uuid, avatar_uuid.as_ref()) {
print_error!(e);
}
} else {
self.sessions.write().unwrap().get_mut(session_id).unwrap().avatar = avatar_uuid;
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_msg_sent(session_id, &buff);
ui_connection.on_avatar_changed(Some(session_id));
});
}
async fn send_store_and_inform<T: PsecWriter>(&self, session_id: usize, session_writer: &mut T, buff: Vec<u8>) -> Result<Option<Vec<u8>>, PsecError> {
self.encrypt_and_send(session_writer, &buff).await?;
let timestamp = get_unix_timestamp_sec();
Ok(match buff[0] {
protocol::Headers::MESSAGE => {
let msg = Message {
outgoing: true,
timestamp,
data: buff,
};
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_msg(&session_id, &msg);
});
self.store_msg(&session_id, msg);
None
}
protocol::Headers::FILE => {
if let Some((filename, content)) = protocol::parse_file(&buff) {
match self.store_file(&session_id, content) {
Ok(file_uuid) => {
let msg = [&[protocol::Headers::FILE][..], file_uuid.as_bytes(), filename.as_bytes()].concat();
self.store_msg(&session_id, Message {
outgoing: true,
timestamp,
data: msg,
});
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_file(&session_id, true, timestamp, filename, file_uuid);
});
}
Err(e) => print_error!(e)
}
}
None
}
_ => Some(buff)
})
}
async fn send_msg(&self, session_id: usize, session_write: &mut SessionWriteHalf, buff: Vec<u8>, is_sending: &mut bool, file_ack_sender: &mut Option<Sender<bool>>) -> Result<(), PsecError> {
if let Some(buff) = self.send_store_and_inform(session_id, session_write, buff).await? {
//not a message or a file
match buff[0] {
protocol::Headers::ACCEPT_LARGE_FILES => self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download.as_mut().unwrap().accepted = true,
protocol::Headers::ABORT_FILES_TRANSFER => {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
*is_sending = false;
if let Some(ack_sender) = file_ack_sender {
if let Err(e) = ack_sender.send(false).await {
print_error!(e);
}
*file_ack_sender = None;
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_file_transfer_aborted(&session_id);
});
}
_ => {}
}
}
Ok(())
}
@ -172,74 +238,17 @@ impl SessionManager {
let mut msg_queue = Vec::new();
let mut is_sending = false;
let (session_read, mut session_write) = session.into_spit().unwrap();
let receiving = session_read.receive_and_decrypt();
let (session_read, mut session_write) = session.into_split().unwrap();
let receiving = session_read.into_receive_and_decrypt();
tokio::pin!(receiving);
loop {
tokio::select! {
result = &mut receiving => {
match result {
Ok((session_read, buffer)) => {
receiving.set(session_read.receive_and_decrypt());
match result.0 {
Ok(buffer) => {
let session_read = result.1;
receiving.set(session_read.into_receive_and_decrypt());
match buffer[0] {
protocol::Headers::ASK_NAME => {
let name = {
self.identity.read().unwrap().as_ref().and_then(|identity| Some(identity.name.clone()))
};
if name.is_some() { //can be None if we log out just before locking the identity mutex
if let Err(e) = session_write.encrypt_and_send(&protocol::tell_name(&name.unwrap())).await {
print_error!(e);
break;
}
}
}
protocol::Headers::TELL_NAME => {
match from_utf8(&buffer[1..]) {
Ok(new_name) => {
let new_name = new_name.replace('\n', " ");
self.with_ui_connection(|ui_connection| {
ui_connection.on_name_told(&session_id, &new_name);
});
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
contact.name = new_name.to_string();
if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().change_contact_name(&contact.uuid, &new_name) {
print_error!(e);
}
} else {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().name = new_name.to_string();
}
}
Err(e) => print_error!(e)
}
}
protocol::Headers::ASK_LARGE_FILES => {
if self.sessions.read().unwrap().get(&session_id).unwrap().files_download.is_none() && !is_sending { //don't accept 2 file transfers at the same time
if let Some(files_info) = protocol::parse_ask_files(&buffer) {
let download_location = UserDirs::new().unwrap().download_dir;
let files: Vec<LargeFileDownload> = files_info.into_iter().map(|info| {
LargeFileDownload {
file_name: info.1,
file_size: info.0,
transferred: 0,
last_chunk: get_unix_timestamp(),
}
}).collect();
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload {
download_location: download_location.clone(),
accepted: false,
index: 0,
files: files.clone(),
});
self.with_ui_connection(|ui_connection| {
ui_connection.on_ask_large_files(&session_id, &files, download_location.to_str().unwrap());
})
}
} else if let Err(e) = session_write.encrypt_and_send(&[protocol::Headers::ABORT_FILES_TRANSFER]).await {
print_error!(e);
break;
}
}
protocol::Headers::LARGE_FILE_CHUNK => {
let mut should_accept_chunk = false;
{
@ -271,7 +280,7 @@ impl SessionManager {
let mut sessions = self.sessions.write().unwrap();
let files_transfer = sessions.get_mut(&session_id).unwrap().files_download.as_mut().unwrap();
let file_transfer = &mut files_transfer.files[files_transfer.index];
file_transfer.last_chunk = get_unix_timestamp();
file_transfer.last_chunk = get_unix_timestamp_ms();
file_transfer.transferred += chunk_size;
if file_transfer.transferred >= file_transfer.file_size { //we downloaded all the file
if files_transfer.index+1 == files_transfer.files.len() {
@ -282,7 +291,7 @@ impl SessionManager {
local_file_handle = None;
}
}
if let Err(e) = session_write.encrypt_and_send(&[protocol::Headers::ACK_CHUNK]).await {
if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ACK_CHUNK]).await {
print_error!(e);
break;
}
@ -297,7 +306,7 @@ impl SessionManager {
if !is_success {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
local_file_handle = None;
if let Err(e) = session_write.encrypt_and_send(&[protocol::Headers::ABORT_FILES_TRANSFER]).await {
if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await {
print_error!(e);
break;
}
@ -329,51 +338,140 @@ impl SessionManager {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
local_file_handle = None;
self.with_ui_connection(|ui_connection| {
ui_connection.on_received(&session_id, &buffer);
ui_connection.on_file_transfer_aborted(&session_id);
});
}
_ => {
let header = buffer[0];
let buffer = match header {
protocol::Headers::FILE => {
if let Some((file_name, content)) = protocol::parse_file(&buffer) {
match self.store_file(&session_id, content) {
Ok(file_uuid) => {
Some([&[protocol::Headers::FILE][..], file_uuid.as_bytes(), file_name].concat())
}
Err(e) => {
protocol::Headers::ASK_LARGE_FILES => {
if self.sessions.read().unwrap().get(&session_id).unwrap().files_download.is_none() && !is_sending { //don't accept 2 file transfers at the same time
if let Some(files_info) = protocol::parse_ask_files(&buffer) {
let download_location = UserDirs::new().unwrap().download_dir;
let files: Vec<LargeFileDownload> = files_info.into_iter().map(|info| {
LargeFileDownload {
file_name: info.1,
file_size: info.0,
transferred: 0,
last_chunk: get_unix_timestamp_ms(),
}
}).collect();
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload {
download_location: download_location.clone(),
accepted: false,
index: 0,
files: files.clone(),
});
self.with_ui_connection(|ui_connection| {
ui_connection.on_ask_large_files(&session_id, &files, download_location.to_str().unwrap());
})
}
} else if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await {
print_error!(e);
break;
}
}
protocol::Headers::ASK_PROFILE_INFO => {
let identity = {
self.identity.read().unwrap().clone()
};
if let Some(identity) = identity { //can be None if we log out just before locking the identity mutex
match self.encrypt_and_send(&mut session_write, &protocol::name(&identity.name)).await {
Ok(_) => {
if let Ok(avatar) = Identity::get_identity_avatar() {
if let Err(e) = self.encrypt_and_send(&mut session_write, &protocol::avatar(&avatar)).await {
print_error!(e);
None
break;
}
}
} else {
None
}
Err(e) => {
print_error!(e);
break;
}
}
_ => {
Some(buffer)
}
}
protocol::Headers::NAME => {
match from_utf8(&buffer[1..]) {
Ok(new_name) => {
let new_name = new_name.replace('\n', " ");
self.with_ui_connection(|ui_connection| {
ui_connection.on_name_told(&session_id, &new_name);
});
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
contact.name = new_name.to_string();
if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().change_contact_name(&contact.uuid, &new_name) {
print_error!(e);
}
} else {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().name = new_name.to_string();
}
}
};
if buffer.is_some() {
let is_classical_message = header == protocol::Headers::MESSAGE || header == protocol::Headers::FILE;
if is_classical_message {
Err(e) => print_error!(e)
}
}
protocol::Headers::AVATAR => {
if buffer.len() < 10000000 {
match image::load_from_memory(&buffer[1..]) {
Ok(image) => {
drop(image);
match self.identity.read().unwrap().as_ref().unwrap().store_avatar(&buffer[1..]) {
Ok(avatar_uuid) => self.set_avatar_uuid(&session_id, Some(avatar_uuid)),
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
}
}
protocol::Headers::REMOVE_AVATAR => self.set_avatar_uuid(&session_id, None),
_ => {
let header = buffer[0];
let timestamp = get_unix_timestamp_sec();
match header {
protocol::Headers::MESSAGE => {
let msg = Message {
outgoing: false,
timestamp,
data: buffer,
};
self.set_seen(session_id, false);
} else if header == protocol::Headers::ACCEPT_LARGE_FILES {
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_msg(&session_id, &msg);
});
self.store_msg(&session_id, msg);
}
protocol::Headers::FILE => {
if let Some((filename, content)) = protocol::parse_file(&buffer) {
match self.store_file(&session_id, content) {
Ok(file_uuid) => {
self.set_seen(session_id, false);
self.with_ui_connection(|ui_connection| {
ui_connection.on_new_file(&session_id, false, timestamp, filename, file_uuid);
});
self.store_msg(&session_id, Message {
outgoing: false,
timestamp,
data: [&[protocol::Headers::FILE][..], file_uuid.as_bytes(), filename.as_bytes()].concat(),
});
}
Err(e) => print_error!(e)
}
}
}
protocol::Headers::ACCEPT_LARGE_FILES => {
is_sending = true;
last_chunks_sizes = Some(Vec::new());
self.with_ui_connection(|ui_connection| {
ui_connection.on_large_files_accepted(&session_id);
})
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_received(&session_id, buffer.as_ref().unwrap());
});
if is_classical_message {
self.store_msg(&session_id, false, buffer.unwrap());
}
_ => {}
}
}
}
}
Err(e) => {
if e != SessionError::BrokenPipe && e != SessionError::ConnectionReset && e != SessionError::BufferTooLarge {
if e != PsecError::BrokenPipe && e != PsecError::ConnectionReset {
print_error!(e);
}
break;
@ -386,26 +484,24 @@ impl SessionManager {
//don't send msg if we already encrypted a file chunk (keep PSEC nonces synchronized)
if is_sending {
msg_queue.push(buff);
} else {
if let Err(e) = self.send_msg(session_id, &mut session_write, &buff, &mut is_sending, &mut file_ack_sender).await {
print_error!(e);
break;
}
} else if let Err(e) = self.send_msg(session_id, &mut session_write, buff, &mut is_sending, &mut file_ack_sender).await {
print_error!(e);
break;
}
}
SessionCommand::EncryptFileChunk { plain_text } => {
last_chunks_sizes.as_mut().unwrap().push(plain_text.len() as u32);
next_chunk = Some(session_write.encrypt(&plain_text));
next_chunk = Some(session_write.encrypt(&plain_text, self.identity.read().unwrap().as_ref().unwrap().use_padding));
}
SessionCommand::SendEncryptedFileChunk { ack_sender } => {
if let Some(chunk) = next_chunk.as_ref() {
match session_write.socket_write(chunk).await {
match session_write.send(chunk).await {
Ok(_) => {
file_ack_sender = Some(ack_sender);
//once the pre-encrypted chunk is sent, we can send the pending messages
while msg_queue.len() > 0 {
while !msg_queue.is_empty() {
let msg = msg_queue.remove(0);
if let Err(e) = self.send_msg(session_id, &mut session_write, &msg, &mut is_sending, &mut file_ack_sender).await {
if let Err(e) = self.send_msg(session_id, &mut session_write, msg, &mut is_sending, &mut file_ack_sender).await {
print_error!(e);
break;
}
@ -426,20 +522,35 @@ impl SessionManager {
}
}
async fn on_session_initialized(&self, session: &mut Session, session_id: usize, is_contact: bool) -> Result<(), PsecError> {
if is_contact {
let pending_msgs = self.pending_msgs.lock().unwrap().get_mut(&session_id).unwrap().split_off(0);
self.with_ui_connection(|ui_connection| {
ui_connection.on_sending_pending_msgs(&session_id);
});
for buff in pending_msgs {
self.send_store_and_inform(session_id, session, buff).await?;
}
self.with_ui_connection(|ui_connection| {
ui_connection.on_pending_msgs_sent(&session_id);
});
Ok(())
} else {
self.encrypt_and_send(session, &protocol::ask_profile_info()).await
}
}
fn handle_new_session(session_manager: Arc<SessionManager>, mut session: Session, outgoing: bool) {
tokio::spawn(async move {
let mut peer_public_key = [0; PUBLIC_KEY_LENGTH];
session.set_max_recv_size(constants::MAX_RECV_SIZE, false);
let session = {
let identity = {
let identity_opt = session_manager.identity.read().unwrap();
match identity_opt.as_ref() {
Some(identity) => Some(identity.clone()),
None => None
}
session_manager.identity.read().unwrap().clone()
};
match identity {
Some(identity) => {
match session.do_handshake(&identity).await {
match session.do_handshake(&identity.keypair).await {
Ok(_) => {
peer_public_key = session.peer_public_key.unwrap();
if identity.get_public_key() != peer_public_key {
@ -458,7 +569,7 @@ impl SessionManager {
}
};
if let Some(mut session) = session {
let ip = session.get_ip();
let ip = session.peer_addr().unwrap().ip();
let mut is_contact = false;
let session_data = {
let mut sessions = session_manager.sessions.write().unwrap();
@ -471,12 +582,13 @@ impl SessionManager {
}
if is_new_session && session_manager.is_identity_loaded() { //check if we didn't logged out during the handshake
let (sender, receiver) = mpsc::channel(32);
let session_data = SessionData{
let session_data = SessionData {
name: ip.to_string(),
avatar: None,
outgoing,
peer_public_key,
ip,
sender: sender,
sender,
files_download: None,
};
let mut session_id = None;
@ -495,7 +607,7 @@ impl SessionManager {
*session_counter += 1;
}
let session_id = session_id.unwrap();
session_manager.saved_msgs.lock().unwrap().insert(session_id, Vec::new());
session_manager.saved_msgs.write().unwrap().insert(session_id, Vec::new());
Some((session_id, receiver))
} else {
None
@ -506,17 +618,10 @@ impl SessionManager {
session_manager.with_ui_connection(|ui_connection| {
ui_connection.on_new_session(&session_id, &ip.to_string(), outgoing, &crypto::generate_fingerprint(&peer_public_key), ip, None);
});
if !is_contact {
match session.encrypt_and_send(&protocol::ask_name()).await {
Ok(_) => {}
Err(e) => {
print_error!(e);
session_manager.remove_session(&session_id);
return;
}
}
match session_manager.on_session_initialized(&mut session, session_id, is_contact).await {
Ok(_) => session_manager.session_worker(session_id, receiver, session).await,
Err(e) => print_error!(e)
}
session_manager.session_worker(session_id, receiver, session).await;
session_manager.remove_session(&session_id);
}
}
@ -541,7 +646,7 @@ impl SessionManager {
client = server_v4.accept() => client,
_ = receiver.recv() => break
}).unwrap();
SessionManager::handle_new_session(session_manager.clone(), Session::new(stream), false);
SessionManager::handle_new_session(session_manager.clone(), Session::from(stream), false);
}
});
Ok(())
@ -551,8 +656,8 @@ impl SessionManager {
self.loaded_contacts.read().unwrap().iter().map(|c| (*c.0, c.1.name.clone(), c.1.verified, c.1.public_key)).collect()
}
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<(bool, Vec<u8>)>> {
self.saved_msgs.lock().unwrap().clone()
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<Message>> {
self.saved_msgs.read().unwrap().clone()
}
pub fn set_seen(&self, session_id: usize, seen: bool) {
@ -564,36 +669,38 @@ impl SessionManager {
}
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
match loaded_contacts.get_mut(&session_id) {
Some(contact) => {
if contact.seen != seen {
match self.identity.read().unwrap().as_ref().unwrap().set_contact_seen(&contact.uuid, seen) {
Ok(_) => contact.seen = seen,
Err(e) => print_error!(e)
}
if let Some(contact) = loaded_contacts.get_mut(&session_id) {
if contact.seen != seen {
match self.identity.read().unwrap().as_ref().unwrap().set_contact_seen(&contact.uuid, seen) {
Ok(_) => contact.seen = seen,
Err(e) => print_error!(e)
}
}
None => {}
}
}
pub fn add_contact(&self, session_id: usize, name: String) -> Result<(), rusqlite::Error> {
let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(name, self.sessions.read().unwrap().get(&session_id).unwrap().peer_public_key)?;
pub fn add_contact(&self, session_id: usize) -> Result<(), rusqlite::Error> {
let sessions = self.sessions.read().unwrap();
let session = sessions.get(&session_id).unwrap();
let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(session.name.clone(), session.avatar, session.peer_public_key)?;
self.loaded_contacts.write().unwrap().insert(session_id, contact);
self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0);
self.pending_msgs.lock().unwrap().insert(session_id, Vec::new());
Ok(())
}
pub fn remove_contact(&self, session_id: usize) -> Result<usize, rusqlite::Error> {
pub fn remove_contact(&self, session_id: &usize) -> Result<usize, rusqlite::Error> {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let result = Identity::remove_contact(&loaded_contacts.get(&session_id).unwrap().uuid);
let result = Identity::remove_contact(&loaded_contacts.get(session_id).unwrap().uuid);
if result.is_ok() {
if let Some(contact) = loaded_contacts.remove(&session_id) {
if let Some(session) = self.sessions.write().unwrap().get_mut(&session_id) {
if let Some(contact) = loaded_contacts.remove(session_id) {
if let Some(session) = self.sessions.write().unwrap().get_mut(session_id) {
session.name = contact.name;
session.avatar = contact.avatar;
}
}
self.last_loaded_msg_offsets.write().unwrap().remove(&session_id);
self.last_loaded_msg_offsets.write().unwrap().remove(session_id);
self.pending_msgs.lock().unwrap().remove(session_id);
}
result
}
@ -612,58 +719,71 @@ impl SessionManager {
let result = Identity::delete_conversation(&self.loaded_contacts.read().unwrap().get(&session_id).unwrap().uuid);
if result.is_ok() {
self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0);
self.saved_msgs.lock().unwrap().insert(session_id, Vec::new());
self.saved_msgs.write().unwrap().insert(session_id, Vec::new());
}
result
}
pub fn is_contact(&self, session_id: &usize) -> bool {
self.loaded_contacts.read().unwrap().contains_key(session_id)
}
pub fn load_file(&self, uuid: Uuid) -> Option<Vec<u8>> {
self.identity.read().unwrap().as_ref().unwrap().load_file(uuid)
}
pub fn store_file(&self, session_id: &usize, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
self.identity.read().unwrap().as_ref().unwrap().store_file(match self.loaded_contacts.read().unwrap().get(session_id) {
Some(contact) => Some(contact.uuid),
None => None
}, data)
self.identity.read().unwrap().as_ref().unwrap().store_file(
self.loaded_contacts.read().unwrap().get(session_id).map(|contact| contact.uuid),
data
)
}
pub fn load_msgs(&self, session_id: &usize, count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
pub fn load_msgs(&self, session_id: &usize, count: usize) -> Option<Vec<Message>> {
let mut offsets = self.last_loaded_msg_offsets.write().unwrap();
let msgs = self.identity.read().unwrap().as_ref().unwrap().load_msgs(&self.loaded_contacts.read().unwrap().get(session_id).unwrap().uuid, *offsets.get(session_id).unwrap(), count);
if msgs.is_some() {
*offsets.get_mut(session_id).unwrap() += msgs.as_ref().unwrap().len();
let msgs = self.identity.read().unwrap().as_ref().unwrap().load_msgs(
&self.loaded_contacts.read().unwrap().get(session_id)?.uuid,
*offsets.get(session_id)?,
count
);
if let Some(msgs) = msgs.as_ref() {
*offsets.get_mut(session_id)? += msgs.len();
}
msgs
}
pub fn get_my_public_key(&self) -> [u8; PUBLIC_KEY_LENGTH] {
self.identity.read().unwrap().as_ref().unwrap().get_public_key()
}
pub fn get_my_name(&self) -> String {
self.identity.read().unwrap().as_ref().unwrap().name.clone()
pub fn get_avatar(&self, session_id: &usize) -> Option<Vec<u8>> {
let avatar_uuid = match self.loaded_contacts.read().unwrap().get(session_id) {
Some(contact) => contact.avatar?,
None => self.sessions.read().unwrap().get(session_id)?.avatar?
};
self.identity.read().unwrap().as_ref().unwrap().get_avatar(&avatar_uuid)
}
#[allow(unused_must_use)]
pub async fn change_name(&self, new_name: String) -> Result<usize, rusqlite::Error> {
let telling_name = protocol::tell_name(&new_name);
let result = self.identity.write().unwrap().as_mut().unwrap().change_name(new_name);
if result.is_ok() {
let senders: Vec<Sender<SessionCommand>> = {
self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect()
};
for sender in senders.into_iter() {
sender.send(SessionCommand::Send {
buff: telling_name.clone()
}).await;
}
pub async fn send_avatar(&self, avatar: &[u8]) {
let avatar_msg = protocol::avatar(&avatar);
for sender in self.get_all_senders().into_iter() {
sender.send(SessionCommand::Send {
buff: avatar_msg.clone()
}).await;
}
result
}
#[allow(unused_must_use)]
pub async fn remove_avatar(&self) -> Result<(), rusqlite::Error> {
Identity::remove_identity_avatar()?;
let avatar_msg = protocol::remove_avatar();
for sender in self.get_all_senders().into_iter() {
sender.send(SessionCommand::Send {
buff: avatar_msg.clone()
}).await;
}
Ok(())
}
#[allow(unused_must_use)]
pub async fn change_name(&self, new_name: String) -> Result<(), rusqlite::Error> {
let telling_name = protocol::name(&new_name);
self.identity.write().unwrap().as_mut().unwrap().change_name(new_name)?;
for sender in self.get_all_senders().into_iter() {
sender.send(SessionCommand::Send {
buff: telling_name.clone()
}).await;
}
Ok(())
}
#[allow(unused_must_use)]
@ -681,7 +801,7 @@ impl SessionManager {
*self.ui_connection.lock().unwrap() = None;
*self.session_counter.write().unwrap() = 0;
self.loaded_contacts.write().unwrap().clear();
self.saved_msgs.lock().unwrap().clear();
self.saved_msgs.write().unwrap().clear();
}
pub fn is_identity_loaded(&self) -> bool {
@ -695,20 +815,18 @@ impl SessionManager {
}
*identity_guard = identity;
if identity_guard.is_some() { //login
match identity_guard.as_ref().unwrap().load_contacts() {
Some(contacts) => {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let mut session_counter = self.session_counter.write().unwrap();
let mut not_seen = self.not_seen.write().unwrap();
contacts.into_iter().for_each(|contact| {
if !contact.seen {
not_seen.push(*session_counter);
}
loaded_contacts.insert(*session_counter, contact);
*session_counter += 1;
})
}
None => {}
if let Some(contacts) = identity_guard.as_ref().unwrap().load_contacts() {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let mut session_counter = self.session_counter.write().unwrap();
let mut not_seen = self.not_seen.write().unwrap();
contacts.into_iter().for_each(|contact| {
if !contact.seen {
not_seen.push(*session_counter);
}
loaded_contacts.insert(*session_counter, contact);
self.pending_msgs.lock().unwrap().insert(*session_counter, Vec::new());
*session_counter += 1;
});
}
}
}
@ -725,7 +843,8 @@ impl SessionManager {
ui_connection: Mutex::new(None),
loaded_contacts: RwLock::new(HashMap::new()),
last_loaded_msg_offsets: RwLock::new(HashMap::new()),
saved_msgs: Mutex::new(HashMap::new()),
saved_msgs: RwLock::new(HashMap::new()),
pending_msgs: Mutex::new(HashMap::new()),
not_seen: RwLock::new(Vec::new()),
mdns_service: Mutex::new(None),
listener_stop_signal: Mutex::new(None),

View File

@ -1,320 +0,0 @@
use std::{convert::TryInto, io::ErrorKind, net::IpAddr};
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpStream, tcp::{OwnedReadHalf, OwnedWriteHalf}}};
use ed25519_dalek;
use ed25519_dalek::{ed25519::signature::Signature, Verifier, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH};
use x25519_dalek;
use rand_7::{RngCore, rngs::OsRng};
use sha2::{Sha384, Digest};
use aes_gcm::{Aes128Gcm, aead::Aead, NewAead, aead::Payload, Nonce};
use crate::utils::*;
use crate::crypto::*;
use crate::identity::Identity;
use crate::session_manager::SessionError;
use crate::print_error;
const RANDOM_LEN: usize = 64;
const MESSAGE_LEN_LEN: usize = 4;
type MessageLenType = u32;
async fn socket_read<T: AsyncReadExt + Unpin>(reader: &mut T, buff: &mut [u8]) -> Result<usize, SessionError> {
match reader.read(buff).await {
Ok(read) => {
if read > 0 {
Ok(read)
} else {
Err(SessionError::BrokenPipe)
}
}
Err(e) => {
match e.kind() {
ErrorKind::ConnectionReset => Err(SessionError::ConnectionReset),
_ => {
print_error!("Receive error ({:?}): {}", e.kind(), e);
Err(SessionError::Unknown)
}
}
}
}
}
async fn socket_write<T: AsyncWriteExt + Unpin>(writer: &mut T, buff: &[u8]) -> Result<(), SessionError> {
match writer.write_all(buff).await {
Ok(_) => Ok(()),
Err(e) => Err(match e.kind() {
ErrorKind::BrokenPipe => SessionError::BrokenPipe,
ErrorKind::ConnectionReset => SessionError::ConnectionReset,
_ => {
print_error!("Send error ({:?}): {}", e.kind(), e);
SessionError::Unknown
}
})
}
}
fn pad(plain_text: &[u8]) -> Vec<u8> {
let encoded_msg_len = (plain_text.len() as MessageLenType).to_be_bytes();
let msg_len = plain_text.len()+encoded_msg_len.len();
let mut len = 1000;
while len < msg_len {
len *= 2;
}
let mut output = Vec::from(encoded_msg_len);
output.reserve(len);
output.extend(plain_text);
output.resize(len, 0);
OsRng.fill_bytes(&mut output[msg_len..]);
output
}
fn unpad(input: Vec<u8>) -> Vec<u8> {
let msg_len = MessageLenType::from_be_bytes(input[0..MESSAGE_LEN_LEN].try_into().unwrap()) as usize;
Vec::from(&input[MESSAGE_LEN_LEN..MESSAGE_LEN_LEN+msg_len])
}
fn encrypt(local_cipher: &Aes128Gcm, local_iv: &[u8], local_counter: &mut usize, plain_text: &[u8]) -> Vec<u8> {
let padded_msg = pad(plain_text);
let cipher_len = (padded_msg.len() as MessageLenType).to_be_bytes();
let payload = Payload {
msg: &padded_msg,
aad: &cipher_len
};
let nonce = iv_to_nonce(local_iv, local_counter);
let cipher_text = local_cipher.encrypt(Nonce::from_slice(&nonce), payload).unwrap();
[&cipher_len, cipher_text.as_slice()].concat()
}
pub async fn encrypt_and_send<T: AsyncWriteExt + Unpin>(writer: &mut T, local_cipher: &Aes128Gcm, local_iv: &[u8], local_counter: &mut usize, plain_text: &[u8]) -> Result<(), SessionError> {
let cipher_text = encrypt(local_cipher, local_iv, local_counter, plain_text);
socket_write(writer, &cipher_text).await
}
pub struct SessionRead {
read_half: OwnedReadHalf,
peer_cipher: Aes128Gcm,
peer_iv: [u8; IV_LEN],
peer_counter: usize,
}
impl SessionRead {
async fn socket_read(&mut self, buff: &mut [u8]) -> Result<usize, SessionError> {
socket_read(&mut self.read_half, buff).await
}
pub async fn receive_and_decrypt(mut self) -> Result<(SessionRead, Vec<u8>), SessionError> {
let mut message_len = [0; MESSAGE_LEN_LEN];
self.socket_read(&mut message_len).await?;
let recv_len = MessageLenType::from_be_bytes(message_len) as usize + AES_TAG_LEN;
if recv_len <= Session::MAX_RECV_SIZE {
let mut cipher_text = vec![0; recv_len];
let mut read = 0;
while read < recv_len {
read += self.socket_read(&mut cipher_text[read..]).await?;
}
let peer_nonce = iv_to_nonce(&self.peer_iv, &mut self.peer_counter);
let payload = Payload {
msg: &cipher_text,
aad: &message_len
};
match self.peer_cipher.decrypt(Nonce::from_slice(&peer_nonce), payload) {
Ok(plain_text) => Ok((self, unpad(plain_text))),
Err(_) => Err(SessionError::TransmissionCorrupted)
}
} else {
print_error!("Buffer too large: {} B", recv_len);
Err(SessionError::BufferTooLarge)
}
}
}
pub struct SessionWrite {
write_half: OwnedWriteHalf,
local_cipher: Aes128Gcm,
local_iv: [u8; IV_LEN],
local_counter: usize,
}
impl SessionWrite {
pub async fn encrypt_and_send(&mut self, plain_text: &[u8]) -> Result<(), SessionError> {
encrypt_and_send(&mut self.write_half, &self.local_cipher, &self.local_iv, &mut self.local_counter, plain_text).await
}
pub fn encrypt(&mut self, plain_text: &[u8]) -> Vec<u8> {
encrypt(&self.local_cipher, &self.local_iv, &mut self.local_counter, plain_text)
}
pub async fn socket_write(&mut self, cipher_text: &[u8]) -> Result<(), SessionError> {
socket_write(&mut self.write_half, cipher_text).await
}
}
pub struct Session {
stream: TcpStream,
handshake_sent_buff: Vec<u8>,
handshake_recv_buff: Vec<u8>,
local_cipher: Option<Aes128Gcm>,
local_iv: Option<[u8; IV_LEN]>,
local_counter: usize,
peer_cipher: Option<Aes128Gcm>,
peer_iv: Option<[u8; IV_LEN]>,
peer_counter: usize,
pub peer_public_key: Option<[u8; PUBLIC_KEY_LENGTH]>,
}
impl Session {
const PADDED_MAX_SIZE: usize = 32768000;
const MAX_RECV_SIZE: usize = MESSAGE_LEN_LEN + Session::PADDED_MAX_SIZE + AES_TAG_LEN;
pub fn new(stream: TcpStream) -> Session {
Session {
stream: stream,
handshake_sent_buff: Vec::new(),
handshake_recv_buff: Vec::new(),
local_cipher: None,
local_iv: None,
local_counter: 0,
peer_cipher: None,
peer_iv: None,
peer_counter: 0,
peer_public_key: None,
}
}
pub fn into_spit(self) -> Option<(SessionRead, SessionWrite)> {
let (read_half, write_half) = self.stream.into_split();
Some((
SessionRead {
read_half,
peer_cipher: self.peer_cipher?,
peer_iv: self.peer_iv?,
peer_counter: self.peer_counter,
},
SessionWrite {
write_half,
local_cipher: self.local_cipher?,
local_iv: self.local_iv?,
local_counter: self.local_counter,
}
))
}
pub fn get_ip(&self) -> IpAddr {
self.stream.peer_addr().unwrap().ip()
}
async fn socket_read(&mut self, buff: &mut [u8]) -> Result<usize, SessionError> {
socket_read(&mut self.stream, buff).await
}
pub async fn socket_write(&mut self, buff: &[u8]) -> Result<(), SessionError> {
socket_write(&mut self.stream, buff).await
}
async fn handshake_read(&mut self, buff: &mut [u8]) -> Result<(), SessionError> {
self.socket_read(buff).await?;
self.handshake_recv_buff.extend(buff.as_ref());
Ok(())
}
async fn handshake_write(&mut self, buff: &[u8]) -> Result<(), SessionError> {
self.socket_write(buff).await?;
self.handshake_sent_buff.extend(buff);
Ok(())
}
fn hash_handshake(&self, i_am_bob: bool) -> [u8; 48] {
let handshake_bytes = if i_am_bob {
[self.handshake_sent_buff.as_slice(), self.handshake_recv_buff.as_slice()].concat()
} else {
[self.handshake_recv_buff.as_slice(), self.handshake_sent_buff.as_slice()].concat()
};
let mut hasher = Sha384::new();
hasher.update(handshake_bytes);
let handshake_hash = hasher.finalize();
to_array_48(handshake_hash.as_slice())
}
fn on_handshake_successful(&mut self, application_keys: ApplicationKeys){
self.local_cipher = Some(Aes128Gcm::new_from_slice(&application_keys.local_key).unwrap());
self.local_iv = Some(application_keys.local_iv);
self.peer_cipher = Some(Aes128Gcm::new_from_slice(&application_keys.peer_key).unwrap());
self.peer_iv = Some(application_keys.peer_iv);
self.handshake_sent_buff.clear();
self.handshake_sent_buff.shrink_to_fit();
self.handshake_recv_buff.clear();
self.handshake_recv_buff.shrink_to_fit();
}
pub async fn do_handshake(&mut self, identity: &Identity) -> Result<(), SessionError> {
//ECDHE initial exchange
//generate random bytes
let mut handshake_buffer = [0; RANDOM_LEN+PUBLIC_KEY_LENGTH];
OsRng.fill_bytes(&mut handshake_buffer[..RANDOM_LEN]);
//generate ephemeral x25519 keys
let ephemeral_secret = x25519_dalek::EphemeralSecret::new(OsRng);
let ephemeral_public_key = x25519_dalek::PublicKey::from(&ephemeral_secret);
handshake_buffer[RANDOM_LEN..].copy_from_slice(&ephemeral_public_key.to_bytes());
self.handshake_write(&handshake_buffer).await?;
self.handshake_read(&mut handshake_buffer).await?;
let peer_ephemeral_public_key = x25519_dalek::PublicKey::from(to_array_32(&handshake_buffer[RANDOM_LEN..]));
//calc handshake keys
let i_am_bob = self.handshake_sent_buff < self.handshake_recv_buff; //mutual consensus for keys attribution
let handshake_hash = self.hash_handshake(i_am_bob);
let shared_secret = ephemeral_secret.diffie_hellman(&peer_ephemeral_public_key);
let handshake_keys = HandshakeKeys::derive_keys(shared_secret.to_bytes(), handshake_hash, i_am_bob);
//encrypted handshake
//generate random bytes
let mut random_bytes = [0; RANDOM_LEN];
OsRng.fill_bytes(&mut random_bytes);
self.handshake_write(&random_bytes).await?;
drop(random_bytes);
//receive peer random bytes
let mut peer_random = [0; RANDOM_LEN];
self.handshake_read(&mut peer_random).await?;
drop(peer_random);
//get public key & sign our ephemeral public key
let mut auth_msg = [0; PUBLIC_KEY_LENGTH+SIGNATURE_LENGTH];
auth_msg[..PUBLIC_KEY_LENGTH].copy_from_slice(&identity.get_public_key());
auth_msg[PUBLIC_KEY_LENGTH..].copy_from_slice(&identity.sign(ephemeral_public_key.as_bytes()));
//encrypt auth_msg
let local_cipher = Aes128Gcm::new_from_slice(&handshake_keys.local_key).unwrap();
let mut local_handshake_counter = 0;
let nonce = iv_to_nonce(&handshake_keys.local_iv, &mut local_handshake_counter);
let encrypted_auth_msg = local_cipher.encrypt(Nonce::from_slice(&nonce), auth_msg.as_ref()).unwrap();
self.handshake_write(&encrypted_auth_msg).await?;
let mut encrypted_peer_auth_msg = [0; PUBLIC_KEY_LENGTH+SIGNATURE_LENGTH+AES_TAG_LEN];
self.handshake_read(&mut encrypted_peer_auth_msg).await?;
//decrypt peer_auth_msg
let peer_cipher = Aes128Gcm::new_from_slice(&handshake_keys.peer_key).unwrap();
let mut peer_handshake_counter = 0;
let peer_nonce = iv_to_nonce(&handshake_keys.peer_iv, &mut peer_handshake_counter);
match peer_cipher.decrypt(Nonce::from_slice(&peer_nonce), encrypted_peer_auth_msg.as_ref()) {
Ok(peer_auth_msg) => {
//verify ephemeral public key signature
self.peer_public_key = Some(to_array_32(&peer_auth_msg[..PUBLIC_KEY_LENGTH]));
let peer_public_key = ed25519_dalek::PublicKey::from_bytes(&self.peer_public_key.unwrap()).unwrap();
let peer_signature = Signature::from_bytes(&peer_auth_msg[PUBLIC_KEY_LENGTH..]).unwrap();
if peer_public_key.verify(peer_ephemeral_public_key.as_bytes(), &peer_signature).is_ok() {
let handshake_hash = self.hash_handshake(i_am_bob);
//sending handshake finished
let handshake_finished = compute_handshake_finished(handshake_keys.local_handshake_traffic_secret, handshake_hash);
self.socket_write(&handshake_finished).await?;
let mut peer_handshake_finished = [0; HASH_OUTPUT_LEN];
self.socket_read(&mut peer_handshake_finished).await?;
if verify_handshake_finished(peer_handshake_finished, handshake_keys.peer_handshake_traffic_secret, handshake_hash) {
//calc application keys
let application_keys = ApplicationKeys::derive_keys(handshake_keys.handshake_secret, handshake_hash, i_am_bob);
self.on_handshake_successful(application_keys);
return Ok(());
}
}
}
Err(_) => {}
}
Err(SessionError::TransmissionCorrupted)
}
pub async fn encrypt_and_send(&mut self, plain_text: &[u8]) -> Result<(), SessionError> {
encrypt_and_send(&mut self.stream, self.local_cipher.as_ref().unwrap(), self.local_iv.as_ref().unwrap(), &mut self.local_counter, plain_text).await
}
}

View File

@ -1,129 +1,7 @@
use std::net::{IpAddr, TcpStream};
use std::{fmt::Display, net::{IpAddr, TcpStream}, str::from_utf8};
use tungstenite::{WebSocket, protocol::Role, Message};
use crate::{protocol, session_manager::{LargeFileDownload, LargeFilesDownload}};
mod ui_messages {
use std::{fmt::Display, iter::FromIterator, net::IpAddr, str::from_utf8};
use tungstenite::Message;
use uuid::Uuid;
use crate::{print_error, session_manager::{LargeFileDownload, LargeFilesDownload, protocol}, utils::to_uuid_bytes};
const ON_NEW_MESSAGE: &str = "new_message";
const LOAD_SENT_MESSAGE: &str = "load_sent_msg";
fn new_message(command: &str, session_id: &usize, outgoing: bool, raw_message: &[u8]) -> Option<Message> {
match from_utf8(raw_message) {
Ok(msg) => Some(Message::from(format!("{} {} {} {}", command, session_id, outgoing, msg))),
Err(e) => {
print_error!(e);
None
}
}
}
fn simple_event(command: &str, session_id: &usize) -> Message {
Message::from(format!("{} {}", command, session_id))
}
fn data_list<T: Display>(command: &str, data: Vec<T>) -> Message {
Message::from(command.to_owned()+&String::from_iter(data.into_iter().map(|i| {
format!(" {}", i)
})))
}
pub fn on_disconnected(session_id: &usize) -> Message {
simple_event("disconnected", session_id)
}
pub fn on_new_session(session_id: &usize, name: &str, outgoing: bool, fingerprint: &str, ip: IpAddr) -> Message {
Message::from(format!("new_session {} {} {} {} {}", session_id, outgoing, fingerprint, ip, name))
}
pub fn on_file_received(session_id: &usize, buffer: &[u8]) -> Option<Message> {
let uuid = Uuid::from_bytes(to_uuid_bytes(&buffer[1..17])?);
match from_utf8(&buffer[17..]) {
Ok(file_name) => Some(Message::from(format!("file {} {} {}", session_id, uuid.to_string(), file_name))),
Err(e) => {
print_error!(e);
None
}
}
}
pub fn new_files_transfer(session_id: &usize, files_transfer: &LargeFilesDownload) -> Message {
if files_transfer.accepted {
let mut s = format!(
"files_transfer {} {}",
session_id,
files_transfer.index
);
files_transfer.files.iter().for_each(|file| {
s.push_str(&format!(
" {} {} {} {}",
base64::encode(&file.file_name),
file.file_size,
file.transferred,
file.last_chunk,
));
});
Message::from(s)
} else {
on_ask_large_files(session_id, &files_transfer.files, files_transfer.download_location.to_str().unwrap())
}
}
pub fn on_ask_large_files(session_id: &usize, files: &Vec<LargeFileDownload>, download_location: &str) -> Message {
let mut s = format!("ask_large_files {} {}", session_id, base64::encode(download_location));
files.into_iter().for_each(|file| {
s.push_str(&format!(
" {} {}",
base64::encode(&file.file_name),
file.file_size,
));
});
Message::from(s)
}
pub fn on_large_files_accepted(session_id: &usize) -> Message {
simple_event("files_accepted", session_id)
}
pub fn on_file_transfer_aborted(session_id: &usize) -> Message {
simple_event("aborted", session_id)
}
pub fn on_new_message(session_id: &usize, outgoing: bool, buffer: &[u8]) -> Option<Message> {
new_message(ON_NEW_MESSAGE, session_id, outgoing, &buffer[1..])
}
pub fn inc_files_transfer(session_id: &usize, chunk_size: u64) -> Message {
Message::from(format!("inc_file_transfer {} {}", session_id, chunk_size))
}
pub fn load_msg(session_id: &usize, outgoing: bool, buffer: &[u8]) -> Option<Message> {
match buffer[0] {
protocol::Headers::MESSAGE => new_message(LOAD_SENT_MESSAGE, session_id, outgoing, &buffer[1..]),
protocol::Headers::FILE => {
let uuid = Uuid::from_bytes(to_uuid_bytes(&buffer[1..17])?);
match from_utf8(&buffer[17..]) {
Ok(file_name) => Some(Message::from(format!("load_sent_file {} {} {} {}", session_id, outgoing, uuid.to_string(), file_name))),
Err(e) => {
print_error!(e);
None
}
}
}
_ => None
}
}
pub fn set_not_seen(session_ids: Vec<usize>) -> Message {
data_list("not_seen", session_ids)
}
pub fn set_local_ips(ips: Vec<IpAddr>) -> Message {
data_list("local_ips", ips)
}
pub fn on_name_told(session_id: &usize, name: &str) -> Message {
Message::from(format!("name_told {} {}", session_id, name))
}
pub fn set_as_contact(session_id: usize, name: &str, verified: bool, fingerprint: &str) -> Message {
Message::from(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name))
}
pub fn set_name(new_name: &str) -> Message {
Message::from(format!("set_name {}", new_name))
}
pub fn password_changed(success: bool, is_protected: bool) -> Message {
Message::from(format!("password_changed {} {}", success, is_protected))
}
}
use uuid::Uuid;
use crate::{identity, print_error, protocol, session_manager::{LargeFileDownload, LargeFilesDownload}, utils::to_uuid_bytes};
pub struct UiConnection{
pub websocket: WebSocket<TcpStream>,
@ -133,83 +11,138 @@ pub struct UiConnection{
impl UiConnection {
pub fn new(websocket: WebSocket<TcpStream>) -> UiConnection {
UiConnection {
websocket: websocket,
websocket,
is_valid: true
}
}
pub fn write_message(&mut self, message: Message) {
if self.websocket.write_message(message).is_err() {
pub fn write_message<T: Into<Message>>(&mut self, message: T) {
if self.websocket.write_message(message.into()).is_err() {
self.is_valid = false
}
}
pub fn on_received(&mut self, session_id: &usize, buffer: &[u8]) {
let ui_message = match buffer[0] {
protocol::Headers::MESSAGE => ui_messages::on_new_message(session_id, false, buffer),
protocol::Headers::FILE => ui_messages::on_file_received(session_id, buffer),
protocol::Headers::ACCEPT_LARGE_FILES => Some(ui_messages::on_large_files_accepted(session_id)),
protocol::Headers::ABORT_FILES_TRANSFER => Some(ui_messages::on_file_transfer_aborted(session_id)),
_ => None
};
if ui_message.is_some() {
self.write_message(ui_message.unwrap())
fn simple_event(&mut self, command: &str, session_id: &usize) {
self.write_message(format!("{} {}", command, session_id));
}
fn data_list<T: Display>(command: &str, data: &[T]) -> String {
command.to_string()+&data.iter().map(|i| {
format!(" {}", i)
}).collect::<String>()
}
pub fn on_ask_large_files(&mut self, session_id: &usize, files: &[LargeFileDownload], download_location: &str) {
let mut s = format!("ask_large_files {} {}", session_id, base64::encode(download_location));
files.iter().for_each(|file| {
s.push_str(&format!(
" {} {}",
base64::encode(&file.file_name),
file.file_size,
));
});
self.write_message(s);
}
pub fn on_large_files_accepted(&mut self, session_id: &usize) {
self.simple_event("files_accepted", session_id);
}
pub fn on_file_transfer_aborted(&mut self, session_id: &usize) {
self.simple_event("aborted", session_id);
}
pub fn on_new_msg(&mut self, session_id: &usize, message: &identity::Message) {
match from_utf8(&message.data[1..]) {
Ok(msg) => self.write_message(format!("new_message {} {} {} {}", session_id, message.outgoing, message.timestamp, msg)),
Err(e) => print_error!(e)
}
}
pub fn on_ask_large_files(&mut self, session_id: &usize, files: &Vec<LargeFileDownload>, download_location: &str) {
self.write_message(ui_messages::on_ask_large_files(session_id, files, download_location))
}
pub fn on_msg_sent(&mut self, session_id: usize, buffer: &[u8]) {
match buffer[0] {
protocol::Headers::MESSAGE => match ui_messages::on_new_message(&session_id, true, buffer) {
Some(msg) => self.write_message(msg),
None => {}
}
protocol::Headers::ABORT_FILES_TRANSFER => self.write_message(ui_messages::on_file_transfer_aborted(&session_id)),
_ => {}
}
pub fn on_new_file(&mut self, session_id: &usize, outgoing: bool, timestamp: u64, filename: &str, uuid: Uuid) {
self.write_message(format!("file {} {} {} {} {}", session_id, outgoing, timestamp, uuid.to_string(), filename));
}
pub fn on_new_session(&mut self, session_id: &usize, name: &str, outgoing: bool, fingerprint: &str, ip: IpAddr, files_transfer: Option<&LargeFilesDownload>) {
self.write_message(ui_messages::on_new_session(session_id, name, outgoing, fingerprint, ip));
self.write_message(format!("new_session {} {} {} {} {}", session_id, outgoing, fingerprint, ip, name));
if let Some(files_transfer) = files_transfer {
self.write_message(ui_messages::new_files_transfer(session_id, files_transfer));
if files_transfer.accepted {
let mut s = format!(
"files_transfer {} {}",
session_id,
files_transfer.index
);
files_transfer.files.iter().for_each(|file| {
s.push_str(&format!(
" {} {} {} {}",
base64::encode(&file.file_name),
file.file_size,
file.transferred,
file.last_chunk,
));
});
self.write_message(s);
} else {
self.on_ask_large_files(session_id, &files_transfer.files, files_transfer.download_location.to_str().unwrap())
}
}
}
pub fn on_disconnected(&mut self, session_id: &usize) {
self.write_message(ui_messages::on_disconnected(session_id));
self.simple_event("disconnected", session_id);
}
pub fn on_name_told(&mut self, session_id: &usize, name: &str) {
self.write_message(ui_messages::on_name_told(session_id, name));
self.write_message(format!("name_told {} {}", session_id, name));
}
pub fn on_avatar_changed(&mut self, session_id: Option<&usize>) {
match session_id {
Some(session_id) => self.simple_event("avatar_changed", session_id),
None => self.write_message("avatar_changed self")
}
}
pub fn inc_files_transfer(&mut self, session_id: &usize, chunk_size: u64) {
self.write_message(ui_messages::inc_files_transfer(session_id, chunk_size));
self.write_message(format!("inc_file_transfer {} {}", session_id, chunk_size));
}
pub fn set_as_contact(&mut self, session_id: usize, name: &str, verified: bool, fingerprint: &str) {
self.write_message(ui_messages::set_as_contact(session_id, name, verified, fingerprint));
self.write_message(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name));
}
pub fn load_msgs(&mut self, session_id: &usize, msgs: &Vec<(bool, Vec<u8>)>) {
msgs.into_iter().rev().for_each(|msg| {
match ui_messages::load_msg(session_id, msg.0, &msg.1) {
Some(msg) => self.write_message(msg),
None => {}
pub fn load_msgs(&mut self, session_id: &usize, msgs: &[identity::Message]) {
let mut s = format!("load_msgs {}", session_id);
msgs.iter().rev().for_each(|message| {
match message.data[0] {
protocol::Headers::MESSAGE => match from_utf8(&message.data[1..]) {
Ok(msg) => s.push_str(&format!(" m {} {} {}", message.outgoing, message.timestamp, base64::encode(msg))),
Err(e) => print_error!(e)
}
protocol::Headers::FILE => {
let uuid = Uuid::from_bytes(to_uuid_bytes(&message.data[1..17]).unwrap());
match from_utf8(&message.data[17..]) {
Ok(file_name) => s.push_str(&format!(" f {} {} {} {}", message.outgoing, message.timestamp, uuid.to_string(), base64::encode(file_name))),
Err(e) => print_error!(e)
}
}
_ => {}
}
})
});
self.write_message(s);
}
pub fn set_not_seen(&mut self, session_ids: Vec<usize>) {
self.write_message(ui_messages::set_not_seen(session_ids));
pub fn set_not_seen(&mut self, session_ids: &[usize]) {
self.write_message(Self::data_list("not_seen", session_ids));
}
pub fn set_local_ips(&mut self, ips: Vec<IpAddr>) {
self.write_message(ui_messages::set_local_ips(ips));
pub fn new_pending_msg(&mut self, session_id: &usize, is_file: bool, data: &str) {
self.write_message(format!("pending {} {} {}", session_id, is_file, data));
}
pub fn on_sending_pending_msgs(&mut self, session_id: &usize) {
self.simple_event("sending_pending_msgs", session_id);
}
pub fn on_pending_msgs_sent(&mut self, session_id: &usize) {
self.simple_event("pending_msgs_sent", session_id);
}
pub fn set_local_ips(&mut self, ips: &[IpAddr]) {
self.write_message(Self::data_list("local_ips", ips));
}
pub fn set_name(&mut self, new_name: &str) {
self.write_message(ui_messages::set_name(new_name));
self.write_message(format!("set_name {}", new_name));
}
pub fn password_changed(&mut self, success: bool, is_protected: bool) {
self.write_message(ui_messages::password_changed(success, is_protected));
self.write_message(format!("password_changed {} {}", success, is_protected));
}
pub fn logout(&mut self) {
self.write_message(Message::from("logout"));
self.write_message("logout");
}
}

View File

@ -1,15 +1,7 @@
use std::{convert::TryInto, time::{SystemTime, UNIX_EPOCH}, path::PathBuf};
use std::{convert::TryInto, time::{SystemTime, UNIX_EPOCH}, path::Path};
use uuid::Bytes;
use crate::print_error;
pub fn to_array_48(s: &[u8]) -> [u8; 48] {
s.try_into().unwrap()
}
pub fn to_array_32(s: &[u8]) -> [u8; 32] {
s.try_into().unwrap()
}
pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
match bytes.try_into() {
Ok(uuid) => Some(uuid),
@ -24,11 +16,15 @@ pub fn escape_double_quote(origin: String) -> String {
origin.replace("\"", "\\\"")
}
pub fn get_unix_timestamp() -> u128 {
pub fn get_unix_timestamp_ms() -> u128 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis()
}
pub fn get_not_used_path(file_name: &str, parent_directory: &PathBuf) -> String {
pub fn get_unix_timestamp_sec() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
}
pub fn get_not_used_path(file_name: &str, parent_directory: &Path) -> String {
let has_extension = file_name.matches('.').count() > 0;
let mut path = parent_directory.join(&file_name);
let mut n = 1;