From ec5034cb62ebc4d3b0b7524cb4353187cf7c7b56 Mon Sep 17 00:00:00 2001 From: rictorlome Date: Sat, 12 Feb 2022 14:59:04 -0500 Subject: [PATCH] File negotiation and crypto --- README.md | 24 +++++---- src/client.rs | 132 +++++++++++++++++++++++++++++++++++++++++++------ src/crypto.rs | 25 +++++----- src/file.rs | 21 ++++---- src/main.rs | 3 ++ src/message.rs | 46 ++++++++++++++++- src/server.rs | 3 -- src/tests.rs | 11 +++++ 8 files changed, 211 insertions(+), 54 deletions(-) create mode 100644 src/tests.rs diff --git a/README.md b/README.md index 0c5a952..d556140 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ -https://github.com/mcginty/snow/tree/master/examples -https://github.com/black-binary/snowstorm/tree/master/examples -https://cryptoservices.github.io/cryptography/protocols/2016/04/27/noise-protocol.html +- https://github.com/mcginty/snow/tree/master/examples +- https://github.com/black-binary/snowstorm/tree/master/examples +- https://cryptoservices.github.io/cryptography/protocols/2016/04/27/noise-protocol.html -https://github.com/libp2p +- https://github.com/libp2p -https://docs.rs/spake2/latest/spake2/ -https://crates.io/crates/chacha20poly1305 -https://briansmith.org/rustdoc/ring/aead/index.html -https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream +- https://docs.rs/spake2/latest/spake2/ +- https://crates.io/crates/chacha20poly1305 +- https://briansmith.org/rustdoc/ring/aead/index.html +- https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream -https://kerkour.com/rust-symmetric-encryption-aead-benchmark/ -https://docs.rs/aes-gcm/latest/aes_gcm/ +- https://kerkour.com/rust-symmetric-encryption-aead-benchmark/ +- https://docs.rs/aes-gcm/latest/aes_gcm/ + + +- https://github.com/rust-lang/flate2-rs +- https://crates.io/crates/async-compression diff --git a/src/client.rs b/src/client.rs index 203d12b..ef63dd5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,14 +1,17 @@ use crate::crypto::handshake; -use crate::file::FileHandle; -use crate::message::{Message, MessageStream}; +use crate::file::{to_size_string, FileHandle, FileInfo}; +use crate::message::{EncryptedMessage, FileNegotiationPayload, Message, MessageStream}; -use anyhow::Result; +use aes_gcm::Aes256Gcm; +use anyhow::{anyhow, Result}; use blake2::{Blake2s256, Digest}; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::future::join_all; +use bytes::{Bytes, BytesMut}; +use futures::future::try_join_all; use futures::prelude::*; use std::path::PathBuf; +use tokio::io; use tokio::net::TcpStream; +use tokio_util::codec::{FramedRead, LinesCodec}; fn pass_to_bytes(password: &String) -> Bytes { let mut hasher = Blake2s256::new(); @@ -18,17 +21,15 @@ fn pass_to_bytes(password: &String) -> Bytes { } pub async fn send(file_paths: &Vec, password: &String) -> Result<()> { - let tasks = file_paths - .into_iter() - .map(|path| FileHandle::new(path.to_path_buf()).map(|f| f.map(|s| s.to_file_info()))); - let metadatas = join_all(tasks).await; - println!("mds: {:?}", metadatas); - return Ok(()); + // Fail early if there are problems generating file handles + let handles = get_file_handles(file_paths).await?; + // Establish connection to server let socket = TcpStream::connect("127.0.0.1:8080").await?; let mut stream = Message::to_stream(socket); - let (stream, key) = handshake( + // Complete handshake, returning cipher used for encryption + let (stream, cipher) = handshake( &mut stream, true, Bytes::from(password.to_string()), @@ -36,22 +37,102 @@ pub async fn send(file_paths: &Vec, password: &String) -> Result<()> { ) .await?; - // return upload_encrypted_files(stream, file_paths, key).await; + // Complete file negotiation + let handles = negotiate_files_up(handles, stream, &cipher).await?; + + // Upload negotiated files + + // Exit + Ok(()) } pub async fn receive(password: &String) -> Result<()> { let socket = TcpStream::connect("127.0.0.1:8080").await?; let mut stream = Message::to_stream(socket); - let (stream, key) = handshake( + let (stream, cipher) = handshake( &mut stream, false, Bytes::from(password.to_string()), pass_to_bytes(password), ) .await?; + + let files = negotiate_files_down(stream, &cipher).await?; return Ok(()); } +pub async fn get_file_handles(file_paths: &Vec) -> Result> { + let tasks = file_paths + .into_iter() + .map(|path| FileHandle::new(path.to_path_buf())); + let handles = try_join_all(tasks).await?; + Ok(handles) +} + +pub async fn negotiate_files_up( + file_handles: Vec, + stream: &mut MessageStream, + cipher: &Aes256Gcm, +) -> Result> { + let files = file_handles.iter().map(|fh| fh.to_file_info()).collect(); + let msg = EncryptedMessage::FileNegotiationMessage(FileNegotiationPayload { files }); + let server_msg = msg.to_encrypted_message(cipher)?; + stream.send(server_msg).await?; + let reply_payload = match stream.next().await { + Some(Ok(msg)) => match msg { + Message::EncryptedMessage(response) => response, + _ => return Err(anyhow!("Expecting encrypted message back")), + }, + _ => { + return Err(anyhow!("No response to negotiation message")); + } + }; + let plaintext_reply = EncryptedMessage::from_encrypted_message(cipher, &reply_payload)?; + let requested_paths: Vec = match plaintext_reply { + EncryptedMessage::FileNegotiationMessage(fnm) => { + fnm.files.into_iter().map(|f| f.path).collect() + } + _ => return Err(anyhow!("Expecting file negotiation message back")), + }; + Ok(file_handles + .into_iter() + .filter(|fh| requested_paths.contains(&fh.path)) + .collect()) +} + +pub async fn negotiate_files_down(stream: &mut MessageStream, cipher: &Aes256Gcm) -> Result<()> { + let file_offer = match stream.next().await { + Some(Ok(msg)) => match msg { + Message::EncryptedMessage(response) => response, + _ => return Err(anyhow!("Expecting encrypted message back")), + }, + _ => { + return Err(anyhow!("No response to negotiation message")); + } + }; + let plaintext_offer = EncryptedMessage::from_encrypted_message(cipher, &file_offer)?; + let requested_infos: Vec = match plaintext_offer { + EncryptedMessage::FileNegotiationMessage(fnm) => fnm.files, + _ => return Err(anyhow!("Expecting file negotiation message back")), + }; + let mut stdin = FramedRead::new(io::stdin(), LinesCodec::new()); + let mut files = vec![]; + for path in requested_infos.into_iter() { + let mut reply = prompt_user_input(&mut stdin, &path).await; + while reply.is_none() { + reply = prompt_user_input(&mut stdin, &path).await; + } + match reply { + Some(true) => files.push(path), + _ => {} + } + } + let msg = EncryptedMessage::FileNegotiationMessage(FileNegotiationPayload { files }); + let server_msg = msg.to_encrypted_message(cipher)?; + stream.send(server_msg).await?; + Ok(()) +} + pub async fn upload_encrypted_files( stream: &mut MessageStream, file_paths: &Vec, @@ -59,3 +140,26 @@ pub async fn upload_encrypted_files( ) -> Result<()> { return Ok(()); } + +pub async fn prompt_user_input( + stdin: &mut FramedRead, + file_info: &FileInfo, +) -> Option { + let prompt_name = file_info.path.file_name().unwrap(); + println!( + "Do you want to download {:?}? It's {:?}. (Y/n)", + prompt_name, + to_size_string(file_info.size) + ); + match stdin.next().await { + Some(Ok(line)) => match line.as_str() { + "" | "Y" | "y" | "yes" | "Yes" | "YES" => Some(true), + "N" | "n" | "NO" | "no" | "No" => Some(false), + _ => { + println!("Invalid input. Please enter one of the following characters: [YyNn]"); + return None; + } + }, + _ => None, + } +} diff --git a/src/crypto.rs b/src/crypto.rs index c6b4ad4..789175a 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -13,7 +13,7 @@ pub async fn handshake( up: bool, password: Bytes, id: Bytes, -) -> Result<(&mut MessageStream, Bytes)> { +) -> Result<(&mut MessageStream, Aes256Gcm)> { let (s1, outbound_msg) = Spake2::::start_symmetric(&Password::new(password), &Identity::new(&id)); println!("client - sending handshake msg"); @@ -39,34 +39,33 @@ pub async fn handshake( Err(e) => return Err(anyhow!(e.to_string())), }; println!("Handshake successful. Key is {:?}", key); - return Ok((stream, Bytes::from(key))); + return Ok((stream, new_cipher(&key))); } -pub fn new_cypher(key: Bytes) -> Aes256Gcm { +pub fn new_cipher(key: &Vec) -> Aes256Gcm { let key = Key::from_slice(&key[..]); Aes256Gcm::new(key) } -const NONCE_SIZE_IN_BYTES: usize = 96 / 8; - -pub fn encrypt(cipher: &Aes256Gcm, body: Bytes) -> Result { +pub const NONCE_SIZE_IN_BYTES: usize = 96 / 8; +pub fn encrypt(cipher: &Aes256Gcm, body: &Vec) -> Result { let mut arr = [0u8; NONCE_SIZE_IN_BYTES]; thread_rng().try_fill(&mut arr[..])?; let nonce = Nonce::from_slice(&arr); let plaintext = body.as_ref(); match cipher.encrypt(nonce, plaintext) { - Ok(ciphertext) => Ok(EncryptedPayload { - nonce: Bytes::from(&arr[..]), - body: Bytes::from(ciphertext.as_ref()), + Ok(body) => Ok(EncryptedPayload { + nonce: arr.to_vec(), + body, }), - Err(_) => anyhow!("Encryption error"), + Err(_) => Err(anyhow!("Encryption error")), } } -pub fn decrypt(cipher: &Aes256Gcm, payload: EncryptedPayload) -> Result { +pub fn decrypt(cipher: &Aes256Gcm, payload: &EncryptedPayload) -> Result { let nonce = Nonce::from_slice(payload.nonce.as_ref()); match cipher.decrypt(nonce, payload.body.as_ref()) { - Ok(_) => Ok(Bytes::from("hello")), - Err(_) => anyhow!("Decryption error"), + Ok(payload) => Ok(Bytes::from(payload)), + Err(_) => Err(anyhow!("Decryption error")), } } diff --git a/src/file.rs b/src/file.rs index d6e36be..79f6c02 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,20 +1,19 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; -use std::ffi::OsString; use std::fs::Metadata; use std::path::PathBuf; use tokio::fs::File; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FileInfo { - name: OsString, - size: u64, + pub path: PathBuf, + pub size: u64, } pub struct FileHandle { - file: File, - md: Metadata, - path: PathBuf, + pub file: File, + pub md: Metadata, + pub path: PathBuf, } impl FileHandle { @@ -27,10 +26,7 @@ impl FileHandle { pub fn to_file_info(&self) -> FileInfo { FileInfo { - name: match self.path.file_name() { - Some(s) => s.to_os_string(), - None => OsString::from("Unknown"), - }, + path: self.path.clone(), size: self.md.len(), } } @@ -38,7 +34,8 @@ impl FileHandle { const SUFFIX: [&'static str; 9] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; // Stolen: https://gitlab.com/forkbomb9/human_bytes-rs/-/blob/master/src/lib.rs -pub fn to_size_string(size: f64) -> String { +pub fn to_size_string(size: u64) -> String { + let size = size as f64; let base = size.log10() / 1024_f64.log10(); let mut result = format!("{:.1}", 1024_f64.powf(base - base.floor()),) .trim_end_matches(".0") diff --git a/src/main.rs b/src/main.rs index c5a0968..d79940f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,9 @@ mod file; mod message; mod server; +#[cfg(test)] +mod tests; + use clap::Parser; use cli::{Cli, Commands}; use client::{receive, send}; diff --git a/src/message.rs b/src/message.rs index 31b3f62..769d015 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,3 +1,9 @@ +use crate::crypto::{decrypt, encrypt}; +use crate::file::FileInfo; + +use aes_gcm::Aes256Gcm; // Or `Aes128Gcm` +use anyhow::{anyhow, Result}; +use bincode::config; use bytes::Bytes; use serde::{Deserialize, Serialize}; use std::error::Error; @@ -9,6 +15,7 @@ use tokio_util::codec::{Framed, LengthDelimitedCodec}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Message { HandshakeMessage(HandshakePayload), + EncryptedMessage(EncryptedPayload), ErrorMessage(RuckError), } @@ -21,8 +28,43 @@ pub struct HandshakePayload { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct EncryptedPayload { - pub nonce: Bytes, - pub body: Bytes, + pub nonce: Vec, + pub body: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum EncryptedMessage { + FileNegotiationMessage(FileNegotiationPayload), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FileNegotiationPayload { + pub files: Vec, +} + +impl EncryptedMessage { + pub fn from_encrypted_message(cipher: &Aes256Gcm, payload: &EncryptedPayload) -> Result { + let raw = decrypt(cipher, payload)?; + let res = match bincode::deserialize(raw.as_ref()) { + Ok(result) => result, + Err(e) => { + println!("deserialize error {:?}", e); + return Err(anyhow!("deser error")); + } + }; + Ok(res) + } + pub fn to_encrypted_message(&self, cipher: &Aes256Gcm) -> Result { + let raw = match bincode::serialize(&self) { + Ok(result) => result, + Err(e) => { + println!("serialize error {:?}", e); + return Err(anyhow!("serialize error")); + } + }; + let payload = encrypt(cipher, &raw)?; + Ok(Message::EncryptedMessage(payload)) + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/src/server.rs b/src/server.rs index e3e6917..cf155b0 100644 --- a/src/server.rs +++ b/src/server.rs @@ -156,9 +156,6 @@ pub async fn handle_connection( return Ok(()); } }; - // How do I get this handshake message to the peer - // If it's the sender, the recipient hasn't arrived yet - // If it's the recipient, the sender was created before println!("server - received msg from {:?}", addr); let mut client = Client::new( handshake_payload.up, diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..50e2f2d --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,11 @@ +use crate::crypto::new_cipher; +use crate::file::FileHandle; + +use std::path::PathBuf; + +#[tokio::test] +async fn test_file_handle_nonexistent_file() { + let pb = PathBuf::new(); + let fh = FileHandle::new(pb).await; + assert!(fh.is_err()); +}