diff --git a/core_lib/Cargo.lock b/core_lib/Cargo.lock index 9dafb78..53c67d6 100644 --- a/core_lib/Cargo.lock +++ b/core_lib/Cargo.lock @@ -1010,6 +1010,22 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -1422,6 +1438,7 @@ dependencies = [ "libaes", "log", "mdns-sd", + "mime_guess", "once_cell", "open", "p256", @@ -1836,6 +1853,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/core_lib/Cargo.toml b/core_lib/Cargo.toml index 8f33087..4b574bd 100644 --- a/core_lib/Cargo.toml +++ b/core_lib/Cargo.toml @@ -33,6 +33,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } ts-rs = { version = "7.1", features = ["serde-compat", "uuid-impl", "chrono-impl"] } uuid = "1.7.0" serde = { version = "1.0", features = ["derive"] } +mime_guess = "2.0.4" [build-dependencies] prost-build = "0.12" diff --git a/core_lib/src/hdl/mod.rs b/core_lib/src/hdl/mod.rs index 4b21e82..65086d8 100644 --- a/core_lib/src/hdl/mod.rs +++ b/core_lib/src/hdl/mod.rs @@ -32,9 +32,11 @@ pub enum State { ReceivedUkeyClientFinish, SentConnectionResponse, SentPairedKeyResult, + SentIntroduction, ReceivedPairedKeyResult, WaitingForUserConsent, ReceivingFiles, + SendingFiles, Disconnected, Finished, } @@ -52,6 +54,7 @@ pub struct InnerState { pub pin_code: Option, pub transfer_metadata: Option, pub transferred_files: HashMap, + pub bytes_to_send: u64, // Everything needed for encryption/decryption/verif pub cipher_commitment: Option, diff --git a/core_lib/src/hdl/outbound.rs b/core_lib/src/hdl/outbound.rs index 0490076..413acd8 100644 --- a/core_lib/src/hdl/outbound.rs +++ b/core_lib/src/hdl/outbound.rs @@ -1,3 +1,7 @@ +use std::fs::File; +use std::os::unix::fs::MetadataExt; +use std::path::Path; + use anyhow::anyhow; use hmac::{Hmac, Mac}; use libaes::{Cipher, AES_256_KEY_LEN}; @@ -29,6 +33,9 @@ use crate::securemessage::{ EcP256PublicKey, EncScheme, GenericPublicKey, Header, HeaderAndBody, PublicKeyType, SecureMessage, SigScheme, }; +use crate::sharing_nearby::{ + file_metadata, paired_key_result_frame, FileMetadata, IntroductionFrame, +}; use crate::utils::{ gen_ecdsa_keypair, gen_random, hkdf_extract_expand, stream_read_exact, to_four_digit_string, DeviceType, RemoteDeviceInfo, @@ -41,7 +48,7 @@ const SANE_FRAME_LENGTH: i32 = 5 * 1024 * 1024; #[derive(Debug)] pub enum OutboundPayload { - File(String), + Files(Vec), } #[derive(Debug)] @@ -414,6 +421,269 @@ impl OutboundRequest { .v1 .as_ref() .ok_or_else(|| anyhow!("Missing required fields"))?; + match v1_frame.r#type() { + location_nearby_connections::v1_frame::FrameType::PayloadTransfer => { + trace!("Received FrameType::PayloadTransfer"); + let payload_transfer = v1_frame + .payload_transfer + .as_ref() + .ok_or_else(|| anyhow!("Missing required fields"))?; + + let header = payload_transfer + .payload_header + .as_ref() + .ok_or_else(|| anyhow!("Missing required fields"))?; + let chunk = payload_transfer + .payload_chunk + .as_ref() + .ok_or_else(|| anyhow!("Missing required fields"))?; + + match header.r#type() { + payload_header::PayloadType::Bytes => { + info!("Processing PayloadType::Bytes"); + let payload_id = header.id(); + + if header.total_size() > SANE_FRAME_LENGTH.into() { + self.state.payload_buffers.remove(&payload_id); + return Err(anyhow!( + "Payload too large: {} bytes", + header.total_size() + )); + } + + self.state + .payload_buffers + .entry(payload_id) + .or_insert_with(|| Vec::with_capacity(header.total_size() as usize)); + + // Get the current length of the buffer, if it exists, without holding a mutable borrow. + let buffer_len = self.state.payload_buffers.get(&payload_id).unwrap().len(); + if chunk.offset() != buffer_len as i64 { + self.state.payload_buffers.remove(&payload_id); + return Err(anyhow!( + "Unexpected chunk offset: {}, expected: {}", + chunk.offset(), + buffer_len + )); + } + + let buffer = self.state.payload_buffers.get_mut(&payload_id).unwrap(); + if let Some(body) = &chunk.body { + buffer.extend(body); + } + + if (chunk.flags() & 1) == 1 { + // Clear payload_buffer for payload_id + debug!("Chunk flags & 1 == 1 ?? End of data ??"); + + if payload_id == self.state.text_payload_id { + open::that(std::str::from_utf8(buffer)?)?; + + info!("Transfer finished"); + self.update_state( + |e| { + e.state = State::Finished; + }, + true, + ); + self.disconnection().await?; + return Err(anyhow!(crate::errors::AppError::NotAnError)); + } else { + let innner_frame = + sharing_nearby::Frame::decode(buffer.as_slice())?; + self.process_transfer_setup(&innner_frame).await?; + } + } + } + payload_header::PayloadType::File => { + error!("Unhandled PayloadType::File: {:?}", header.r#type()) + } + payload_header::PayloadType::Stream => { + error!("Unhandled PayloadType::Stream: {:?}", header.r#type()) + } + payload_header::PayloadType::UnknownPayloadType => { + error!( + "Invalid PayloadType::UnknownPayloadType: {:?}", + header.r#type() + ) + } + } + } + location_nearby_connections::v1_frame::FrameType::KeepAlive => { + trace!("Sending keepalive"); + self.send_keepalive(true).await?; + } + _ => { + error!("Unhandled offline frame encrypted: {:?}", offline); + } + } + + Ok(()) + } + + async fn process_transfer_setup( + &mut self, + frame: &sharing_nearby::Frame, + ) -> Result<(), anyhow::Error> { + let v1_frame = frame + .v1 + .as_ref() + .ok_or_else(|| anyhow!("Missing required fields"))?; + + if v1_frame.r#type() == sharing_nearby::v1_frame::FrameType::Cancel { + info!("Transfer canceled"); + return self.disconnection().await; + } + + match self.state.state { + State::SentPairedKeyEncryption => { + debug!("Processing State::SentPairedKeyEncryption"); + self.process_paired_key_encryption_frame(v1_frame).await?; + self.update_state( + |e| { + e.state = State::SentPairedKeyResult; + }, + false, + ); + } + State::SentPairedKeyResult => { + debug!("Processing State::SentPairedKeyResult"); + self.process_paired_key_result(v1_frame).await?; + self.update_state( + |e| { + e.state = State::SentIntroduction; + }, + false, + ); + } + State::SentIntroduction => { + debug!("Processing State::SentIntroduction"); + // self.process_introduction(v1_frame).await?; + } + State::SendingFiles => {} + _ => { + info!( + "Unhandled connection state in process_transfer_setup: {:?}", + self.state.state + ); + } + } + + Ok(()) + } + + async fn process_paired_key_encryption_frame( + &mut self, + v1_frame: &sharing_nearby::V1Frame, + ) -> Result<(), anyhow::Error> { + if v1_frame.paired_key_encryption.is_none() { + return Err(anyhow!("Missing required fields")); + } + + let paired_result = sharing_nearby::Frame { + version: Some(sharing_nearby::frame::Version::V1.into()), + v1: Some(sharing_nearby::V1Frame { + r#type: Some(sharing_nearby::v1_frame::FrameType::PairedKeyResult.into()), + paired_key_result: Some(sharing_nearby::PairedKeyResultFrame { + status: Some(paired_key_result_frame::Status::Unable.into()), + }), + ..Default::default() + }), + }; + + self.send_encrypted_frame(&paired_result).await?; + + Ok(()) + } + + async fn process_paired_key_result( + &mut self, + v1_frame: &sharing_nearby::V1Frame, + ) -> Result<(), anyhow::Error> { + if v1_frame.paired_key_result.is_none() { + return Err(anyhow!("Missing required fields")); + } + + let mut file_metadata: Vec = vec![]; + let mut total_to_send = 0; + // TODO - Handle sending Text + match &self.payload { + OutboundPayload::Files(files) => { + for f in files { + let path = Path::new(f); + if !path.is_file() { + continue; + } + + let file = match File::open(f) { + Ok(_f) => _f, + Err(e) => { + error!("Failed to open file: {f}: {:?}", e); + continue; + } + }; + let fmetadata = match file.metadata() { + Ok(_fm) => _fm, + Err(e) => { + error!("Failed to get metadata for: {f}: {:?}", e); + continue; + } + }; + + let ftype = mime_guess::from_path(path) + .first_or_octet_stream() + .to_string(); + + let meta_type = if ftype.starts_with("image/") { + file_metadata::Type::Image + } else if ftype.starts_with("video/") { + file_metadata::Type::Video + } else if ftype.starts_with("audio/") { + file_metadata::Type::Audio + } else if path.extension().unwrap_or_default() == "apk" { + file_metadata::Type::App + } else { + file_metadata::Type::Unknown + }; + + let fname = path + .file_name() + .ok_or_else(|| anyhow!("Failed to get file_name for {f}"))?; + let fmeta = FileMetadata { + payload_id: Some(rand::thread_rng().gen::()), + name: Some(fname.to_os_string().into_string().unwrap()), + size: Some(fmetadata.size() as i64), + mime_type: Some(ftype), + r#type: Some(meta_type.into()), + ..Default::default() + }; + // TODO - See how to handle those + file_metadata.push(fmeta); + total_to_send += fmetadata.size(); + } + } + } + + self.update_state( + |e| { + e.bytes_to_send = total_to_send; + }, + false, + ); + + let introduction = sharing_nearby::Frame { + version: Some(sharing_nearby::frame::Version::V1.into()), + v1: Some(sharing_nearby::V1Frame { + r#type: Some(sharing_nearby::v1_frame::FrameType::Introduction.into()), + introduction: Some(IntroductionFrame { + file_metadata, + ..Default::default() + }), + ..Default::default() + }), + }; + + self.send_encrypted_frame(&introduction).await?; Ok(()) } diff --git a/core_lib/src/lib.rs b/core_lib/src/lib.rs index c6ddf3e..e83d71f 100644 --- a/core_lib/src/lib.rs +++ b/core_lib/src/lib.rs @@ -19,6 +19,7 @@ mod hdl; mod manager; mod utils; +pub use hdl::OutboundPayload; pub use utils::DeviceType; pub mod sharing_nearby { diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 53b33ed..905d0d3 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -2559,6 +2559,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -3623,6 +3633,7 @@ dependencies = [ "libaes", "log", "mdns-sd", + "mime_guess", "once_cell", "open 5.0.1", "p256", @@ -4941,6 +4952,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15"