Compare commits

..

No commits in common. "multi-identity" and "master" have entirely different histories.

39 changed files with 9560 additions and 2542 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,36 +1,49 @@
[package]
name = "aira"
version = "0.0.1"
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"}
socket2 = "0.3.19"
ed25519-dalek = "1" #for singing
x25519-dalek = "1.1" #for shared secret
sha2 = "0.9.3"
hkdf = "0.10.0"
aes-gcm = "0.8.0"
hmac = "0.10.1"
hex = "0.4.3"
strum_macros = "0.20.1" #display enums
actix-web = "3"
tungstenite = "0.13.0" #websocket
serde = "1.0.124" #serialization
html-escape = "0.2.7"
dirs = "3.0"
uuid = { version = "0.8", features = ["v4"] }
webbrowser = "0.5.5"
astro-dnssd = "0.2.0" #mDNS advertiser
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"
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.17" #websocket
serde = { version = "1.0", features = ["derive"] } #serialization
html-escape = "0.2"
sanitize-filename = "0.3"
platform-dirs = "0.3"
uuid = { version = "1.0", features = ["v4"] }
webbrowser = "0.7"
libmdns = "0.6" #mDNS advertiser
multicast_dns = "0.5" #mDNS browser
base64 = "0.13.0"
time = "0.2.25"
aes-gcm-siv = "0.9.0"
scrypt = "0.6.3"
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
[dependencies.rusqlite]
version = "0.24.2"
features = ["bundled"]
[build-dependencies]
html-minifier = "3.0"
yaml-rust = "0.4"
linked-hash-map = "0.5"
[profile.dev.package.scrypt]
opt-level = 3

View File

@ -1,42 +1,65 @@
# 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 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.
<img src="https://forge.chapril.org/hardcoresushi/AIRA/raw/branch/master/screenshot.png">
![Screenshot of a conversation between Alice and Bob on AIRA](https://forge.chapril.org/hardcoresushi/AIRA/raw/branch/master/screenshot.png)
# Motivations
# 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.
Likewise, when people are lazy, they talk sometimes to their friends in the next room using centralized mobiles apps like Whatsapp. Their messages can be exported thousands of kilometers away to reach Facebook's servers where they are analysed with the aim of offering personalized ads and selling your data before being sent on the same way back to your friend's smartphone.
All this practices generate useless traffic, overload servers, often breach privacy but above all pollute a lot. This is why I decided to build a more ecological and private way to communicate with near devices. There are many awesome P2P projects built on top of the Internet, but none of them provide local-network communications that work even totally disconnected from the rest of the world. AIRA is the first to provide this capability.
All this practices generate useless traffic, overload servers, often breach privacy but above all pollute a lot. This is why I decided to build a more ecological and private way to communicate with near devices.
# Similar works
There are already some great projects that allow offline P2P communications, but they require that the peer you want to communicate with be a known contact, usually by first adding their public key to your contact list.
- [Briar](https://briarproject.org)
- [berty](https://berty.tech)
- [BeeBEEP](https://www.beebeep.net)
- Add your own !
# Disclaimer
AIRA is still under developement and is not ready for production usage yet. Not all features have been implemented and bugs are expected. Neither the code or the PSEC protocol received any security audit and therefore shouldn't be considered fully secure. AIRA is provided "as is", without any warranty of any kind.
# Features
- IPv4/v6 compatibility
- Cross-platform
- Web frontend that directly runs in your browser
- mDNS discovery
- End-to-End encryption using the [PSEC protocol](https://forge.chapril.org/hardcoresushi/PSEC)
- Automatic peer discovery using mDNS
- Manual peer connection
- Database encryption
- Multi-identities
- File transferts
- Encrypted database
- Contact verification
- IPv4/v6 compatibility
- Web frontend that directly runs in browser
- Free/Libre and Open Source
# Download
AIRA releases are availables in the "Release" section. All files MUST be signed with my PGP key. Don't execute them if the verification fails.
To download my key:
`gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 007F84120107191E` \
Fingerprint: `BD56 2147 9E7B 74D3 6A40 5BE8 007F 8412 0107 191E` \
Email: `Hardcore Sushi <hardcore.sushi@disroot.org>`
Then, verify release file: `gpg --verify aira.elf.asc aira.elf`
# Build
### Install Rust
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
curl --proto '=https' --tlsv1.3 -sSf https://sh.rustup.rs | sh
```
### Download AIRA
```
git clone --depth=1 https://forge.chapril.org/hardcoresushi/AIRA.git && cd AIRA
```
### Verify commit
```
git verify-commit HEAD
```
### Build AIRA
```
cargo build
cargo build --release
```
## What does AIRA stand for ?
The name AIRA was inspired by the name [GNU](https://www.gnu.org): "GNU's Not Unix". In the same way, AIRA stands for "AIRA Is a Recursive Acronym".
AIRA Is a Recursive Acronym.

74
build.rs Normal file
View File

@ -0,0 +1,74 @@
#[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,
};
#[cfg(not(debug_assertions))]
fn minify_content(content: &str, language: &str) -> Option<String> {
match language {
"html" => Some(html_minifier::minify(content).unwrap()),
"js" => Some(html_minifier::js::minify(content)),
"css" => Some(html_minifier::css::minify(content).unwrap()),
_ => None,
}
}
#[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");
if let Err(e) = create_dir(out_dir.join("commons")) {
if e.kind() != ErrorKind::AlreadyExists {
panic!("Failed to create \"commons\" directory");
}
}
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",
].iter().for_each(|file_name| {
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))]
generate_web_files();
}

1
config.yml Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,64 +0,0 @@
use std::{io, io::Write, sync::Arc};
use crate::identity::Identity;
use crate::session_manager::SessionManager;
use text_io::read;
/*fn get_identity_or_create() -> identity::Identity {
match identity::get_identity() {
Ok(identity) => {identity}
Err(_) => {
identity::create_new_identidy("Bob").unwrap()
}
}
}*/
pub fn main(){
let identity = match Identity::get_identity() {
Ok(identity) => {identity}
Err(_) => {
println!("Creating new identity");
Identity::create_new_identidy("Bob").unwrap()
}
};
let session_manager = Arc::new(SessionManager::new(identity));
match SessionManager::start_listener(&session_manager) {
Ok(_) => {}
Err(e) => println!("{}", e.to_string())
}
SessionManager::start_receiver_loop(&session_manager);
let mut input = String::new();
while input != "exit" {
io::stdout().write(":~$ ".as_bytes()).unwrap();
io::stdout().flush().unwrap();
input = read!("{}\n");
let args: Vec<&str> = input.split(" ").collect();
if args[0] == "init" {
match session_manager.connect_to(args[1]) {
Ok(index) => {
println!("Connected !");
while input != "exit" {
io::stdout().write("BOB:~$ ".as_bytes()).unwrap();
io::stdout().flush().unwrap();
input = read!("{}\n");
match session_manager.send_to(&index, &input) {
Ok(_) => {
match session_manager.receive_from(index) {
Ok(msg) => println!("{}", std::str::from_utf8(&msg).unwrap()),
Err(e) => println!("{}", e.to_string())
}
}
Err(e) => println!("{}", e.to_string())
}
}
}
Err(e) => println!("{}", e.to_string())
}
} else if args[0] == "list" {
for (i, n) in session_manager.get_sessions() {
println!("{}: {}", i, n);
}
} else if args[0] != "exit" {
println!("?");
}
}
}

View File

@ -1,4 +1,8 @@
pub const PORT: &str = "7530";
pub const PORT: u16 = 7530;
pub const UI_PORT: u16 = 9510;
pub const APPLICATION_FOLDER: &str = "AIRA";
pub const DB_NAME: &str = "AIRA.db";
pub const HTTP_COOKIE_NAME: &str = "aira_auth";
pub const MUTEX_RELEASE_DELAY_MS: u64 = 100;
pub const MSG_LOADING_COUNT: usize = 20;
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 aes_gcm::{aead::{Aead, generic_array::GenericArray}, NewAead};
use aes_gcm_siv::{Aes256GcmSiv};
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;
const SALT_LEN: usize = 32;
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().to_be_bytes(), label.as_bytes(), &context.len().to_be_bytes(), context];
hkdf.expand_multi_info(&info, okm).unwrap();
}
None => {
let info = [&label.len().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_varkey(&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_varkey(&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);
@ -161,26 +30,35 @@ pub fn encrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoErr
if master_key.len() != MASTER_KEY_LEN {
return Err(CryptoError::InvalidLength);
}
let cipher = Aes256GcmSiv::new(GenericArray::from_slice(master_key));
let cipher = Aes256GcmSiv::new_from_slice(master_key).unwrap();
let mut iv = [0; IV_LEN];
OsRng.fill_bytes(&mut iv); //use it for IV for now
let mut cipher_text = iv.to_vec();
cipher_text.extend(cipher.encrypt(GenericArray::from_slice(&iv), data).unwrap());
cipher_text.extend(cipher.encrypt(Nonce::from_slice(&iv), data).unwrap());
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);
}
let cipher = Aes256GcmSiv::new(GenericArray::from_slice(master_key));
match cipher.decrypt(GenericArray::from_slice(&data[..IV_LEN]), &data[IV_LEN..]) {
let cipher = Aes256GcmSiv::new_from_slice(master_key).unwrap();
match cipher.decrypt(Nonce::from_slice(&data[..IV_LEN]), &data[IV_LEN..]) {
Ok(data) => {
Ok(data)
},
@ -189,21 +67,21 @@ pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoErr
}
fn scrypt_params() -> Params {
Params::new(13, 8, 1).unwrap()
Params::new(16, 8, 1).unwrap()
}
pub fn encrypt_master_key(mut master_key: [u8; MASTER_KEY_LEN], password: &str) -> (
pub fn encrypt_master_key(mut master_key: [u8; MASTER_KEY_LEN], password: &[u8]) -> (
[u8; SALT_LEN], //salt
[u8; IV_LEN+MASTER_KEY_LEN+AES_TAG_LEN] //encrypted masterkey with IV
) {
let mut salt = [0; SALT_LEN];
OsRng.fill_bytes(&mut salt);
let mut password_hash = [0; PASSWORD_HASH_LEN];
scrypt(password.as_bytes(), &salt, &scrypt_params(), &mut password_hash).unwrap();
scrypt(password, &salt, &scrypt_params(), &mut password_hash).unwrap();
let mut output = [0; IV_LEN+MASTER_KEY_LEN+AES_TAG_LEN];
OsRng.fill_bytes(&mut output); //use it for IV for now
let cipher = Aes256GcmSiv::new(GenericArray::from_slice(&password_hash));
let encrypted_master_key = cipher.encrypt(GenericArray::from_slice(&output[..IV_LEN]), master_key.as_ref()).unwrap();
let cipher = Aes256GcmSiv::new_from_slice(&password_hash).unwrap();
let encrypted_master_key = cipher.encrypt(Nonce::from_slice(&output[..IV_LEN]), master_key.as_ref()).unwrap();
master_key.zeroize();
password_hash.zeroize();
encrypted_master_key.into_iter().enumerate().for_each(|i|{
@ -212,14 +90,14 @@ pub fn encrypt_master_key(mut master_key: [u8; MASTER_KEY_LEN], password: &str)
(salt, output)
}
pub fn decrypt_master_key(encrypted_master_key: &[u8], password: &str, salt: &[u8]) -> Result<[u8; MASTER_KEY_LEN], CryptoError> {
pub fn decrypt_master_key(encrypted_master_key: &[u8], password: &[u8], salt: &[u8]) -> Result<[u8; MASTER_KEY_LEN], CryptoError> {
if encrypted_master_key.len() != IV_LEN+MASTER_KEY_LEN+AES_TAG_LEN || salt.len() != SALT_LEN {
return Err(CryptoError::InvalidLength);
}
let mut password_hash = [0; PASSWORD_HASH_LEN];
scrypt(password.as_bytes(), salt, &scrypt_params(), &mut password_hash).unwrap();
let cipher = Aes256GcmSiv::new(GenericArray::from_slice(&password_hash));
let result = match cipher.decrypt(GenericArray::from_slice(&encrypted_master_key[..IV_LEN]), &encrypted_master_key[IV_LEN..]) {
scrypt(password, salt, &scrypt_params(), &mut password_hash).unwrap();
let cipher = Aes256GcmSiv::new_from_slice(&password_hash).unwrap();
let result = match cipher.decrypt(Nonce::from_slice(&encrypted_master_key[..IV_LEN]), &encrypted_master_key[IV_LEN..]) {
Ok(master_key) => {
if master_key.len() == MASTER_KEY_LEN {
Ok(master_key.try_into().unwrap())
@ -231,4 +109,4 @@ pub fn decrypt_master_key(encrypted_master_key: &[u8], password: &str, salt: &[u
};
password_hash.zeroize();
result
}
}

View File

@ -1,40 +1,29 @@
use std::thread;
use astro_dnssd::register::DNSServiceBuilder;
use std::{net::IpAddr, io};
use tokio::runtime::Handle;
use libmdns::{Responder, Service};
use multicast_dns::discovery::{DiscoveryManager, DiscoveryListeners, ResolveListeners};
use crate::{constants, print_error};
const SERVICE_TYPE: &str = "_aira._tcp";
pub fn advertise_me(){
thread::spawn(||{
let mut service = DNSServiceBuilder::new(SERVICE_TYPE)
.with_name("AIRA Node")
.with_port(constants::PORT.parse().unwrap())
.build()
.unwrap();
match service.register(|reply| match reply {
Ok(_) => {},
Err(e) => println!("Error registering: {:?}", e)
}) {
Ok(_) => {
loop {
service.process_result();
}
}
Err(e) => println!("Unable to register mDNS service. You won't be discoverable by others peers. {}", e)
};
});
pub async fn advertise_me() -> io::Result<Service> {
Ok(Responder::spawn(&Handle::current())?.register(
SERVICE_TYPE.to_string(),
"AIRA Node".to_string(),
constants::PORT,
&[]
))
}
pub fn discover_peers<F: Fn(&DiscoveryManager, String)>(on_service_discovered: F){
pub fn discover_peers<F: Fn(&DiscoveryManager, IpAddr)>(on_service_discovered: F) {
let discovery_manager = DiscoveryManager::new();
let result = discovery_manager.discover_services(SERVICE_TYPE, DiscoveryListeners{
match discovery_manager.discover_services(SERVICE_TYPE, DiscoveryListeners{
on_service_discovered: Some(&|service| {
discovery_manager.resolve_service(service, ResolveListeners{
on_service_resolved: Some(&|service| {
match service.address {
Some(service_ip) => on_service_discovered(&discovery_manager, service_ip),
None => println!("Service discovered without IP address: {:?}", service)
Some(service_ip) => on_service_discovered(&discovery_manager, service_ip.parse().unwrap()),
None => print_error!("Service discovered without IP address: {:?}", service)
};
})
});
@ -42,8 +31,8 @@ pub fn discover_peers<F: Fn(&DiscoveryManager, String)>(on_service_discovered: F
on_all_discovered: Some(&|| {
discovery_manager.stop_service_discovery();
})
});
if result.is_err() {
print_error(result.err().unwrap());
}) {
Ok(_) => {},
Err(e) => print_error!(e)
}
}

View File

@ -1,13 +1,67 @@
function generate_avatar(name){
let span = document.createElement("span");
if (typeof name == "undefined"){
span.appendChild(document.createTextNode("?"));
} else {
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

@ -1,3 +1,19 @@
@font-face {
font-family: TwitterColorEmoji;
src: url("/static/fonts/TwitterColorEmoji.ttf");
}
:root {
--accent: #ACCENT_COLOR;
--transparent: #00000000;
}
html {
height: 100%;
font-family: TwitterColorEmoji,Arial,Helvetica,Sans-Serif;
color: white;
}
input[type="text"], input[type="password"] {
background-color: var(--transparent);
color: white;
@ -8,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;
}

Binary file not shown.

BIN
src/frontend/imgs/frog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

1908
src/frontend/imgs/frog.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 90 KiB

View File

@ -0,0 +1 @@
<svg viewBox="0 0 329.26933 329" fill="#FILL_COLOR" xmlns="http://www.w3.org/2000/svg"><path d="m194.800781 164.769531 128.210938-128.214843c8.34375-8.339844 8.34375-21.824219 0-30.164063-8.339844-8.339844-21.824219-8.339844-30.164063 0l-128.214844 128.214844-128.210937-128.214844c-8.34375-8.339844-21.824219-8.339844-30.164063 0-8.34375 8.339844-8.34375 21.824219 0 30.164063l128.210938 128.214843-128.210938 128.214844c-8.34375 8.339844-8.34375 21.824219 0 30.164063 4.15625 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921875-2.089844 15.082031-6.25l128.210937-128.214844 128.214844 128.214844c4.160156 4.160156 9.621094 6.25 15.082032 6.25 5.460937 0 10.921874-2.089844 15.082031-6.25 8.34375-8.339844 8.34375-21.824219 0-30.164063zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 757 B

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="M15 16h4v2h-4zm0-8h7v2h-7zm0 4h6v2h-6zM3 18c0 1.1.9 2 2 2h6c1.1 0 2-.9 2-2V8H3v10zM14 5h-3l-1-1H6L5 5H2v2h12z"/></svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FILL_COLOR"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>

After

Width:  |  Height:  |  Size: 233 B

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 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FILL_COLOR"><path d="M0 0h24v24H0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>

After

Width:  |  Height:  |  Size: 336 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

616
src/frontend/index.css Normal file
View File

@ -0,0 +1,616 @@
:root {
--button-background: #52585C
}
body {
margin: 0;
height: 100%;
}
main {
display: grid;
grid-template-columns: 25% auto;
height: 100%;
}
.panel {
padding-left: 20px;
padding-right: 20px;
display: flex;
flex-direction: column;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
button {
background-color: var(--transparent);
border: none;
cursor: pointer;
}
button::after {
background-color: var(--button-background);
border-radius: 100%;
display: block;
width: 20px;
height: 20px;
padding: 8px;
}
button:hover::after {
background-color: var(--accent);
}
.classic_button {
background-color: var(--button-background);
color: white;
cursor: pointer;
padding: 10px 20px;
border-radius: 8px;
font-weight: bold;
}
.classic_button:hover {
background-color: var(--accent);
}
.file_picker {
display: flex;
align-items: center;
cursor: pointer;
}
.file_picker::after {
content: url("/static/imgs/icons/attach/ACCENT_COLOR");
width: 2em;
}
.popup h2.warning::before {
content: url("/static/imgs/icons/warning/ACCENT_COLOR");
width: 2em;
display: inline-block;
vertical-align: middle;
}
.switch_preference {
display: flex;
align-items: center;
}
.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;
}
.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;
}
.section_title {
margin-left: 8px;
font-weight: bold;
opacity: 0.5;
}
.section_title:first-of-type {
margin-top: 25px;
}
#left_panel {
background-color: #1D2228;
}
#right_panel {
background-color: #15191E;
overflow: hidden;
}
#me {
border-bottom: 2px solid var(--accent);
padding: 10px;
display: flex;
align-items: center;
cursor: pointer;
}
#me>div {
display: flex;
align-items: center;
flex-grow: 1;
}
#me p {
font-size: 1.7em;
margin: 0;
font-weight: bold;
display: inline;
}
#me>div:hover p {
color: var(--accent);
}
#me .avatar {
font-size: 1.2em;
}
#left_panel ul:last-of-type, #msg_log {
flex-grow: 1;
}
#left_panel ul li {
font-size: 1.1em;
padding: 15px;
height: 50px;
box-sizing: border-box;
margin-left: 8px;
margin-bottom: 10px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
}
#left_panel ul li>p {
display: inline;
font-weight: bold;
flex-grow: 1;
}
#left_panel ul li .avatar {
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/ACCENT_COLOR");
display: inline-block;
width: 1em;
margin-left: 5px;
vertical-align: middle;
}
#left_panel ul li.is_contact p::after {
content: unset;
}
#left_panel ul li.is_verified p::after {
content: url("/static/imgs/icons/verified/ACCENT_COLOR");
}
#left_panel ul li.not_seen::after {
content: "";
width: 12px;
height: 12px;
background-color: var(--accent);
border-radius: 12px;
}
#aira_version {
opacity: 0.5;
font-weight: bold;
margin: 8px;
}
#refresher button::after {
content: url("/static/imgs/icons/refresh");
}
#refresher, #connect_box>div {
position: relative;
}
#show_local_ips, #refresher button {
position: absolute;
right: 10px;
top: 15px;
z-index: 1;
}
#show_local_ips::after {
content: url("/static/imgs/icons/info/52585C");
background-color: unset;
padding: unset;
width: 38px;
height: 38px;
}
#show_local_ips:hover::after {
background-color: unset;
content: url("/static/imgs/icons/info/ACCENT_COLOR");
}
.popup ul {
list-style-type: unset;
}
#chat_header {
flex-direction: row;
align-items: center;
padding: 20px 20px;
}
#chat_header>div {
display: flex;
align-items: center;
flex-grow: 1;
cursor: pointer;
}
#chat_header>div:hover p {
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/ACCENT_COLOR");
display: inline-block;
width: 1.2em;
vertical-align: middle;
margin-left: 10px;
}
#chat_header.is_contact p::after {
content: unset;
}
#chat_header.is_verified p::after {
content: url("/static/imgs/icons/verified/ACCENT_COLOR");
}
#chat_header.is_contact #delete_conversation::after {
content: url("/static/imgs/icons/delete_conversation");
}
#add_contact::after {
content: url("/static/imgs/icons/add_contact");
}
#chat_header.is_contact #remove_contact::after {
content: url("/static/imgs/icons/remove_contact");
}
#chat_header.is_contact #verify {
display: unset;
}
#chat_header #verify,
#chat_header.is_verified #verify,
#chat_header.is_contact #add_contact,
#chat_header.offline #add_contact {
display: none;
}
#chat_header.is_contact #verify::after {
content: url("/static/imgs/icons/verified");
}
#logout::after {
content: url("/static/imgs/icons/logout");
}
#message_input {
border: unset;
padding: 20px;
font-size: 1.1em;
}
#file_transfer {
border-top: 2px solid var(--accent);
position: relative;
}
#file_transfer.active {
display: block;
}
#file_transfer span {
font-weight: bold;
}
#file_control {
display: flex;
align-items: center;
}
#file_cancel {
padding: 0;
}
#file_cancel::after {
background-color: unset;
width: 20px;
content: url("/static/imgs/icons/cancel/ACCENT_COLOR");
}
#file_progress {
display: none;
gap: 10px;
align-items: center;
margin-bottom: 15px;
}
#file_transfer.active>#file_progress {
display: flex;
}
#file_status {
margin-top: 0;
}
#file_percent, #file_speed {
font-weight: bold;
margin: 0;
border-left: 2px solid var(--accent);
padding-left: 10px;
}
#file_progress_bar {
flex-grow: 1;
height: 25px;
}
#file_progress_bar div {
height: 100%;
background-color: var(--accent);
}
#msg_log {
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;
}
#msg_log li .header p {
color: var(--accent);
font-weight: bold;
margin-left: .5em;
}
#msg_log li .content {
margin-left: 3em;
}
#msg_log li .content p {
word-break: break-word;
}
#msg_log a {
color: #238cf5;
}
#msg_log .file {
display: flex;
align-items: end;
border-left: 3px solid var(--accent);
padding-left: 15px;
}
#msg_log .file div { /*title and filename container*/
display: flex;
flex-direction: column;
}
#msg_log .file h4 {
margin: 0;
}
#msg_log .file p {
color: var(--accent);
}
#msg_log .file a::after {
content: url("/static/imgs/icons/download/ACCENT_COLOR");
display: block;
width: 2em;
margin-left: 15px;
}
#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,285 +3,20 @@
<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">
<style>
:root {
--accent: #FF3C00;
--transparent: #00000000;
}
html {
height: 100%;
font-family: Arial, Helvetica, Sans-Serif;
color: white;
}
body {
margin: 0;
height: 100%;
}
main {
display: grid;
grid-template-columns: 25% auto;
height: 100%;
}
.panel {
padding-left: 20px;
padding-right: 20px;
display: flex;
flex-direction: column;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
button {
background-color: var(--transparent);
border: none;
cursor: pointer;
}
button::after {
background-color: #8E99A4;
border-radius: 100%;
display: block;
width: 20px;
height: 20px;
padding: 8px;
}
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/FF3C00");
width: 2em;
}
.popup {
position: fixed;
top: 40%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
width: 40%;
box-sizing: border-box;
padding: 20px 70px;
background-color: #2B2F31;
border-radius: 10px;
text-align: center;
font-size: 1.3em;
}
.popup h2::before {
content: url("/static/imgs/icons/warning/FF3C00");
width: 9%;
display: inline-block;
vertical-align: middle;
}
.popup .buttons {
display: flex;
justify-content: center;
}
.popup button {
background-color: #52585C;
color: white;
cursor: pointer;
padding: 15px 30px;
border-radius: 8px;
margin-left: 25px;
margin-right: 25px;
font-weight: bold;
}
.popup button:hover {
background-color: var(--accent);
}
.section_title {
margin-left: 8px;
font-weight: bold;
opacity: 0.5;
}
#left_panel {
background-color: #1D2228;
}
#right_panel {
background-color: #15191E;
display: flex;
flex-direction: column;
overflow: hidden;
}
#me {
border-bottom: 2px solid var(--accent);
padding: 10px;
display: flex;
align-items: center;
font-size: 1.7em;
}
#me p {
flex-grow: 1;
margin: 0;
font-weight: bold;
}
#left_panel ul:last-of-type, #msg_log {
flex-grow: 1;
}
#left_panel ul li {
font-size: 1.1em;
padding: 15px;
height: 50px;
box-sizing: border-box;
margin-left: 8px;
margin-bottom: 10px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
}
#left_panel ul li>p {
display: inline;
font-weight: bold;
}
#left_panel ul li .avatar {
width: 2em;
height: 2em;
}
#left_panel ul li:hover, #left_panel ul li.current {
background-color: #333940;
}
#left_panel ul li.output {
}
#left_panel ul li.input {
}
#left_panel ul li p::after {
content: url("/static/imgs/icons/warning/FF3C00");
display: inline-block;
width: 1em;
margin-left: .3em;
vertical-align: middle;
}
#left_panel ul li.is_contact p::after {
content: unset;
}
#left_panel ul li.is_verified p::after {
content: url("/static/imgs/icons/verified/FF3C00");
}
#connect_box {
margin-bottom: 20px;
}
#chat_header {
flex-direction: row;
align-items: center;
padding: 5px 20px;
font-size: 1.5em;
}
#chat_header>div {
display: flex;
align-items: center;
flex-grow: 1;
}
#chat_header>div>p {
font-weight: bold;
}
#chat_header p::after {
content: url("/static/imgs/icons/warning/FF3C00");
display: inline-block;
width: 1.2em;
vertical-align: middle;
padding-left: .3em;
}
#chat_header.is_contact p::after {
content: unset;
}
#chat_header.is_verified p::after {
content: url("/static/imgs/icons/verified/FF3C00");
}
#add_contact::after {
content: url("/static/imgs/icons/add_contact");
}
#remove_contact::after {
content: url("/static/imgs/icons/remove_contact");
}
#chat_header.is_contact #verify::after {
content: url("/static/imgs/icons/verified");
}
#chat_header.is_verified #verify::after {
content: unset;
}
#logout::after {
content: url("/static/imgs/icons/logout");
}
#message_input {
border: unset;
padding: 1em;
font-size: 1.1em;
}
#message_box {
border-top: 2px solid var(--accent);
margin-bottom: 0;
}
#msg_log {
font-size: 1.1em;
overflow-y: scroll;
white-space: pre;
}
#msg_log li>div:first-of-type { /*Message header*/
display: flex;
align-items: center;
}
#msg_log p.name {
margin: 0;
color: var(--accent);
font-weight: bold;
}
#msg_log li>div:last-of-type { /*Message content container*/
margin-left: 2em;
}
#msg_log li p {
margin-top: 0;
}
#msg_log .file {
display: flex;
align-items: end;
margin-bottom: 1em;
border-left: 3px solid var(--accent);
padding-left: .5em;
margin-top: .5em;
}
#msg_log .file div { /*Title and filename container*/
display: flex;
flex-direction: column;
}
#msg_log .file h4 {
margin: 0;
}
#msg_log .file p {
margin: 0;
color: var(--accent);
}
#msg_log .file a::after {
content: url("/static/imgs/icons/download/FF3C00");
display: block;
width: 2em;
margin-left: 1em;
}
#message_box, #chat_header, #msg_log {
display: none;
}
</style>
<link rel="stylesheet" href="/static/index.css">
</head>
<body>
<main id="main">
<div id="left_panel" class="panel">
<div id="me">
<p>IDENTITY_NAME</p>
<div></div>
<button id="logout" title="Log out"></button>
</div>
<div id="refresher">
<button title="Refresh"></button>
</div>
<p class="section_title">Online peers:</p>
<ul id="online_sessions">
</ul>
@ -289,422 +24,72 @@
<ul id="offline_sessions">
</ul>
<div id="connect_box">
<div>
<button id="show_local_ips" title="Local IP addresses"></button>
</div>
<p class="section_title">Add a new peer by IP:</p>
<input type="text" id="ip_input" placeholder="Enter IP address">
</div>
<p id="aira_version">AIRA vAIRA_VERSION</p>
</div>
<div id="right_panel" class="panel">
<div id="chat_header">
<div></div>
<button id="delete_conversation" title="Delete conversation"></button>
<button id="verify" title="Verify"></button>
<button id="add_contact" title="Add to contact"></button>
<button id="remove_contact" title="Remove from contact"></button>
</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>
<p id="file_info"></p>
</div>
<p id="file_status"></p>
<div id="file_progress">
<p id="file_percent"></p>
<p id="file_speed"></p>
<div id="file_progress_bar">
<div></div>
</div>
</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">
</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>
<script>
//replaced by web server
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>
<script src="/static/commons/script.js"></script>
<script>
"use strict";
const ENTER_KEY_CODE = 13;
const identity_name = "IDENTITY_NAME";
let socket = null;
let current_chat_index = -1;
let sessions_data = new Map();
let msg_history = new Map();
function on_click_session(event) {
let index = event.currentTarget.getAttribute("data-index");
if (index != null){
current_chat_index = index;
refresh_sessions();
display_header();
if (sessions_data.get(index).is_online){
document.getElementById("message_box").style.display = "flex";
}
display_history();
}
}
let ip_input = document.getElementById("ip_input");
ip_input.addEventListener("keyup", function(event){
if (event.keyCode === ENTER_KEY_CODE) {
socket.send("connect "+ip_input.value);
ip_input.value = "";
}
});
let message_input = document.getElementById("message_input");
message_input.addEventListener("keyup", function(event){
if (event.keyCode === ENTER_KEY_CODE) {
socket.send("send "+current_chat_index+" "+message_input.value);
msg_history.get(current_chat_index).push([true, false, message_input.value]);
message_input.value = "";
display_history(current_chat_index);
}
});
document.getElementById("add_contact").onclick = function(){
socket.send("contact "+current_chat_index+" "+sessions_data.get(current_chat_index).name);
sessions_data.get(current_chat_index).is_contact = true;
display_header();
refresh_sessions();
}
document.getElementById("remove_contact").onclick = function(){
socket.send("uncontact "+current_chat_index);
let session = sessions_data.get(current_chat_index);
session.is_contact = false;
session.is_verified = false;
display_header();
refresh_sessions();
}
document.getElementById("verify").onclick = function(){
socket.send("fingerprints "+current_chat_index);
}
document.getElementById("logout").onclick = function(){
display_popup("<p>If you log out, you will no longer receive messages and pending messages will not be sent until you log in back.</p><p>Do you really want to log out ?</p>",
"<button onclick=\"logout();\">Log out</button>"
);
}
document.getElementById("attach_file").onchange = function(event){
let file = event.target.files[0];
if (file.size > 45000000) { //45MB
display_popup("<p>The file is too large. Please select only files under 45MB.</p>");
} else {
let reader = new FileReader();
reader.onload = function(e){
socket.send("file "+current_chat_index+" "+file.name);
let data = e.target.result;
socket.send(data);
}
reader.readAsArrayBuffer(file);
}
}
socket = new WebSocket("ws://"+location.hostname+":WEBSOCKET_PORT/ws");
socket.onopen = function() {
console.log("Connected");
};
socket.onmessage = function(msg){
if (typeof msg.data == "string"){
console.log("Message: "+msg.data);
let args = msg.data.split(" ");
switch (args[0]){
case "connected":
on_connected(args[1]);
break;
case "disconnected":
on_disconnected(args[1]);
break;
case "new_session":
on_new_session(args[1]);
break;
case "new_message":
on_message_received(args[1], args.slice(2).join(" "));
break;
case "load_sent_msg":
on_msg_load(args[1], args.slice(2).join(" "));
break;
case "load_sent_file":
on_file_load(args[1], args[2], args.slice(3).join(" "));
break;
case "name_told":
on_name_told(args[1], args.slice(2).join(" "));
break;
case "is_contact":
on_is_contact(args[1], args[2], args.slice(3).join(" "));
break;
case "fingerprints":
on_fingerprints(args[1], args[2]);
break;
case "file":
on_file_received(args[1], args[2], args.slice(3).join(" "));
break;
case "file_sent":
on_file_sent(args[1], args[2], args.slice(3).join(" "));
break;
}
} else { //receiving file
on_file_re
}
}
socket.onclose = function(){
console.log("Disconnected");
}
let me = document.getElementById("me");
me.insertBefore(generate_avatar(identity_name), me.firstChild);
function on_connected(index){
if (sessions_data.has(index)){
sessions_data.get(index).is_online = true;
refresh_sessions();
} else {
add_session(index, undefined, true, false, false, true);
}
}
function on_new_session(index){
if (sessions_data.has(index)){
let session = sessions_data.get(index);
session.is_online = true;
session.outgoing = false;
refresh_sessions();
} else {
add_session(index, undefined, false, false, false, true);
}
}
function on_name_told(index, name){
sessions_data.get(index).name = name;
if (index == current_chat_index) {
display_header();
}
refresh_sessions();
}
function on_is_contact(index, str_verified, name){
let verified = (str_verified === "true");
if (sessions_data.has(index)){
let session = sessions_data.get(index);
session.is_contact = true;
session.is_verified = verified;
on_name_told(index, name);
} else {
add_session(index, name, true, true, verified, false);
}
}
function on_message_received(index, msg){
msg_history.get(index).push([false, false, msg]);
if (current_chat_index == index){
display_history();
}
}
function on_msg_load(index, msg){
msg_history.get(index).push([true, false, msg]);
if (current_chat_index == index){
display_history();
}
}
function on_file_load(index, uuid, file_name){
msg_history.get(index).push([true, true, [uuid, file_name]]);
if (current_chat_index == index){
display_history();
}
}
function on_disconnected(index){
if (current_chat_index == index){
document.getElementById("message_box").style.display = "none";
}
let session = sessions_data.get(index);
if (session.is_contact){
session.is_online = false;
} else {
sessions_data.delete(index);
if (current_chat_index == index){
current_chat_index = -1;
}
}
refresh_sessions();
}
function on_fingerprints(local, peer){
let beautify_fingerprints = function(f){
for (let i=4; i<f.length; i+=5){
f = f.slice(0, i)+" "+f.slice(i);
}
return f;
};
display_popup("<p>Compare the following fingerprints by a trusted way of communication (such as real life) before clicking on Verify.</p><p>Local fingerprint:</p><pre>"+beautify_fingerprints(local)+"</pre><p>Peer fingerprint:</p><pre>"+beautify_fingerprints(peer)+"</pre>",
"<button onclick=\"verify();\">Verified</button>"
);
}
function on_file_received(index, uuid, file_name){
msg_history.get(index).push([false, true, [uuid, file_name]]);
if (current_chat_index == index){
display_history();
}
}
function on_file_sent(index, uuid, file_name){
msg_history.get(index).push([true, true, [uuid, file_name]]);
if (current_chat_index == index){
display_history();
}
}
function add_session(index, name, outgoing, is_contact, is_verified, is_online){
sessions_data.set(index, {
"name": name,
"outgoing": outgoing,
"is_contact": is_contact,
"is_verified": is_verified,
"is_online": is_online,
});
msg_history.set(index, []);
refresh_sessions();
}
function refresh_sessions(){
let online_sessions = document.getElementById("online_sessions");
online_sessions.innerHTML = "";
let offline_sessions = document.getElementById("offline_sessions");
offline_sessions.innerHTML = "";
sessions_data.forEach(function (session, index){
let session_element = generate_session(index, session.outgoing, session.is_contact, session.is_verified, session.name);
if (session.is_online){
online_sessions.appendChild(session_element);
} else {
offline_sessions.appendChild(session_element) ;
}
});
}
function verify(){
socket.send("verify "+current_chat_index);
sessions_data.get(current_chat_index).is_verified = true;
remove_popup();
display_header();
refresh_sessions();
}
function logout(){
window.location = "/logout";
}
function display_header(){
let chat_header = document.getElementById("chat_header");
let session = sessions_data.get(current_chat_index);
chat_header.children[0].innerHTML = "";
chat_header.children[0].appendChild(generate_avatar(session.name));
chat_header.children[0].appendChild(generate_name(session.name));
chat_header.style.display = "flex";
if (session.is_contact){ //is_contact
chat_header.classList.add("is_contact");
document.getElementById("add_contact").style.display = "none";
document.getElementById("remove_contact").style.display = "block";
} else {
chat_header.classList.remove("is_contact");
document.getElementById("remove_contact").style.display = "none";
document.getElementById("add_contact").style.display = "block";
}
if (session.is_verified){
chat_header.classList.add("is_verified");
} else {
chat_header.classList.remove("is_verified");
}
}
function display_popup(content, button){
let popup_content = "<h2>Warning!</h2>"+content+"<div class=\"buttons\"><button onclick=\"remove_popup();\">";
if (typeof button == "undefined"){
popup_content += "OK</button></div>";
} else {
popup_content += "Cancel</button>"+button+"</div>";
}
let popup = document.createElement("div");
popup.classList.add("popup");
popup.innerHTML = popup_content;
let main = document.getElementsByTagName("main")[0];
main.insertBefore(popup, main.firstChild);
}
function remove_popup(){
document.getElementsByClassName("popup")[0].remove();
}
function generate_name(name){
let p = document.createElement("p");
if (typeof name == "undefined"){
p.appendChild(document.createTextNode("Unknown"));
} else {
p.appendChild(document.createTextNode(name));
}
return p;
}
function generate_session(index, outgoing, is_contact, is_verified, name){
let li = document.createElement("li");
li.setAttribute("data-index", index);
if (outgoing) {
li.classList.add("outgoing");
} else {
li.classList.add("incomming");
}
if (is_contact) {
li.classList.add("is_contact");
}
if (is_verified) {
li.classList.add("is_verified");
}
if (index == current_chat_index){
li.classList.add("current");
}
li.appendChild(generate_avatar(name));
li.appendChild(generate_name(name));
li.onclick = on_click_session;
return li;
}
function generate_msg_header(name){
let text = document.createTextNode(name);
let p = document.createElement("p");
p.appendChild(text);
p.classList.add("name");
let div = document.createElement("div");
div.appendChild(generate_avatar(name));
div.appendChild(p);
return div;
}
function generate_message(name, msg){
let text = document.createTextNode(msg);
let p = document.createElement("p");
p.appendChild(text);
let div = document.createElement("div");
div.appendChild(linkifyElement(p));
let li = document.createElement("li");
li.appendChild(generate_msg_header(name))
li.appendChild(div);
return li;
}
function generate_file(name, outgoing, file_info){
let content = "<li>"+generate_msg_header(name)+"<div class=\"file\"><div><h4>";
if (outgoing) {
content += "File sent:";
} else {
content += "File received:";
}
content += "</h4><p>"+file_info[1]+"</p></div>";
if (file_info[0] !== "None"){
content += "<a href=\"/load_file?uuid="+file_info[0]+"&file_name="+encodeURIComponent(file_info[1])+"\" target=\"_blank\"></a>";
}
content += "</div></li>";
return content;
}
function display_history(){
let msg_log = document.getElementById("msg_log");
msg_log.style.display = "block";
msg_log.innerHTML = "";
msg_history.get(current_chat_index).forEach(entry => {
let name;
if (entry[0]){ //outgoing msg
name = identity_name;
} else {
name = sessions_data.get(current_chat_index).name;
}
if (entry[1]){ //is file
msg_log.innerHTML += generate_file(name, entry[0], entry[2]);
} else {
msg_log.appendChild(generate_message(name, entry[2]));
}
});
msg_log.scrollTop = msg_log.scrollHeight;
}
function get_session_li_by_index(index){
["online_sessions", "offline_sessions"].forEach(function(id){
let sessions = document.getElementById(id);
for (let i = 0; i < sessions.children.length; i++) {
let li = sessions.children[i];
if (li.getAttribute("data-index") == index){
return li;
}
}
});
}
</script>
<script src="/static/index.js"></script>
</body>
</html>

1268
src/frontend/index.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -3,17 +3,9 @@
<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>
:root {
--accent: #FF3C00;
--transparent: #00000000;
}
html {
height: 100%;
font-family: Arial, Helvetica, Sans-Serif;
color: white;
}
body {
margin: 0;
height: 100%;
@ -23,14 +15,6 @@
background-color: black;
}
main {
max-width: 900px;
background-color: #2B2F31;
border-radius: 10px;
position: absolute;
top: 20vh;
left: 0;
right: 0;
margin: auto;
padding-bottom: 20px;
}
h1, main>h3, #error_msg {
@ -49,14 +33,15 @@
font-weight: bold;
margin-bottom: 0;
}
#panel_container {
display: grid;
grid-template-columns: 50% 50%;
position: relative;
.action_page>h2 {
margin-top: 0;
}
input[type="text"], input[type="password"] {
margin-bottom: 20px;
}
#create_page input[type="password"] {
display: none;
}
button {
background-color: #52585C;
color: white;
@ -70,142 +55,128 @@
button:hover {
background-color: var(--accent);
}
.panel {
.action_page {
padding: 10px 50px;
}
#separator {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
background-color: var(--accent);
height: 30%;
width: 2px;
.avatar {
font-size: 3em;
}
label {
background-color: #32383C;
padding: 20px;
border-radius: 8px;
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 0 min-content;
grid-gap: 0.5em;
align-items: center;
#identity h2 {
text-align: center;
}
label:hover {
background-color: #42484C;
}
label>div {
display: flex;
align-items: center;
}
label .avatar {
width: 2em;
height: 2em;
font-size: 1.3em;
}
input[type="radio"] {
opacity: 0;
width: 0;
height: 0;
}
.radio_checkmark {
display: flex;
width: 1.5em;
height: 1.5em;
background-color: #52585C;
border-radius: 50%;
align-items: center;
justify-content: center;
}
input[type="radio"]:checked + .radio_checkmark {
background-color: #282D2F;
}
input[type="radio"]:checked + .radio_checkmark::after {
display: block;
}
.radio_checkmark::after {
display: none;
content: "";
width: .5em;
height: .5em;
border-radius: 50%;
background-color: var(--accent);
}
#left_panel.no_identity {
display: none;
}
p {
#error_msg {
color: red;
}
main.no_identity #panel_container {
display: block;
label.checkbox {
display: flex;
align-items: center;
cursor: pointer;
font-size: 1.1em;
margin-bottom: 1em;
}
main.no_identity #left_panel, main.no_identity #separator {
input[type="checkbox"] {
display: none;
}
main.no_identity {
max-width: 500px;
.checkmark {
position: relative;
height: 1.5em;
width: 1.5em;
border: 2px solid var(--accent);
margin-right: .5em;
border-radius: 5px;
}
.checkmark:after {
content: "";
position: absolute;
display: none;
left: .5em;
top: .3em;
width: .3em;
height: .6em;
border: solid white;
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
label.checkbox input:checked ~ .checkmark {
background-color: var(--accent);
}
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 id="main">
<main class="card">
<h1>AIRA</h1>
<h3>Local network secure P2P communications</h3>
<p id="error_msg">ERROR_MSG</p>
<div id="panel_container">
<div id="left_panel" class="panel">
<h2>Login:</h2>
<form id="login_form" method="POST" action="/login">
<div id="identities">
</div>
<input name="password" type="password" placeholder="Password">
<button type="submit">Login</button>
</form>
</div>
<span id="separator"></span>
<div id="right_panel" class="panel">
<h2>Create New Identity:</h2>
<form method="POST">
<input type="text" name="name" placeholder="Name" required>
<input type="password" name="password" placeholder="Password">
<input type="password" name="password_confirm" placeholder="Password (Confirmation)">
<button type="submit">Create</button>
</form>
</div>
<div id="login_page" class="action_page">
<h2>Login:</h2>
<form id="login_form" method="POST" action="/login">
<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 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">
<span class="checkmark"></span>
Encrypt with a password
</label>
<input type="password" name="password" placeholder="Password">
<input type="password" name="password_confirm" placeholder="Password (Confirmation)">
<button type="submit">Create</button>
</form>
</div>
</main>
<script src="/static/commons/script.js"></script>
<script>
const identities = [IDENTITIES];
if (identities.length > 0) {
let identities_container = document.getElementById("identities");
identities.forEach(function(identity) {
let div = document.createElement("div");
div.appendChild(generate_avatar(identity[1]));
let radio_label = document.createElement("span");
radio_label.classList.add("radio_label");
radio_label.appendChild(document.createTextNode(identity[1]));
div.appendChild(radio_label);
let label = document.createElement("label");
label.appendChild(div);
let input = document.createElement("input");
input.type = "radio";
input.name = "uuid";
input.value = identity[0];
input.required = true;
label.appendChild(input);
let radio_checkmark = document.createElement("span");
radio_checkmark.classList.add("radio_checkmark");
label.appendChild(radio_checkmark);
identities_container.appendChild(label);
});
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() {
passwordInputs.forEach(function(i) {
if (enable_password.checked) {
i.style.display = "block";
} else {
i.style.display = "none";
}
});
}
} else {
let main = document.getElementById("main");
main.classList.add("no_identity");
let h2Name = document.getElementById("identityName");
h2Name.textContent = identityName;
document.getElementById("create_page").style.display = "none";
}
</script>
</body>

37
src/frontend/logout.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<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 {
margin: 0;
height: 100%;
background-image: url("/static/imgs/wallpaper");
background-repeat: no-repeat;
background-position: center;
background-color: black;
}
main {
padding: 50px;
}
img {
display: block;
width: 40%;
margin: auto;
}
p {
text-align: center;
font-size: 1.2em;
}
</style>
</head>
<body>
<main class="card">
<img src="/static/imgs/frog">
<p>You've been successfully logged out.</p>
</main>
</body>
</html>

View File

@ -1,17 +1,29 @@
use std::{convert::TryInto, path::Path, str::FromStr};
use std::convert::TryInto;
use crypto::CryptoError;
use ed25519_dalek::{Keypair, Signer, SIGNATURE_LENGTH, PUBLIC_KEY_LENGTH};
use rusqlite::{Connection, NO_PARAMS, params};
use dirs;
use ed25519_dalek::{Keypair, PUBLIC_KEY_LENGTH};
use rusqlite::{Connection, params};
use platform_dirs::AppDirs;
use utils::to_uuid_bytes;
use uuid::Uuid;
use zeroize::Zeroize;
use crate::{constants, crypto, print_error, utils};
use crate::{constants, crypto, key_value_table::KeyValueTable, print_error, utils};
const DB_NAME: &str = "Identities.db";
const IDENTITY_TABLE: &str = "identities";
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";
struct DBKeys;
impl<'a> DBKeys {
pub const NAME: &'a str = "name";
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 {
if b { 75 } else { 30 } //completely arbitrary values
@ -28,197 +40,204 @@ fn byte_to_bool(b: u8) -> Result<bool, ()> {
}
fn get_database_path() -> String {
Path::new(&dirs::config_dir().unwrap()).join(constants::APPLICATION_FOLDER).join(DB_NAME).to_str().unwrap().to_owned()
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>
}
fn get_identity_by_uuid(uuid: Uuid) -> Result<Option<EncryptedIdentity>, rusqlite::Error>{
let db = Connection::open(get_database_path())?;
let mut stmt = db.prepare(&("SELECT name, key, salt, masterkey FROM ".to_owned()+IDENTITY_TABLE+" WHERE uuid=?1"))?;
let mut rows = stmt.query(vec![&uuid.as_bytes()[..]])?;
Ok(match rows.next()? {
Some(row) => {
let name: Vec<u8> = row.get(0)?;
Some(
EncryptedIdentity {
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
encrypted_keypair: row.get(1)?,
salt: row.get(2)?,
encrypted_master_key: row.get(3)?
}
)
}
None => None
})
#[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 uuid: Uuid,
pub name: String,
key: Keypair,
pub master_key: [u8; crypto::MASTER_KEY_LEN]
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.key.sign(input).to_bytes()
}
pub fn get_public_key(&self) -> [u8; PUBLIC_KEY_LENGTH] {
self.key.public.to_bytes()
self.keypair.public.to_bytes()
}
fn get_database_path(&self) -> String {
Path::new(&dirs::config_dir().unwrap()).join(constants::APPLICATION_FOLDER).join(self.uuid.to_string()+".db").to_str().unwrap().to_owned()
}
pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
let db = Connection::open(self.get_database_path())?;
db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, key BLOB, verified BLOB)"), NO_PARAMS)?;
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, 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();
db.execute(&("INSERT INTO ".to_owned()+CONTACTS_TABLE+" (uuid, name, key, verified) VALUES (?1, ?2, ?3, ?4)"), vec![&contact_uuid.as_bytes()[..], encrypted_name.as_slice(), encrypted_public_key.as_slice(), encrypted_verified.as_slice()])?;
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(true)], &self.master_key).unwrap();
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,
verified: false
public_key,
name,
avatar: avatar_uuid,
verified: false,
seen: true,
})
}
pub fn remove_contact(&self, uuid: &Uuid) -> Result<(), rusqlite::Error> {
let db = Connection::open(self.get_database_path())?;
db.execute(&("DELETE FROM ".to_owned()+CONTACTS_TABLE+" WHERE uuid=?"), &[&uuid.as_bytes()[..]])?;
db.execute(&format!("DROP TABLE IF EXISTS \"{}\"", uuid), NO_PARAMS)?;
db.execute(&format!("DELETE FROM {} WHERE contact_uuid=?", FILES_TABLE), &[&uuid.as_bytes()[..]])?;
Ok(())
pub fn remove_contact(uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
Identity::delete_conversation(uuid)?;
db.execute(&format!("DELETE FROM {} WHERE uuid=?", CONTACTS_TABLE), [&uuid.as_bytes()[..]])
}
pub fn set_verified(&self, uuid: &Uuid) -> Result<(), rusqlite::Error> {
let db = Connection::open(self.get_database_path())?;
pub fn set_verified(&self, uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
let encrypted_verified = crypto::encrypt_data(&[bool_to_byte(true)], &self.master_key).unwrap();
db.execute(&format!("UPDATE {} SET verified=?1 WHERE uuid=?2", CONTACTS_TABLE), &[encrypted_verified.as_slice(), &uuid.as_bytes()[..]])?;
Ok(())
db.execute(&format!("UPDATE {} SET verified=?1 WHERE uuid=?2", CONTACTS_TABLE), [encrypted_verified.as_slice(), &uuid.as_bytes()[..]])
}
pub fn change_contact_name(&self, uuid: &Uuid, new_name: &str) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
let encrypted_name = crypto::encrypt_data(new_name.as_bytes(), &self.master_key).unwrap();
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();
db.execute(&format!("UPDATE {} SET seen=?1 WHERE uuid=?2", CONTACTS_TABLE), [encrypted_seen.as_slice(), uuid.as_bytes()])
}
pub fn load_contacts(&self) -> Option<Vec<Contact>> {
match Connection::open(self.get_database_path()) {
match Connection::open(get_database_path()) {
Ok(db) => {
match db.prepare(&("SELECT uuid, name, key, verified FROM ".to_owned()+CONTACTS_TABLE)) {
Ok(mut stmt) => {
let mut rows = stmt.query(NO_PARAMS).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 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()
})
}
None => {}
}
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)
}
}
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(&self) -> Result<usize, rusqlite::Error> {
let db = Connection::open(self.get_database_path())?;
db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), NO_PARAMS)
pub fn clear_cache() -> Result<(), rusqlite::Error> {
let db = Connection::open(get_database_path())?;
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(self.get_database_path()) {
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(NO_PARAMS).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> {
let db = Connection::open(self.get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (contact_uuid BLOB, uuid BLOB, data BLOB)", FILES_TABLE), NO_PARAMS)?;
let db = Connection::open(get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (contact_uuid BLOB, uuid BLOB, data BLOB)", FILES_TABLE), [])?;
let file_uuid = Uuid::new_v4();
let encrypted_uuid = crypto::encrypt_data(file_uuid.as_bytes(), &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(data, &self.master_key).unwrap();
@ -230,59 +249,106 @@ impl Identity {
Ok(file_uuid)
}
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<(), rusqlite::Error> {
let db = Connection::open(self.get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), NO_PARAMS)?;
let outgoing_byte: u8 = bool_to_byte(outgoing);
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, 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), vec![encrypted_outgoing, encrypted_data])?;
Ok(())
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) -> Option<Vec<(bool, Vec<u8>)>> {
match Connection::open(self.get_database_path()) {
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 outgoing, data FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => {
let mut rows = stmt.query(NO_PARAMS).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(
(
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,
data
)
)
},
Err(e) => print_error(e)
timestamp: u64::from_be_bytes(timestamp.try_into().unwrap()),
data,
}),
Err(e) => print_error!(e)
}
}
Err(e) => print_error!(e)
}
}
Err(_) => {}
}
Err(e) => print_error!(e)
}
Err(e) => print_error(e)
}
return Some(msgs);
}
Some(msgs)
}
Err(e) => {
print_error(e);
None
}
}
}
Err(e) => print_error!(e)
}
None
}
#[allow(unused_must_use)]
pub fn delete_conversation(contact_uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("DELETE FROM {} WHERE contact_uuid=?", FILES_TABLE), &[&contact_uuid.as_bytes()[..]]);
db.execute(&format!("DROP TABLE IF EXISTS \"{}\"", contact_uuid), [])
}
pub fn change_name(&mut self, new_name: String) -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
let result = db.update(DBKeys::NAME, new_name.as_bytes());
if result.is_ok() {
self.name = new_name;
}
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);
print_error!(e);
None
}
}
@ -290,87 +356,179 @@ impl Identity {
pub fn zeroize(&mut self){
self.master_key.zeroize();
self.key.secret.zeroize();
self.keypair.secret.zeroize();
}
pub fn get_identity(uuid: &str, password: &str) -> Result<Identity, String> {
let uuid = Uuid::from_str(&uuid).unwrap();
match get_identity_by_uuid(uuid) {
fn load_encrypted_identity() -> Result<EncryptedIdentity, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
let name = db.get(DBKeys::NAME)?;
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) => {
match encrypted_identity {
Some(encrypted_identity) => {
match crypto::decrypt_master_key(encrypted_identity.encrypted_master_key.as_slice(), password, encrypted_identity.salt.as_slice()) {
Ok(master_key) => {
match crypto::decrypt_data(encrypted_identity.encrypted_keypair.as_slice(), &master_key) {
Ok(keypair) => {
Ok(Identity{
uuid: uuid,
name: encrypted_identity.name,
key: Keypair::from_bytes(&keypair[..]).unwrap(),
master_key: master_key
})
}
Err(e) => {
print_error(e);
Err("Database corrupted".to_owned())
}
}
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 {
CryptoError::DecryptionFailed => "Bad password".to_owned(),
CryptoError::InvalidLength => 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 crypto::decrypt_data(&encrypted_identity.encrypted_keypair, &master_key) {
Ok(keypair) => {
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) => {
Err(match e {
CryptoError::DecryptionFailed => "Bad password".to_owned(),
CryptoError::InvalidLength => "Database corrupted".to_owned()
})
print_error!(e);
Err(String::from(DATABASE_CORRUPED_ERROR))
}
}
}
None => {
Err("No such identity".to_owned())
Err(e) => {
print_error!(e);
Err(String::from(DATABASE_CORRUPED_ERROR))
}
}
}
Err(e) => {
Err(e.to_string())
}
Err(e) => Err(e.to_string())
}
}
pub fn get_identities() -> Result<Vec<(Uuid, String)>, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
let mut stmt = db.prepare(&("SELECT uuid, name FROM ".to_owned()+IDENTITY_TABLE))?;
let mut rows = stmt.query(NO_PARAMS)?;
let mut identities = Vec::new();
while let Some(row) = rows.next()? {
let uuid: Vec<u8> = row.get(0)?;
let name: Vec<u8> = row.get(1)?;
match to_uuid_bytes(&uuid) {
Some(uuid_bytes) => identities.push(
(
Uuid::from_bytes(uuid_bytes),
std::str::from_utf8(name.as_slice()).unwrap().to_owned()
)
),
None => {}
}
}
Ok(identities)
pub fn get_identity_name() -> Result<String, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
Ok(std::str::from_utf8(&db.get(DBKeys::NAME)?).unwrap().to_string())
}
pub fn is_protected() -> Result<bool, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
Ok(db.get(DBKeys::MASTER_KEY)?.len() != crypto::MASTER_KEY_LEN)
}
pub fn create_new_identidy(name: &str, password: &str) -> Result<Identity, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+IDENTITY_TABLE+"(uuid BLOB PRIMARY KEY, name TEXT, key BLOB, salt BLOB, masterkey BLOB)"), NO_PARAMS)?;
let uuid = Uuid::new_v4();
pub fn create_identidy(name: &str, password: Option<&[u8]>) -> Result<Identity, rusqlite::Error> {
let keypair = Keypair::generate(&mut rand_7::rngs::OsRng);
let master_key = crypto::generate_master_key();
let encrypted_keypair = crypto::encrypt_data(&keypair.to_bytes(), &master_key).unwrap();
let (salt, encrypted_master_key) = crypto::encrypt_master_key(master_key, password);
db.execute(&("INSERT INTO ".to_owned()+IDENTITY_TABLE+" (uuid, name, key, salt, masterkey) VALUES (?1, ?2, ?3, ?4, ?5)"), vec![&uuid.as_bytes()[..], name.as_bytes(), encrypted_keypair.as_slice(), &salt, &encrypted_master_key]).unwrap();
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.set(DBKeys::NAME, name.as_bytes())?;
db.set(DBKeys::KEYPAIR, &encrypted_keypair)?;
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 {
uuid: uuid,
name: name.to_owned(),
key: keypair,
master_key: master_key
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 = 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)
}
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] = 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),
Err(e) => Err(e.to_string())
}
}
Err(e) => Err(e.to_string())
}
}
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())
}
}
impl Clone for Identity {
fn clone(&self) -> Self {
Identity {
name: self.name.clone(),
keypair: Keypair::from_bytes(&self.keypair.to_bytes()).unwrap(),
master_key: self.master_key,
use_padding: self.use_padding,
}
}
}

34
src/key_value_table.rs Normal file
View File

@ -0,0 +1,34 @@
use rusqlite::{Connection, Error, params};
pub struct KeyValueTable<'a> {
db: Connection,
table_name: &'a str,
}
impl<'a> KeyValueTable<'a> {
pub fn new(db_path: &str, table_name: &'a str) -> Result<KeyValueTable<'a>, Error> {
let db = Connection::open(db_path)?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS {} (key TEXT PRIMARY KEY, value BLOB)", table_name), [])?;
Ok(KeyValueTable {db, table_name})
}
pub fn set(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
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))?;
let mut rows = stmt.query([])?;
match rows.next()? {
Some(row) => Ok(row.get(0)?),
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), [])
}
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])
}
}

File diff suppressed because it is too large Load Diff

96
src/protocol.rs Normal file
View File

@ -0,0 +1,96 @@
use std::{convert::TryInto, str::from_utf8};
use crate::print_error;
pub struct Headers;
impl Headers {
pub const MESSAGE: u8 = 0x00;
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: &str) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat()
}
pub fn ask_profile_info() -> Vec<u8> {
vec![Headers::ASK_PROFILE_INFO]
}
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 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 {
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| {
buff.extend(&info.0.to_be_bytes());
buff.extend(&(info.1.len() as u16).to_be_bytes());
buff.extend(info.1);
});
buff
}
pub fn parse_ask_files(buffer: &[u8]) -> Option<Vec<(u64, String)>> {
let mut files_info = Vec::new();
let mut n = 1;
while n < buffer.len() {
if buffer[n..].len() > 10 { //8 + 2
let file_size = u64::from_be_bytes(buffer[n..n+8].try_into().unwrap());
let file_name_len = u16::from_be_bytes(buffer[n+8..n+10].try_into().unwrap()) as usize;
if buffer.len() >= n+10+file_name_len {
match from_utf8(&buffer[n+10..n+10+file_name_len]) {
Ok(file_name) => {
let file_name = sanitize_filename::sanitize(file_name);
files_info.push((file_size, file_name));
n += 10+file_name_len;
}
Err(e) => {
print_error!(e);
return None
}
}
} else {
return None
}
} else {
return None
}
}
Some(files_info)
}
pub fn avatar(avatar: &[u8]) -> Vec<u8> {
[&[Headers::AVATAR], avatar].concat()
}
pub fn remove_avatar() -> Vec<u8> {
vec![Headers::REMOVE_AVATAR]
}

853
src/session_manager.rs Normal file
View File

@ -0,0 +1,853 @@
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 uuid::Uuid;
use platform_dirs::UserDirs;
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 {
buff: Vec<u8>,
},
SendEncryptedFileChunk {
ack_sender: Sender<bool>,
},
EncryptFileChunk {
plain_text: Vec<u8>,
},
SendingEnded,
Close,
}
#[derive(Clone)]
pub struct LargeFileDownload {
pub file_name: String,
pub file_size: u64,
pub transferred: u64,
pub last_chunk: u128,
}
#[derive(Clone)]
pub struct LargeFilesDownload {
pub download_location: PathBuf,
pub accepted: bool,
pub index: usize,
pub files: Vec<LargeFileDownload>,
}
#[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,
sender: Sender<SessionCommand>,
pub files_download: Option<LargeFilesDownload>,
}
pub struct SessionManager {
session_counter: RwLock<usize>,
pub sessions: RwLock<HashMap<usize, SessionData>>,
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>>,
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<()>>>,
}
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();
if let Some(ui_connection) = ui_connection_opt.as_mut() {
if ui_connection.is_valid {
f(ui_connection);
}
}
}
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::from(stream), true);
Ok(())
}
pub fn store_msg(&self, session_id: &usize, message: Message) {
let mut msg_saved = false;
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;
},
Err(e) => print_error!(e),
}
}
if !msg_saved {
//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 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) -> bool {
if let Some(sender) = self.get_session_sender(session_id) {
match sender.send(session_command).await {
Ok(_) => true,
Err(e) => {
print_error!(e);
false
}
}
} else {
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)
}
}
fn remove_session(&self, session_id: &usize) {
self.with_ui_connection(|ui_connection| {
ui_connection.on_disconnected(&session_id);
});
self.sessions.write().unwrap().remove(session_id);
self.saved_msgs.write().unwrap().remove(session_id);
self.not_seen.write().unwrap().retain(|x| x != session_id);
}
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_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(())
}
async fn session_worker(&self, session_id: usize, mut receiver: Receiver<SessionCommand>, session: Session) {
//used when we receive large file
let mut local_file_handle = None;
//used when we send large file
let mut next_chunk: Option<Vec<u8>> = None;
let mut last_chunks_sizes: Option<Vec<u32>> = None;
let mut file_ack_sender: Option<Sender<bool>> = None;
let mut msg_queue = Vec::new();
let mut is_sending = false;
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.0 {
Ok(buffer) => {
let session_read = result.1;
receiving.set(session_read.into_receive_and_decrypt());
match buffer[0] {
protocol::Headers::LARGE_FILE_CHUNK => {
let mut should_accept_chunk = false;
{
let sessions = self.sessions.read().unwrap();
if let Some(files_transfer) = sessions.get(&session_id).unwrap().files_download.as_ref() {
if files_transfer.accepted {
if local_file_handle.is_some() {
should_accept_chunk = true;
} else {
let local_file_path = get_not_used_path(&files_transfer.files[files_transfer.index].file_name, &files_transfer.download_location);
match OpenOptions::new().append(true).create(true).open(local_file_path) {
Ok(file) => {
local_file_handle = Some(file);
should_accept_chunk = true;
}
Err(e) => print_error!(e)
}
}
}
}
}
if should_accept_chunk {
let mut is_success = false;
if let Some(file_handle) = local_file_handle.as_mut() {
match file_handle.write_all(&buffer[1..]) {
Ok(_) => {
let chunk_size = (buffer.len()-1) as u64;
{
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_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() {
sessions.get_mut(&session_id).unwrap().files_download = None;
} else {
files_transfer.index += 1;
}
local_file_handle = None;
}
}
if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ACK_CHUNK]).await {
print_error!(e);
break;
}
self.with_ui_connection(|ui_connection| {
ui_connection.inc_files_transfer(&session_id, chunk_size);
});
is_success = true;
}
Err(e) => print_error!(e)
}
}
if !is_success {
self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = None;
local_file_handle = None;
if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await {
print_error!(e);
break;
}
}
}
}
protocol::Headers::ACK_CHUNK => {
if let Some(ack_sender) = file_ack_sender.clone() {
if let Some(last_chunks_sizes) = last_chunks_sizes.as_mut() {
let chunk_size = last_chunks_sizes.remove(0);
self.with_ui_connection(|ui_connection| {
ui_connection.inc_files_transfer(&session_id, chunk_size.into());
});
}
if ack_sender.send(true).await.is_err() {
is_sending = false;
}
file_ack_sender = None;
}
}
protocol::Headers::ABORT_FILES_TRANSFER => {
if let Some(ack_sender) = file_ack_sender.clone() {
if let Err(e) = ack_sender.send(false).await {
print_error!(e);
}
is_sending = false;
file_ack_sender = None;
}
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_file_transfer_aborted(&session_id);
});
}
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);
break;
}
}
}
Err(e) => {
print_error!(e);
break;
}
}
}
}
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();
}
}
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);
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);
})
}
_ => {}
}
}
}
}
Err(e) => {
if e != PsecError::BrokenPipe && e != PsecError::ConnectionReset {
print_error!(e);
}
break;
}
}
}
command = receiver.recv() => {
match command.unwrap() {
SessionCommand::Send { buff } => {
//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;
}
}
SessionCommand::EncryptFileChunk { plain_text } => {
last_chunks_sizes.as_mut().unwrap().push(plain_text.len() as u32);
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.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.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 {
print_error!(e);
break;
}
}
}
Err(e) => {
print_error!(e);
break;
}
}
}
}
SessionCommand::SendingEnded => is_sending = false,
SessionCommand::Close => break
}
}
}
}
}
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 = {
session_manager.identity.read().unwrap().clone()
};
match identity {
Some(identity) => {
match session.do_handshake(&identity.keypair).await {
Ok(_) => {
peer_public_key = session.peer_public_key.unwrap();
if identity.get_public_key() != peer_public_key {
Some(session)
} else {
None
}
}
Err(e) => {
print_error!(e);
None
}
}
}
None => None
}
};
if let Some(mut session) = session {
let ip = session.peer_addr().unwrap().ip();
let mut is_contact = false;
let session_data = {
let mut sessions = session_manager.sessions.write().unwrap();
let mut is_new_session = true;
for (_, registered_session) in sessions.iter() {
if registered_session.peer_public_key == peer_public_key { //already connected to this identity
is_new_session = false;
break;
}
}
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 {
name: ip.to_string(),
avatar: None,
outgoing,
peer_public_key,
ip,
sender,
files_download: None,
};
let mut session_id = None;
for (i, contact) in session_manager.loaded_contacts.read().unwrap().iter() {
if contact.public_key == peer_public_key { //session is a known contact. Assign the contact session_id to it
sessions.insert(*i, session_data.clone());
is_contact = true;
session_id = Some(*i);
break;
}
}
if session_id.is_none() { //if not a contact, increment the session_counter
let mut session_counter = session_manager.session_counter.write().unwrap();
sessions.insert(*session_counter, session_data);
session_id = Some(*session_counter);
*session_counter += 1;
}
let session_id = session_id.unwrap();
session_manager.saved_msgs.write().unwrap().insert(session_id, Vec::new());
Some((session_id, receiver))
} else {
None
}
};
if let Some(session_data) = session_data {
let (session_id, receiver) = session_data;
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);
});
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.remove_session(&session_id);
}
}
});
}
pub async fn start_listener(session_manager: Arc<SessionManager>) -> io::Result<()> {
let server_v6 = TcpListener::bind(SocketAddr::new("::1".parse().unwrap(), constants::PORT)).await?;
let server_v4 = TcpListener::bind(SocketAddr::new("0.0.0.0".parse().unwrap(), constants::PORT)).await?;
let (sender, mut receiver) = mpsc::channel(1);
*session_manager.listener_stop_signal.lock().unwrap() = Some(sender);
match discovery::advertise_me().await {
Ok(service) => *session_manager.mdns_service.lock().unwrap() = Some(service),
Err(e) => {
print_error!("{}: you won't be discoverable by other peers.", e);
}
}
tokio::spawn(async move {
loop {
let (stream, _addr) = (tokio::select! {
client = server_v6.accept() => client,
client = server_v4.accept() => client,
_ = receiver.recv() => break
}).unwrap();
SessionManager::handle_new_session(session_manager.clone(), Session::from(stream), false);
}
});
Ok(())
}
pub fn list_contacts(&self) -> Vec<(usize, String, bool, [u8; PUBLIC_KEY_LENGTH])> {
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<Message>> {
self.saved_msgs.read().unwrap().clone()
}
pub fn set_seen(&self, session_id: usize, seen: bool) {
let mut not_seen = self.not_seen.write().unwrap();
if seen {
not_seen.retain(|i| i != &session_id)
} else if !not_seen.contains(&session_id) {
not_seen.push(session_id);
}
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
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)
}
}
}
}
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> {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
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) {
session.name = contact.name;
session.avatar = contact.avatar;
}
}
self.last_loaded_msg_offsets.write().unwrap().remove(session_id);
self.pending_msgs.lock().unwrap().remove(session_id);
}
result
}
pub fn set_verified(&self, session_id: &usize) -> Result<usize, rusqlite::Error> {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let contact = loaded_contacts.get_mut(session_id).unwrap();
let result = self.identity.read().unwrap().as_ref().unwrap().set_verified(&contact.uuid);
if result.is_ok() {
contact.verified = true;
}
result
}
pub fn delete_conversation(&self, session_id: usize) -> Result<usize, rusqlite::Error> {
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.write().unwrap().insert(session_id, Vec::new());
}
result
}
pub fn store_file(&self, session_id: &usize, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
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<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)?.uuid,
*offsets.get(session_id)?,
count
);
if let Some(msgs) = msgs.as_ref() {
*offsets.get_mut(session_id)? += msgs.len();
}
msgs
}
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 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;
}
}
#[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)]
pub async fn stop(&self) {
*self.mdns_service.lock().unwrap() = None; //unregister mdns service
let mut sender = self.listener_stop_signal.lock().unwrap();
if sender.is_some() {
sender.as_ref().unwrap().send(()).await;
*sender = None;
}
self.set_identity(None);
for session_data in self.sessions.read().unwrap().values() {
session_data.sender.send(SessionCommand::Close).await;
}
*self.ui_connection.lock().unwrap() = None;
*self.session_counter.write().unwrap() = 0;
self.loaded_contacts.write().unwrap().clear();
self.saved_msgs.write().unwrap().clear();
}
pub fn is_identity_loaded(&self) -> bool {
self.identity.read().unwrap().is_some()
}
pub fn set_identity(&self, identity: Option<Identity>) {
let mut identity_guard = self.identity.write().unwrap();
if identity.is_none() { //logout
identity_guard.as_mut().unwrap().zeroize();
}
*identity_guard = identity;
if identity_guard.is_some() { //login
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;
});
}
}
}
pub fn set_ui_connection(&self, ui_connection: UiConnection){
*self.ui_connection.lock().unwrap() = Some(ui_connection);
}
pub fn new() -> SessionManager {
SessionManager {
session_counter: RwLock::new(0),
sessions: RwLock::new(HashMap::new()),
identity: RwLock::new(None),
ui_connection: Mutex::new(None),
loaded_contacts: RwLock::new(HashMap::new()),
last_loaded_msg_offsets: RwLock::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,405 +0,0 @@
mod session;
pub mod protocol;
use std::{collections::HashMap, io::ErrorKind, net::{SocketAddr, TcpStream}, sync::{Arc, Mutex, MutexGuard, RwLock}, thread, thread::sleep, time::Duration};
use socket2::{Socket, Domain, Type};
use session::Session;
use strum_macros::Display;
use ed25519_dalek::PUBLIC_KEY_LENGTH;
use uuid::Uuid;
use zeroize::Zeroize;
use crate::{constants, identity::{Contact, Identity}, print_error};
use crate::ui_interface::UiConnection;
#[derive(Display, Debug, PartialEq, Eq)]
pub enum SessionError {
SocketTimeout,
ConnectionReset,
BrokenPipe,
TransmissionCorrupted,
BufferTooLarge,
ConnectFailed,
InvalidSessionId,
BindFailed,
AlreadyConnected,
IsUs,
Unknown
}
pub struct SessionManager {
session_counter: RwLock<usize>,
sessions: RwLock<HashMap<usize, Session>>,
identity: RwLock<Option<Identity>>,
ui_connection: Mutex<Option<Arc<Mutex<UiConnection>>>>,
loaded_contacts: RwLock<HashMap<usize, Contact>>,
msg_queue: Mutex<Vec<(usize, Vec<u8>)>>,
is_stopping: RwLock<bool>
}
impl SessionManager {
fn with_ui_connection<F>(&self, f: F) -> bool where F: Fn(MutexGuard<UiConnection>) {
let mut ui_connection_opt = self.ui_connection.lock().unwrap();
let ui_connection = ui_connection_opt.as_mut().unwrap().lock().unwrap();
if ui_connection.is_valid {
f(ui_connection);
true
} else {
false
}
}
fn do_handshake_then_add(&self, mut session: Session) -> Result<usize, SessionError>{
let identity_opt = self.identity.read().unwrap();
let identity = identity_opt.as_ref().unwrap();
session.do_handshake(identity)?;
let peer_public_key = session.peer_public_key.unwrap();
if identity.get_public_key() == peer_public_key { //did handshake with the same Identity
return Err(SessionError::IsUs);
}
let mut sessions = self.sessions.write().unwrap();
for (_, registered_session) in sessions.iter() {
if registered_session.peer_public_key.unwrap() == peer_public_key { //already connected with a different addr
return Err(SessionError::AlreadyConnected)
}
}
for (index, contact) in self.loaded_contacts.read().unwrap().iter() {
if contact.public_key == peer_public_key { //session is a known contact. Assign the contact index to it
sessions.insert(*index, session);
return Ok(*index)
}
}
//if not a contact, increment the session_counter
let mut session_counter = self.session_counter.write().unwrap();
sessions.insert(*session_counter, session);
let r = *session_counter;
*session_counter += 1;
Ok(r)
}
pub fn connect_to(&self, ip: &str) -> Result<usize, SessionError> {
let sessions = self.sessions.read().unwrap();
for (_, s) in sessions.iter() {
if s.get_ip() == ip {
return Err(SessionError::AlreadyConnected)
}
}
drop(sessions); //release mutex
match TcpStream::connect((ip, constants::PORT.parse().unwrap())) {
Ok(stream) => {
let session = Session::new(stream);
self.do_handshake_then_add(session)
}
Err(_) => Err(SessionError::ConnectFailed)
}
}
pub fn send_to(&self, index: &usize, message: &[u8]) -> Result<(), SessionError> {
let mut sessions = self.sessions.write().unwrap();
match sessions.get_mut(index) {
Some(session) => session.encrypt_and_send(message),
None => Err(SessionError::InvalidSessionId)
}
}
pub fn start_receiver_loop(session_manager: &Arc<SessionManager>) {
let session_manager_clone = Arc::clone(session_manager);
thread::spawn(move || {
loop {
let mut dead_sessions = Vec::new();
let mut sessions = session_manager_clone.sessions.write().unwrap();
for (index, session) in sessions.iter_mut() {
let mut dead_session = false;
match session.receive_and_decrypt() {
Ok(buffer) => {
if buffer[0] == protocol::Headers::ASK_NAME {
session.encrypt_and_send(&protocol::tell_name(&session_manager_clone.identity.read().unwrap().as_ref().unwrap().name)).unwrap();
} else {
let buffer = if buffer[0] == protocol::Headers::FILE {
let file_name_len = u16::from_be_bytes([buffer[1], buffer[2]]) as usize;
let file_name = &buffer[3..3+file_name_len];
match session_manager_clone.store_file(index, &buffer[3+file_name_len..]) {
Ok(file_uuid) => {
Some([&[protocol::Headers::FILE][..], file_uuid.as_bytes(), file_name].concat())
}
Err(e) => {
print_error(e);
None
}
}
} else {
Some(buffer)
};
if buffer.is_some() {
let mut msg_saved = false;
if session_manager_clone.is_contact(index) && buffer.as_ref().unwrap()[0] != protocol::Headers::TELL_NAME {
match session_manager_clone.store_msg(&index, false, &buffer.as_ref().unwrap()) {
Ok(_) => msg_saved = true,
Err(e) => print_error(e)
}
}
let ui_connection_valid = session_manager_clone.with_ui_connection(|mut ui_connection| {
ui_connection.on_received(index, &buffer.as_ref().unwrap());
});
if !ui_connection_valid && !msg_saved {
session_manager_clone.msg_queue.lock().unwrap().push((*index, buffer.unwrap()));
}
}
}
}
Err(e) => {
if e == SessionError::BrokenPipe {
dead_session = true
} else if e != SessionError::SocketTimeout {
print_error(e);
}
}
}
if dead_session {
session_manager_clone.with_ui_connection(|mut ui_connection| {
ui_connection.on_disconnected(*index);
});
dead_sessions.push(*index);
}
}
dead_sessions.into_iter().for_each(|index| {
sessions.remove(&index);
});
drop(sessions); //release mutex
if *session_manager_clone.is_stopping.read().unwrap() {
break;
}
sleep(Duration::from_millis(constants::MUTEX_RELEASE_DELAY_MS));
}
println!("Stopping receiver thread");
});
}
pub fn start_listener(session_manager: &Arc<SessionManager>) -> Result<(), SessionError> {
let socket_v6 = Socket::new(Domain::ipv6(), Type::stream(), None).unwrap();
let socket_v4 = Socket::new(Domain::ipv4(), Type::stream(), None).unwrap();
socket_v4.set_reuse_address(true).unwrap();
socket_v6.set_reuse_address(true).unwrap();
let addr_v6 = "[::1]:".to_owned()+constants::PORT;
let addr_v4 = "0.0.0.0:".to_owned()+constants::PORT;
let mut sockets = Vec::new();
match socket_v6.bind(&addr_v6.parse::<SocketAddr>().unwrap().into()) {
Ok(_) => sockets.push(socket_v6),
Err(e) => println!("Unable to bind on IPv6: {}", e)
};
match socket_v4.bind(&addr_v4.parse::<SocketAddr>().unwrap().into()) {
Ok(_) => sockets.push(socket_v4),
Err(e) => println!("Unable to bind on IPv4: {}", e)
}
if sockets.len() > 0 {
println!("Listening on port {}...", constants::PORT);
for socket in sockets {
socket.listen(256).unwrap();
socket.set_read_timeout(Some(Duration::from_millis(100))).unwrap();
let session_manager_clone = Arc::clone(session_manager);
thread::spawn(move ||{
for stream in socket.into_tcp_listener().incoming() {
match stream {
Ok(stream) => {
let session = Session::new(stream);
match session_manager_clone.do_handshake_then_add(session) {
Ok(index) => {
session_manager_clone.with_ui_connection(|mut ui_connection| {
ui_connection.on_new_session(index);
session_manager_clone.handle_new_session(&index, ui_connection);
});
}
Err(e) => {
if e != SessionError::AlreadyConnected && e != SessionError::IsUs {
print_error(e);
}
}
}
}
Err(e) => {
if e.kind() != ErrorKind::WouldBlock {
print_error(e);
}
}
}
if *session_manager_clone.is_stopping.read().unwrap() {
break;
}
}
println!("Stopping listener thread");
});
}
Ok(())
} else {
Err(SessionError::BindFailed)
}
}
pub fn handle_new_session(&self, index: &usize, mut ui_connection: MutexGuard<UiConnection>) {
if self.is_contact(index) {
match self.load_msgs(index) {
Some(msgs) => {
ui_connection.load_msgs(index, msgs);
}
None => {}
}
} else {
match self.ask_name_to(&index) {
Ok(_) => {}
Err(e) => print_error(e)
}
}
}
pub fn list_sessions(&self) -> Vec<usize> {
let sessions = self.sessions.read().unwrap();
sessions.iter().map(|t| *t.0).collect()
}
pub fn list_contacts(&self) -> Vec<(usize, String, bool)> {
self.loaded_contacts.read().unwrap().iter().map(|c| (*c.0, c.1.name.clone(), c.1.verified)).collect()
}
pub fn get_identity_uuid(&self) -> Option<Uuid> {
Some(self.identity.read().unwrap().as_ref()?.uuid)
}
pub fn get_saved_msgs(&self) -> Vec<(usize, Vec<u8>)> {
let mut msgs = Vec::new();
let mut msg_queue = self.msg_queue.lock().unwrap();
let sessions = self.sessions.read().unwrap();
for i in 0..msg_queue.len() {
let mut entry = msg_queue.remove(i);
if sessions.contains_key(&entry.0) {
msgs.push(entry);
} else {
entry.1.zeroize();
}
};
msgs
}
fn ask_name_to(&self, index: &usize) -> Result<(), SessionError> {
let mut sessions = self.sessions.write().unwrap();
match sessions.get_mut(index) {
Some(session) => {
session.encrypt_and_send(&protocol::ask_name())
},
None => Err(SessionError::InvalidSessionId)
}
}
pub fn get_peer_public_key(&self, index: &usize) -> Option<[u8; PUBLIC_KEY_LENGTH]> {
let sessions = self.sessions.read().unwrap();
let session = sessions.get(index)?;
session.peer_public_key
}
pub fn add_contact(&self, index: usize, name: String) -> Result<(), rusqlite::Error> {
let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(name, self.get_peer_public_key(&index).unwrap())?;
self.loaded_contacts.write().unwrap().insert(index, contact);
Ok(())
}
pub fn remove_contact(&self, index: &usize) -> Result<(), rusqlite::Error> {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let result = self.identity.read().unwrap().as_ref().unwrap().remove_contact(&loaded_contacts.get(index).unwrap().uuid);
if result.is_ok() {
loaded_contacts.remove(index);
}
result
}
pub fn set_verified(&self, index: &usize) -> Result<(), rusqlite::Error> {
let mut loaded_contacts = self.loaded_contacts.write().unwrap();
let contact = loaded_contacts.get_mut(index).unwrap();
let result = self.identity.read().unwrap().as_ref().unwrap().set_verified(&contact.uuid);
if result.is_ok() {
contact.verified = true;
}
result
}
pub fn is_contact(&self, index: &usize) -> bool {
self.loaded_contacts.read().unwrap().contains_key(index)
}
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, index: &usize, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
self.identity.read().unwrap().as_ref().unwrap().store_file(match self.loaded_contacts.read().unwrap().get(index) {
Some(contact) => Some(contact.uuid),
None => None
}, data)
}
pub fn store_msg(&self, index: &usize, outgoing: bool, data: &[u8]) -> Result<(), rusqlite::Error> {
self.identity.read().unwrap().as_ref().unwrap().store_msg(&self.loaded_contacts.read().unwrap().get(index).unwrap().uuid, outgoing, data)
}
pub fn load_msgs(&self, index: &usize) -> Option<Vec<(bool, Vec<u8>)>> {
self.identity.read().unwrap().as_ref().unwrap().load_msgs(&self.loaded_contacts.read().unwrap().get(index).unwrap().uuid)
}
pub fn get_public_keys(&self, index: &usize) -> ([u8; PUBLIC_KEY_LENGTH], [u8; PUBLIC_KEY_LENGTH]) {
(self.identity.read().unwrap().as_ref().unwrap().get_public_key(), self.loaded_contacts.read().unwrap().get(index).unwrap().public_key)
}
fn clear_identity_related_data(&self){
self.loaded_contacts.write().unwrap().clear();
let mut msg_queue = self.msg_queue.lock().unwrap();
msg_queue.iter_mut().for_each(|m| m.1.zeroize());
msg_queue.clear();
}
pub fn stop(&self) {
*self.is_stopping.write().unwrap() = true;
self.set_identity(None);
*self.ui_connection.lock().unwrap() = None;
}
pub fn set_identity(&self, identity: Option<Identity>) {
let mut identity_guard = self.identity.write().unwrap();
match identity_guard.as_mut() {
Some(previous_identity) => {
previous_identity.zeroize();
self.sessions.write().unwrap().clear();
*self.session_counter.write().unwrap() = 0;
self.clear_identity_related_data();
}
None => {}
}
*identity_guard = identity;
if identity_guard.is_some() {
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();
contacts.into_iter().for_each(|contact|{
loaded_contacts.insert(*session_counter, contact);
*session_counter += 1;
})
}
None => {}
}
}
}
pub fn set_ui_connection(&self, ui_connection: &Arc<Mutex<UiConnection>>){
*self.ui_connection.lock().unwrap() = Some(ui_connection.clone());
}
pub fn new() -> SessionManager {
SessionManager {
session_counter: RwLock::new(0),
sessions: RwLock::new(HashMap::new()),
identity: RwLock::new(None),
ui_connection: Mutex::new(None),
loaded_contacts: RwLock::new(HashMap::new()),
msg_queue: Mutex::new(Vec::new()),
is_stopping: RwLock::new(false)
}
}
}

View File

@ -1,24 +0,0 @@
pub struct Headers;
impl Headers {
pub const MESSAGE: u8 = 0x01;
pub const ASK_NAME: u8 = 0x02;
pub const TELL_NAME: u8 = 0x03;
pub const FILE: u8 = 0x04;
}
pub fn new_message(message: String) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat()
}
pub fn ask_name() -> Vec<u8> {
vec![Headers::ASK_NAME]
}
pub fn tell_name(name: &str) -> Vec<u8> {
[&[Headers::TELL_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()
}

View File

@ -1,237 +0,0 @@
use std::{io::{Read, Write, ErrorKind}, net::{TcpStream}, option::Option, result::Result, time::Duration};
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, aead::generic_array::GenericArray};
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;
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]>,
//pub name: Option<String>,
//pub message_queue: Vec<Vec<u8>>,
}
impl Session {
pub fn new(stream: TcpStream) -> Session {
stream.set_read_timeout(Some(Duration::from_millis(1000))).unwrap();
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 get_ip(&self) -> String {
self.stream.peer_addr().unwrap().ip().to_string()
}
fn socket_read(&mut self, buff: &mut [u8]) -> Result<usize, SessionError> {
match self.stream.read(buff) {
Ok(read) => {
if read > 0 {
return Ok(read);
} else if read == 0 {
return Err(SessionError::BrokenPipe);
}
}
Err(e) => {
match e.kind() {
ErrorKind::WouldBlock => return Err(SessionError::SocketTimeout),
ErrorKind::ConnectionReset => return Err(SessionError::ConnectionReset),
_ => print_error(format!("Receive error: {}", e))
}
}
};
Err(SessionError::Unknown)
}
fn socket_write(&mut self, buff: &[u8]) -> Result<(), SessionError> {
match self.stream.write(buff) {
Ok(sent) => {
if buff.len() == sent {
return Ok(());
} else if sent == 0 {
return Err(SessionError::BrokenPipe);
}
}
Err(e) => print_error(format!("Send error: {}", e))
};
Err(SessionError::Unknown)
}
fn handshake_read(&mut self, buff: &mut [u8]) -> Result<(), SessionError> {
match self.socket_read(buff) {
Ok(_) => {
self.handshake_recv_buff.extend(buff.as_ref());
Ok(())
}
Err(e) => Err(e)
}
}
fn handshake_write(&mut self, buff: &[u8]) -> Result<(), SessionError> {
match self.socket_write(buff) {
Ok(_) => {
self.handshake_sent_buff.extend(buff);
Ok(())
}
Err(e) => Err(e)
}
}
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_varkey(&application_keys.local_key).unwrap());
self.local_iv = Some(application_keys.local_iv);
self.peer_cipher = Some(Aes128Gcm::new_varkey(&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 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)?;
self.handshake_read(&mut handshake_buffer)?;
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)?;
//receive peer random bytes
let mut peer_random = [0; RANDOM_LEN];
self.handshake_read(&mut 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_varkey(&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(GenericArray::from_slice(&nonce), auth_msg.as_ref()).unwrap();
self.handshake_write(&encrypted_auth_msg)?;
let mut encrypted_peer_auth_msg = [0; PUBLIC_KEY_LENGTH+SIGNATURE_LENGTH+AES_TAG_LEN];
self.handshake_read(&mut encrypted_peer_auth_msg)?;
//decrypt peer_auth_msg
let peer_cipher = Aes128Gcm::new_varkey(&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(GenericArray::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)?;
let mut peer_handshake_finished = [0; HASH_OUTPUT_LEN];
self.socket_read(&mut peer_handshake_finished)?;
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 fn encrypt_and_send(&mut self, message: &[u8]) -> Result<(), SessionError> {
let cipher_len = (message.len() as MessageLenType).to_be_bytes();
let payload = Payload {
msg: message,
aad: &cipher_len
};
let nonce = iv_to_nonce(&self.local_iv.unwrap(), &mut self.local_counter);
println!("nonce: {}", hex::encode(&nonce));
let cipher_text = self.local_cipher.as_ref().unwrap().encrypt(GenericArray::from_slice(&nonce), payload).unwrap();
self.socket_write([&cipher_len, cipher_text.as_slice()].concat().as_slice())
}
pub fn receive_and_decrypt(&mut self) -> Result<Vec<u8>, SessionError> {
let mut message_len = [0; MESSAGE_LEN_LEN];
self.socket_read(&mut message_len)?;
let recv_len = MessageLenType::from_be_bytes(message_len) as usize + AES_TAG_LEN;
if recv_len <= 50000000 {
let mut cipher_text = vec![0; recv_len];
let mut read = 0;
while read < recv_len {
read += self.socket_read(&mut cipher_text[read..])?;
}
let peer_nonce = iv_to_nonce(&self.peer_iv.unwrap(), &mut self.peer_counter);
let peer_cipher = self.peer_cipher.as_ref().unwrap();
let payload = Payload {
msg: &cipher_text,
aad: &message_len
};
match peer_cipher.decrypt(GenericArray::from_slice(&peer_nonce), payload) {
Ok(plain_text) => Ok(plain_text),
Err(_) => Err(SessionError::TransmissionCorrupted)
}
} else {
Err(SessionError::BufferTooLarge)
}
}
}

View File

@ -1,87 +1,7 @@
use std::net::TcpStream;
use tungstenite::{Error, WebSocket};
use crate::{protocol};
mod ui_messages {
use std::str::from_utf8;
use tungstenite::Message;
use uuid::Uuid;
use crate::{print_error, session_manager::protocol, utils::to_uuid_bytes};
pub fn on_connected(index: usize) -> Message {
Message::from(format!("connected {}", index))
}
pub fn on_disconnected(index: usize) -> Message {
Message::from(format!("disconnected {}", index))
}
pub fn on_new_session(index: usize) -> Message {
Message::from(format!("new_session {}", index))
}
pub fn on_new_message(index: &usize, raw_message: &[u8]) -> Option<Message> {
match from_utf8(raw_message) {
Ok(msg) => Some(Message::from(format!("new_message {} {}", index, msg))),
Err(e) => {
print_error(e);
None
}
}
}
pub fn on_file_received(index: &usize, buffer: &[u8]) -> Option<Message> {
let uuid = Uuid::from_bytes(to_uuid_bytes(&buffer[..16])?);
match from_utf8(&buffer[16..]) {
Ok(file_name) => Some(Message::from(format!("file {} {} {}", index, uuid.to_string(), file_name))),
Err(e) => {
print_error(e);
None
}
}
}
pub fn load_sent_msg(index: &usize, buffer: &[u8]) -> Option<Message> {
match buffer[0] {
protocol::Headers::MESSAGE => {
match from_utf8(&buffer[1..]) {
Ok(msg) => Some(Message::from(format!("load_sent_msg {} {}", index, msg))),
Err(e) => {
print_error(e);
None
}
}
}
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 {} {} {}", index, uuid.to_string(), file_name))),
Err(e) => {
print_error(e);
None
}
}
}
_ => None
}
}
pub fn on_name_told(index: &usize, raw_name: &[u8]) -> Option<Message> {
match from_utf8(raw_name) {
Ok(name) => Some(Message::from(format!("name_told {} {}", index, name))),
Err(e) => {
print_error(e);
None
}
}
}
pub fn set_as_contact(index: usize, name: &str, verified: bool) -> Message {
Message::from(format!("is_contact {} {} {}", index, verified, name))
}
pub fn fingerprints(local: &str, peer: &str) -> Message {
Message::from(format!("fingerprints {} {}", local, peer))
}
pub fn file_sent(index: usize, file_name: &str, uuid: Option<String>) -> Message {
Message::from(format!("file_sent {} {} {}", index, match uuid {
Some(uuid) => uuid,
None => "None".to_owned()
}, file_name))
}
}
use std::{fmt::Display, net::{IpAddr, TcpStream}, str::from_utf8};
use tungstenite::{WebSocket, protocol::Role, Message};
use uuid::Uuid;
use crate::{identity, print_error, protocol, session_manager::{LargeFileDownload, LargeFilesDownload}, utils::to_uuid_bytes};
pub struct UiConnection{
pub websocket: WebSocket<TcpStream>,
@ -91,63 +11,146 @@ pub struct UiConnection{
impl UiConnection {
pub fn new(websocket: WebSocket<TcpStream>) -> UiConnection {
UiConnection {
websocket: websocket,
websocket,
is_valid: true
}
}
pub fn on_received(&mut self, index: &usize, buffer: &[u8]) {
let ui_message = match buffer[0] {
protocol::Headers::MESSAGE => {
ui_messages::on_new_message(index, &buffer[1..])
}
protocol::Headers::TELL_NAME => {
ui_messages::on_name_told(index, &buffer[1..])
}
protocol::Headers::FILE => {
ui_messages::on_file_received(index, &buffer[1..])
}
_ => None
};
if ui_message.is_some() {
self.websocket.write_message(ui_message.unwrap()).unwrap();
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_new_session(&mut self, index: usize) {
self.websocket.write_message(ui_messages::on_new_session(index)).unwrap();
fn simple_event(&mut self, command: &str, session_id: &usize) {
self.write_message(format!("{} {}", command, session_id));
}
pub fn on_disconnected(&mut self, index: usize) {
self.websocket.write_message(ui_messages::on_disconnected(index)).unwrap();
}
pub fn on_connected(&mut self, index: usize) {
self.websocket.write_message(ui_messages::on_connected(index)).unwrap();
fn data_list<T: Display>(command: &str, data: &[T]) -> String {
command.to_string()+&data.iter().map(|i| {
format!(" {}", i)
}).collect::<String>()
}
pub fn read_message(&mut self) -> Result<String, Error> {
Ok(self.websocket.read_message()?.into_text()?)
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 read_binary(&mut self) -> Result<Vec<u8>, Error> {
Ok(self.websocket.read_message()?.into_data())
pub fn on_large_files_accepted(&mut self, session_id: &usize) {
self.simple_event("files_accepted", session_id);
}
pub fn set_as_contact(&mut self, index: usize, name: &str, verified: bool) {
self.websocket.write_message(ui_messages::set_as_contact(index, name, verified)).unwrap();
pub fn on_file_transfer_aborted(&mut self, session_id: &usize) {
self.simple_event("aborted", session_id);
}
pub fn load_msgs(&mut self, index: &usize, msgs: Vec<(bool, Vec<u8>)>) {
msgs.into_iter().for_each(|msg| {
if msg.0 { //outgoing
match ui_messages::load_sent_msg(index, &msg.1) {
Some(msg) => self.websocket.write_message(msg).unwrap(),
None => {}
}
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_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(format!("new_session {} {} {} {} {}", session_id, outgoing, fingerprint, ip, name));
if let Some(files_transfer) = 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_received(index, &msg.1)
self.on_ask_large_files(session_id, &files_transfer.files, files_transfer.download_location.to_str().unwrap())
}
})
}
}
pub fn fingerprints(&mut self, local: &str, peer: &str) {
self.websocket.write_message(ui_messages::fingerprints(local, peer)).unwrap();
pub fn on_disconnected(&mut self, session_id: &usize) {
self.simple_event("disconnected", session_id);
}
pub fn file_sent(&mut self, index: usize, name: &str, uuid: Option<String>) {
self.websocket.write_message(ui_messages::file_sent(index, name, uuid)).unwrap()
pub fn on_name_told(&mut self, session_id: &usize, name: &str) {
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(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(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name));
}
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: &[usize]) {
self.write_message(Self::data_list("not_seen", session_ids));
}
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(format!("set_name {}", new_name));
}
pub fn password_changed(&mut self, success: bool, is_protected: bool) {
self.write_message(format!("password_changed {} {}", success, is_protected));
}
pub fn logout(&mut self) {
self.write_message("logout");
}
}
impl Clone for UiConnection {
fn clone(&self) -> Self {
UiConnection {
websocket: WebSocket::from_raw_socket(self.websocket.get_ref().try_clone().unwrap(), Role::Server, None),
is_valid: self.is_valid
}
}
}

View File

@ -1,21 +1,12 @@
use std::convert::TryInto;
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),
Err(e) => {
print_error(e);
print_error!(e);
None
}
}
@ -23,4 +14,38 @@ pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
pub fn escape_double_quote(origin: String) -> String {
origin.replace("\"", "\\\"")
}
pub fn get_unix_timestamp_ms() -> u128 {
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis()
}
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;
while path.exists() {
path = if has_extension {
let splits: Vec<&str> = file_name.split('.').collect();
parent_directory.join(format!("{} ({}).{}", splits[..splits.len()-1].join("."), n, splits[splits.len()-1]))
} else {
parent_directory.join(format!("{} ({})", file_name, n))
};
n += 1;
}
path.to_str().unwrap().to_owned()
}
#[macro_export]
macro_rules! print_error {
($arg:tt) => ({
println!("[{}:{}] {}", file!(), line!(), $arg);
});
($($arg:tt)*) => ({
println!("[{}:{}] {}", file!(), line!(), format_args!($($arg)*));
})
}