diff --git a/clients/terminal/Cargo.lock b/clients/terminal/Cargo.lock index 3eb24b0..c1317a0 100644 --- a/clients/terminal/Cargo.lock +++ b/clients/terminal/Cargo.lock @@ -103,6 +103,22 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -129,7 +145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" dependencies = [ "nix", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -148,12 +164,29 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "fcast" version = "0.1.0" dependencies = [ "clap", "ctrlc", + "native-tls", "openssl", "serde", "serde_json", @@ -277,18 +310,48 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nix" version = "0.26.2" @@ -333,6 +396,12 @@ dependencies = [ "syn", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "openssl-sys" version = "0.9.97" @@ -417,12 +486,66 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.193" @@ -488,6 +611,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -562,6 +698,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand", "sha1", "thiserror", @@ -668,7 +805,25 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -677,13 +832,43 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -692,38 +877,122 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/clients/terminal/Cargo.toml b/clients/terminal/Cargo.toml index fc37a9f..25c702d 100644 --- a/clients/terminal/Cargo.toml +++ b/clients/terminal/Cargo.toml @@ -10,7 +10,8 @@ clap = "3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" ctrlc = "3.1.9" -tungstenite = "0.21.0" +tungstenite = { version = "0.21.0", features = ["native-tls"] } url = "2.5.0" tiny_http = "0.12.0" openssl = "0.10.61" +native-tls = "0.2.11" diff --git a/clients/terminal/src/fcastsession.rs b/clients/terminal/src/fcastsession.rs index c22d4ff..366ab0c 100644 --- a/clients/terminal/src/fcastsession.rs +++ b/clients/terminal/src/fcastsession.rs @@ -1,7 +1,6 @@ -use std::{sync::{atomic::{AtomicBool, Ordering}, Arc}, collections::VecDeque}; +use std::sync::{atomic::{AtomicBool, Ordering}, Arc}; -use crate::{models::{PlaybackUpdateMessage, VolumeUpdateMessage, PlaybackErrorMessage, VersionMessage, KeyExchangeMessage, EncryptedMessage, DecryptedMessage}, transport::Transport}; -use openssl::{dh::Dh, base64, pkey::{Private, PKey}, symm::{Cipher, Crypter, Mode}, bn::BigNum}; +use crate::{models::{PlaybackUpdateMessage, VolumeUpdateMessage, PlaybackErrorMessage, VersionMessage}, transport::Transport}; use serde::Serialize; #[derive(Debug)] @@ -26,11 +25,8 @@ pub enum Opcode { PlaybackError = 9, SetSpeed = 10, Version = 11, - KeyExchange = 12, - Encrypted = 13, - Ping = 14, - Pong = 15, - StartEncryption = 16 + Ping = 12, + Pong = 13 } impl Opcode { @@ -48,11 +44,8 @@ impl Opcode { 9 => Opcode::PlaybackError, 10 => Opcode::SetSpeed, 11 => Opcode::Version, - 12 => Opcode::KeyExchange, - 13 => Opcode::Encrypted, - 14 => Opcode::Ping, - 15 => Opcode::Pong, - 16 => Opcode::StartEncryption, + 12 => Opcode::Ping, + 13 => Opcode::Pong, _ => panic!("Unknown value: {}", value), } } @@ -66,60 +59,24 @@ pub struct FCastSession<'a> { bytes_read: usize, packet_length: usize, stream: Box, - state: SessionState, - dh: Option>, - public_key: Option, - aes_key: Option>, - decrypted_messages_queue: VecDeque, - encrypted_messages_queue: VecDeque, - encryption_started: bool, - wait_for_encryption: bool + state: SessionState } impl<'a> FCastSession<'a> { - pub fn new(stream: T, encrypted: bool) -> Result> { - let (dh, public_key) = if encrypted { - println!("Initialized DH."); - generate_key_pair()? - } else { - (None, None) - }; - - Ok(FCastSession { + pub fn new(stream: T) -> Self { + return FCastSession { buffer: vec![0; MAXIMUM_PACKET_LENGTH], bytes_read: 0, packet_length: 0, stream: Box::new(stream), - state: SessionState::Idle, - wait_for_encryption: dh.is_some(), - dh, - public_key, - aes_key: None, - decrypted_messages_queue: VecDeque::new(), - encrypted_messages_queue: VecDeque::new(), - encryption_started: false - }) + state: SessionState::Idle + } } } impl FCastSession<'_> { pub fn send_message(&mut self, opcode: Opcode, message: T) -> Result<(), Box> { let json = serde_json::to_string(&message)?; - - if opcode != Opcode::Encrypted && opcode != Opcode::KeyExchange && opcode != Opcode::StartEncryption { - if self.encryption_started { - println!("Sending encrypted with opcode {:?}.", opcode); - let decrypted_message = DecryptedMessage::new(opcode as u64, Some(json)); - let encrypted_message = encrypt_message(&self.aes_key.as_ref().unwrap(), &decrypted_message)?; - return self.send_message(Opcode::Encrypted, &encrypted_message) - } else if self.wait_for_encryption { - println!("Queued message with opcode {:?} until encryption is established.", opcode); - let decrypted_message = DecryptedMessage::new(opcode as u64, Some(json)); - self.decrypted_messages_queue.push_back(decrypted_message); - return Ok(()); - } - } - let data = json.as_bytes(); let size = 1 + data.len(); let header_size = LENGTH_BYTES + 1; @@ -134,19 +91,6 @@ impl FCastSession<'_> { } pub fn send_empty(&mut self, opcode: Opcode) -> Result<(), Box> { - if opcode != Opcode::Encrypted && opcode != Opcode::KeyExchange && opcode != Opcode::StartEncryption { - let decrypted_message = DecryptedMessage::new(opcode as u64, None); - if self.encryption_started { - println!("Sending encrypted with opcode {:?}.", opcode); - let encrypted_message = encrypt_message(&self.aes_key.as_ref().unwrap(), &decrypted_message)?; - return self.send_message(Opcode::Encrypted, &encrypted_message) - } else if self.wait_for_encryption { - println!("Queued message with opcode {:?} until encryption is established.", opcode); - self.decrypted_messages_queue.push_back(decrypted_message); - return Ok(()); - } - } - let json = String::new(); let data = json.as_bytes(); let size = 1 + data.len(); @@ -159,22 +103,13 @@ impl FCastSession<'_> { Ok(()) } - pub fn receive_loop(&mut self, running: &Arc, until_queues_are_empty: bool) -> Result<(), Box> { - if let Some(pk) = &self.public_key { - println!("Sending public key."); - self.send_message(Opcode::KeyExchange, &KeyExchangeMessage::new(1, pk.clone()))?; - } - + pub fn receive_loop(&mut self, running: &Arc) -> Result<(), Box> { println!("Start receiving."); self.state = SessionState::WaitingForLength; let mut buffer = [0u8; 1024]; while running.load(Ordering::SeqCst) { - if until_queues_are_empty && self.are_queues_empty() { - break; - } - let bytes_read = self.stream.transport_read(&mut buffer)?; self.process_bytes(&buffer[..bytes_read])?; } @@ -323,68 +258,11 @@ impl FCastSession<'_> { println!("Received version with no body."); } } - Opcode::KeyExchange => { - if let Some(body_str) = body { - match serde_json::from_str::(body_str.as_str()) { - Ok(key_exchange_message) => { - if let Some(dh) = &self.dh { - println!("Received key exchange message {:?}", key_exchange_message); - self.aes_key = Some(compute_shared_secret(dh, &key_exchange_message)?); - self.send_empty(Opcode::StartEncryption)?; - - println!("Processing queued encrypted messages to handle."); - while let Some(encrypted_message) = self.encrypted_messages_queue.pop_front() { - let decrypted_message = decrypt_message(&self.aes_key.as_ref().unwrap(), &encrypted_message)?; - self.handle_packet(Opcode::from_u8(decrypted_message.opcode as u8), decrypted_message.message)?; - } - } else { - println!("Received key exchange message while encryption is diabled {:?}", key_exchange_message); - } - }, - Err(e) => println!("Received key exchange with malformed body: {}.", e) - }; - } else { - println!("Received key exchange with no body."); - } - } - Opcode::Encrypted => { - if let Some(body_str) = body { - if let Ok(encrypted_message) = serde_json::from_str::(body_str.as_str()) { - println!("Received encrypted message {:?}", encrypted_message); - - if self.aes_key.is_some() { - println!("Decrypting and handling encrypted message."); - let decrypted_message = decrypt_message(&self.aes_key.as_ref().unwrap(), &encrypted_message)?; - self.handle_packet(Opcode::from_u8(decrypted_message.opcode as u8), decrypted_message.message)?; - } else { - println!("Queued encrypted message until encryption is established."); - self.encrypted_messages_queue.push_back(encrypted_message); - - if self.encrypted_messages_queue.len() > 15 { - self.encrypted_messages_queue.pop_front(); - } - } - } else { - println!("Received encrypted with malformed body."); - } - } else { - println!("Received encrypted with no body."); - } - } Opcode::Ping => { println!("Received ping"); self.send_empty(Opcode::Pong)?; println!("Sent pong"); } - Opcode::StartEncryption => { - self.encryption_started = true; - - println!("Processing queued decrypted messages to send."); - while let Some(decrypted_message) = self.decrypted_messages_queue.pop_front() { - let encrypted_message = encrypt_message(&self.aes_key.as_ref().unwrap(), &decrypted_message)?; - self.send_message(Opcode::Encrypted, &encrypted_message)?; - } - } _ => { println!("Error handling packet"); } @@ -393,184 +271,7 @@ impl FCastSession<'_> { Ok(()) } - fn are_queues_empty(&self) -> bool { - return self.decrypted_messages_queue.is_empty() && self.encrypted_messages_queue.is_empty(); - } - pub fn shutdown(&mut self) -> Result<(), std::io::Error> { return self.stream.transport_shutdown(); } -} - -fn generate_key_pair() -> Result<(Option>, Option), Box> { - //modp14 - let p = "ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff"; - let g = "2"; - - let v = Dh::from_pqg(BigNum::from_hex_str(p)?, None, BigNum::from_hex_str(g)?)?.generate_key()?; - - let private = v.private_key().to_owned()?; - let dh2 = Dh::from_pqg(BigNum::from_hex_str(p)?, None, BigNum::from_hex_str(g)?)?.set_private_key(private)?; - let pkey = PKey::from_dh(dh2)?; - let public_key_der = pkey.public_key_to_der()?; - let public_key_base64 = base64::encode_block(public_key_der.as_ref()); - - Ok((Some(v), Some(public_key_base64))) -} - -fn encrypt_message(aes_key: &Vec, decrypted_message: &DecryptedMessage) -> Result> { - let cipher = Cipher::aes_256_cbc(); - let iv_len = cipher.iv_len().ok_or("Cipher does not support IV")?; - let mut iv = vec![0; iv_len]; - openssl::rand::rand_bytes(&mut iv)?; - - let mut crypter = Crypter::new( - cipher, - Mode::Encrypt, - aes_key, - Some(&iv) - )?; - crypter.pad(true); - - let json = serde_json::to_string(decrypted_message)?; - let mut ciphertext = vec![0; json.len() + cipher.block_size()]; - let count = crypter.update(json.as_bytes(), &mut ciphertext)?; - let rest = crypter.finalize(&mut ciphertext[count..])?; - ciphertext.truncate(count + rest); - - Ok(EncryptedMessage::new(1, Some(base64::encode_block(&iv)), base64::encode_block(&ciphertext))) -} - -fn decrypt_message(aes_key: &Vec, encrypted_message: &EncryptedMessage) -> Result> { - if encrypted_message.iv.is_none() { - return Err("IV is required for decryption.".into()); - } - - let cipher = Cipher::aes_256_cbc(); - let iv = base64::decode_block(&encrypted_message.iv.as_ref().unwrap())?; - let ciphertext = base64::decode_block(&encrypted_message.blob)?; - - let mut crypter = Crypter::new( - cipher, - Mode::Decrypt, - aes_key, - Some(&iv) - )?; - crypter.pad(true); - - let mut plaintext = vec![0; ciphertext.len() + cipher.block_size()]; - let count = crypter.update(&ciphertext, &mut plaintext)?; - let rest = crypter.finalize(&mut plaintext[count..])?; - plaintext.truncate(count + rest); - - let decrypted_str = String::from_utf8(plaintext)?; - Ok(serde_json::from_str(&decrypted_str)?) -} - -fn compute_shared_secret(dh: &Dh, key_exchange_message: &KeyExchangeMessage) -> Result, Box> { - let peer_public_key_der = base64::decode_block(&key_exchange_message.public_key)?; - let peer_public_key = PKey::public_key_from_der(&peer_public_key_der)?; - let peer_dh = peer_public_key.dh()?; - let peer_pub_key = peer_dh.public_key(); - let shared_secret = dh.compute_key(&peer_pub_key)?; - let digest = openssl::hash::hash(openssl::hash::MessageDigest::sha256(), &shared_secret)?.to_vec(); - Ok(digest) -} - -#[cfg(test)] -mod tests { - use super::*; - use openssl::base64; - - #[test] - fn test_dh_encryption_self() { - let (key_pair1, public_key1) = generate_key_pair().unwrap(); - let (key_pair2, public_key2) = generate_key_pair().unwrap(); - - let aes_key1 = compute_shared_secret(&key_pair1.unwrap(), &KeyExchangeMessage::new(1, public_key2.unwrap())).unwrap(); - let aes_key2 = compute_shared_secret(&key_pair2.unwrap(), &KeyExchangeMessage::new(1, public_key1.unwrap())).unwrap(); - - assert_eq!(aes_key1, aes_key2); - - let message = DecryptedMessage { - opcode: 1, - message: Some(r#"{"type": "text/html"}"#.to_string()), - }; - let encrypted_message = encrypt_message(&aes_key1, &message).unwrap(); - let decrypted_message = decrypt_message(&aes_key1, &encrypted_message).unwrap(); - - assert_eq!(message.opcode, decrypted_message.opcode); - assert_eq!(message.message, decrypted_message.message); - } - - - #[test] - fn test_dh_encryption_known() { - let private_key1 = base64::decode_block("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==").unwrap(); - let key_exchange_message_2 = KeyExchangeMessage::new(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=".to_string()); - - let private_key = PKey::private_key_from_der(&private_key1).unwrap(); - let dh = private_key.dh().unwrap(); - - let aes_key1 = compute_shared_secret(&dh, &key_exchange_message_2).unwrap(); - assert_eq!(base64::encode_block(&aes_key1), "vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE="); - - let message = DecryptedMessage { - opcode: 1, - message: Some(r#"{"type": "text/html"}"#.to_string()), - }; - let encrypted_message = encrypt_message(&aes_key1, &message).unwrap(); - let decrypted_message = decrypt_message(&aes_key1, &encrypted_message).unwrap(); - - assert_eq!(message.opcode, decrypted_message.opcode); - assert_eq!(message.message, decrypted_message.message); - } - - #[test] - fn test_decrypt_message_known() { - let encrypted_message_json = r#"{"version":1,"iv":"C4H70VC5FWrNtkty9/cLIA==","blob":"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA=="}"#; - let encrypted_message: EncryptedMessage = serde_json::from_str(encrypted_message_json).unwrap(); - let aes_key_base64 = "+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU="; - let aes_key = base64::decode_block(aes_key_base64).unwrap(); - - let decrypted_message = decrypt_message(&aes_key, &encrypted_message).unwrap(); - assert_eq!(1, decrypted_message.opcode); - assert_eq!("{\"container\":\"text/html\"}", decrypted_message.message.unwrap()); - } - - #[test] - fn test_aes_key_generation() { - let cases = vec![ - ( - // Public other - String::from("MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF"), - // Private self - String::from("MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M="), - // Expected AES key - String::from("7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="), - ), - ( - // Public other - String::from("MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK"), - // Private self - String::from("MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA="), - // Expected AES key - String::from("a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q=") - ) - ]; - - for case in cases { - let private_self_key = base64::decode_block(&case.1).expect("Invalid base64 for private self key"); - let expected_aes_key = base64::decode_block(&case.2).expect("Invalid base64 for expected AES key"); - - let private_key = PKey::private_key_from_der(&private_self_key).expect("Failed to create private key"); - let dh = private_key.dh().expect("Failed to create DH from private key"); - let key_exchange_message = KeyExchangeMessage::new(1, case.0); - - let aes_key = compute_shared_secret(&dh, &key_exchange_message).expect("Failed to compute shared secret"); - let aes_key_base64 = base64::encode_block(&aes_key); - - assert_eq!(aes_key_base64, base64::encode_block(&expected_aes_key), "AES keys do not match"); - } - } } \ No newline at end of file diff --git a/clients/terminal/src/main.rs b/clients/terminal/src/main.rs index 6dc1dc3..775e5d3 100644 --- a/clients/terminal/src/main.rs +++ b/clients/terminal/src/main.rs @@ -3,7 +3,9 @@ mod fcastsession; mod transport; use clap::{App, Arg, SubCommand}; +use native_tls::{TlsConnector, Protocol}; use tiny_http::{Server, Response, ListenAddr, Header}; +use tungstenite::Connector; use tungstenite::stream::MaybeTlsStream; use url::Url; use std::net::IpAddr; @@ -152,36 +154,68 @@ fn run() -> Result<(), Box> { let connection_type = matches.value_of("connection_type").unwrap_or("tcp"); + let encrypted = matches.is_present("encrypted"); let port = match matches.value_of("port") { Some(s) => s, - _ => match connection_type { - "tcp" => "46899", - "ws" => "46898", + _ => match (connection_type, encrypted) { + ("tcp", false) => "46899", + ("tcp", true) => "46897", + ("ws", false) => "46898", + ("ws", true) => "46896", _ => return Err("Unknown connection type, cannot automatically determine port.".into()) } }; - let encrypted = matches.is_present("encrypted"); - let local_ip: Option; - let mut session = match connection_type { - "tcp" => { + let mut session = match (connection_type, encrypted) { + ("tcp", false) => { println!("Connecting via TCP to host={} port={}...", host, port); let stream = TcpStream::connect(format!("{}:{}", host, port))?; local_ip = Some(stream.local_addr()?.ip()); - FCastSession::new(stream, encrypted)? + FCastSession::new(stream) }, - "ws" => { + ("tcp", true) => { + println!("Connecting via TCP TLS to host={} port={}...", host, port); + let mut builder = TlsConnector::builder(); + builder.min_protocol_version(Some(Protocol::Tlsv12)); + builder.danger_accept_invalid_certs(true); + let connector = builder.build()?; + let stream = TcpStream::connect(format!("{}:{}", host, port))?; + let tls_stream = connector.connect(host, stream)?; + local_ip = Some(tls_stream.get_ref().local_addr()?.ip()); + FCastSession::new(tls_stream) + }, + ("ws", false) => { println!("Connecting via WebSocket to host={} port={}...", host, port); let url = Url::parse(format!("ws://{}:{}", host, port).as_str())?; let (stream, _) = tungstenite::connect(url)?; local_ip = match stream.get_ref() { MaybeTlsStream::Plain(ref stream) => Some(stream.local_addr()?.ip()), - _ => None + _ => return Err("Established connection type is not plain.".into()) }; - FCastSession::new(stream, encrypted)? + FCastSession::new(stream) }, - _ => return Err("Invalid connection type. Use 'tcp' or 'websocket'.".into()), + ("ws", true) => { + println!("Connecting via WebSocket to host={} port={}...", host, port); + let mut builder = TlsConnector::builder(); + builder.min_protocol_version(Some(Protocol::Tlsv12)); + builder.danger_accept_invalid_certs(true); + let connector = builder.build()?; + + let url = Url::parse(&format!("wss://{}:{}", host, port))?; + let stream = TcpStream::connect(format!("{}:{}", host, port))?; + let connector = Some(Connector::NativeTls(connector.into())); + let (socket, _) = tungstenite::client_tls_with_config(url, stream, None, connector)?; + + local_ip = match socket.get_ref() { + MaybeTlsStream::NativeTls(ref stream) => Some(stream.get_ref().local_addr()?.ip()), + _ => return Err("Expected TLS stream".into()), + }; + + FCastSession::new(socket) + + }, + _ => return Err("Invalid connection type or encryption flag.".into()), }; println!("Connection established."); @@ -295,7 +329,7 @@ fn run() -> Result<(), Box> { println!("Waiting for Ctrl+C..."); - session.receive_loop(&running, false)?; + session.receive_loop(&running)?; println!("Ctrl+C received, exiting..."); } else if let Some(setvolume_matches) = matches.subcommand_matches("setvolume") { @@ -317,17 +351,6 @@ fn run() -> Result<(), Box> { std::process::exit(1); } - let receive_running = Arc::new(AtomicBool::new(true)); - let receive_r = receive_running.clone(); - - ctrlc::set_handler(move || { - println!("Ctrl+C triggered..."); - receive_r.store(false, Ordering::SeqCst); - }).expect("Error setting Ctrl-C handler"); - - println!("Waiting on queues to be empty... press CTRL-C to cancel."); - session.receive_loop(&receive_running, true)?; - println!("Waiting on other threads..."); if let Some(v) = join_handle { if let Err(_) = v.join() { diff --git a/clients/terminal/src/models.rs b/clients/terminal/src/models.rs index 1ab343e..8c60bbe 100644 --- a/clients/terminal/src/models.rs +++ b/clients/terminal/src/models.rs @@ -69,42 +69,4 @@ pub struct PlaybackErrorMessage { #[derive(Deserialize, Debug)] pub struct VersionMessage { pub version: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct KeyExchangeMessage { - pub version: u64, - #[serde(rename = "publicKey")] - pub public_key: String, -} - -impl KeyExchangeMessage { - pub fn new(version: u64, public_key: String) -> Self { - Self { version, public_key } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct DecryptedMessage { - pub opcode: u64, - pub message: Option, -} - -impl DecryptedMessage { - pub fn new(opcode: u64, message: Option) -> Self { - Self { opcode, message } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct EncryptedMessage { - pub version: u64, - pub iv: Option, - pub blob: String, -} - -impl EncryptedMessage { - pub fn new(version: u64, iv: Option, blob: String) -> Self { - Self { version, iv, blob } - } } \ No newline at end of file diff --git a/clients/terminal/src/transport.rs b/clients/terminal/src/transport.rs index 8914514..9ca8d3d 100644 --- a/clients/terminal/src/transport.rs +++ b/clients/terminal/src/transport.rs @@ -1,5 +1,6 @@ use std::io::{Read, Write}; use std::net::TcpStream; +use native_tls::TlsStream; use tungstenite::Message; use tungstenite::protocol::WebSocket; @@ -54,3 +55,18 @@ impl Transport for WebSocket { Ok(()) } } + + +impl Transport for TlsStream { + fn transport_read(&mut self, buf: &mut [u8]) -> Result { + self.read(buf) + } + + fn transport_write(&mut self, buf: &[u8]) -> Result<(), std::io::Error> { + self.write_all(buf) + } + + fn transport_shutdown(&mut self) -> Result<(), std::io::Error> { + self.shutdown().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + } +} \ No newline at end of file diff --git a/receivers/android/app/build.gradle b/receivers/android/app/build.gradle index 5aae9bf..e1be7fa 100644 --- a/receivers/android/app/build.gradle +++ b/receivers/android/app/build.gradle @@ -20,12 +20,12 @@ if (keystorePropertiesFile.exists()) { android { namespace 'com.futo.fcast.receiver' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.futo.fcast.receiver" minSdk 24 - targetSdk 33 + targetSdk 34 versionCode currentVersionCode versionName currentVersionName @@ -75,17 +75,22 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { + buildConfig true + } } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.8.0' - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" - implementation 'com.google.android.exoplayer:exoplayer:2.18.6' + implementation 'com.google.android.material:material:1.11.0' + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" + implementation 'com.google.android.exoplayer:exoplayer:2.19.1' implementation "com.squareup.okhttp3:okhttp:4.11.0" implementation 'com.journeyapps:zxing-android-embedded:4.3.0' implementation 'org.java-websocket:Java-WebSocket:1.5.4' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' + implementation 'org.bouncycastle:bcprov-jdk18on:1.77' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/receivers/android/app/src/androidTest/java/com/futo/fcast/receiver/EncryptionTest.kt b/receivers/android/app/src/androidTest/java/com/futo/fcast/receiver/EncryptionTest.kt deleted file mode 100644 index b995a17..0000000 --- a/receivers/android/app/src/androidTest/java/com/futo/fcast/receiver/EncryptionTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.futo.fcast.receiver - -import android.util.Base64 -import android.util.Log -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* -import java.security.KeyFactory -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.spec.PKCS8EncodedKeySpec -import java.security.spec.X509EncodedKeySpec -import javax.crypto.interfaces.DHPrivateKey -import javax.crypto.spec.SecretKeySpec - -@RunWith(AndroidJUnit4::class) -class EncryptionTest { - @Test - fun testDHEncryptionSelf() { - val keyPair1 = FCastSession.generateKeyPair() - val keyPair2 = FCastSession.generateKeyPair() - Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})") - - val keyExchangeMessage1 = FCastSession.getKeyExchangeMessage(keyPair1) - val keyExchangeMessage2 = FCastSession.getKeyExchangeMessage(keyPair2) - Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})") - - val aesKey1 = FCastSession.computeSharedSecret(keyPair1.private, keyExchangeMessage2) - val aesKey2 = FCastSession.computeSharedSecret(keyPair2.private, keyExchangeMessage1) - - assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP)) - Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}") - - val message = PlayMessage("text/html") - val serializedBody = Json.encodeToString(message) - val encryptedMessage = FCastSession.encryptMessage(aesKey1, DecryptedMessage(Opcode.Play.value.toLong(), serializedBody)) - Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage)) - - val decryptedMessage = FCastSession.decryptMessage(aesKey1, encryptedMessage) - - assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode) - assertEquals(serializedBody, decryptedMessage.message) - } - - @Test - fun testAESKeyGeneration() { - val cases = listOf( - listOf( - //Public other - "MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF", - //Private self - "MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=", - //AES - "7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk=" - ), - listOf( - //Public other - "MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK", - //Private self - "MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=", - //AES - "a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q=" - ) - ) - - for (case in cases) { - val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP) - val keyExchangeMessage2 = KeyExchangeMessage(1, case[0]) - - val keyFactory = KeyFactory.getInstance("DH") - val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1) - val privateKey = keyFactory.generatePrivate(privateKeySpec) - val aesKey1 = FCastSession.computeSharedSecret(privateKey, keyExchangeMessage2) - assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)) - } - } - - @Test - fun testDHEncryptionKnown() { - val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP) - val keyExchangeMessage2 = KeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=") - - val keyFactory = KeyFactory.getInstance("DH") - val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1) - val privateKey = keyFactory.generatePrivate(privateKeySpec) - val aesKey1 = FCastSession.computeSharedSecret(privateKey, keyExchangeMessage2) - assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)) - - val message = PlayMessage("text/html") - val serializedBody = Json.encodeToString(message) - val encryptedMessage = FCastSession.encryptMessage(aesKey1, DecryptedMessage(Opcode.Play.value.toLong(), serializedBody)) - val decryptedMessage = FCastSession.decryptMessage(aesKey1, encryptedMessage) - - assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode) - assertEquals(serializedBody, decryptedMessage.message) - } - - @Test - fun testDecryptMessageKnown() { - val encryptedMessage = Json.decodeFromString("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}") - val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES") - val decryptedMessage = FCastSession.decryptMessage(aesKey, encryptedMessage) - assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode) - assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message) - } -} \ No newline at end of file diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt index cb3c979..40b5369 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/FCastSession.kt @@ -42,13 +42,10 @@ enum class Opcode(val value: Byte) { VolumeUpdate(7), SetVolume(8), PlaybackError(9), - SetSpeed(10), + SetSpeed(10), Version(11), - KeyExchange(12), - Encrypted(13), - Ping(14), - Pong(15), - StartEncryption(16); + Ping(12), + Pong(13); companion object { private val _map = values().associateBy { it.value } @@ -65,39 +62,9 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress: private var _packetLength = 0 private var _state = SessionState.WaitingForLength private var _outputStream: DataOutputStream? = DataOutputStream(outputStream) - private val _keyPair: KeyPair = generateKeyPair() - private var _aesKey: SecretKeySpec? = null - private val _queuedEncryptedMessages = arrayListOf() - private var _encryptionStarted = false val id = UUID.randomUUID() - init { - send(Opcode.KeyExchange, getKeyExchangeMessage(_keyPair)) - } - - fun sendVersion(value: VersionMessage) { - send(Opcode.Version, value) - } - - fun sendPlaybackError(value: PlaybackErrorMessage) { - send(Opcode.PlaybackError, value) - } - - fun sendPlaybackUpdate(value: PlaybackUpdateMessage) { - send(Opcode.PlaybackUpdate, value) - } - - fun sendVolumeUpdate(value: VolumeUpdateMessage) { - send(Opcode.VolumeUpdate, value) - } - - private fun send(opcode: Opcode, message: String? = null) { - val aesKey = _aesKey - if (_encryptionStarted && aesKey != null && opcode != Opcode.Encrypted && opcode != Opcode.KeyExchange && opcode != Opcode.StartEncryption) { - send(Opcode.Encrypted, encryptMessage(aesKey, DecryptedMessage(opcode.value.toLong(), message))) - return - } - + fun send(opcode: Opcode, message: String? = null) { try { val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0) val size = 1 + data.size @@ -129,7 +96,7 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress: } } - private inline fun send(opcode: Opcode, message: T) { + inline fun send(opcode: Opcode, message: T) { try { send(opcode, message?.let { Json.encodeToString(it) }) } catch (e: Throwable) { @@ -246,42 +213,7 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress: Opcode.Seek -> _service.onCastSeek(json.decodeFromString(body!!)) Opcode.SetVolume -> _service.onSetVolume(json.decodeFromString(body!!)) Opcode.SetSpeed -> _service.onSetSpeed(json.decodeFromString(body!!)) - Opcode.KeyExchange -> { - val keyExchangeMessage: KeyExchangeMessage = json.decodeFromString(body!!) - _aesKey = computeSharedSecret(_keyPair.private, keyExchangeMessage) - send(Opcode.StartEncryption) - - synchronized(_queuedEncryptedMessages) { - for (queuedEncryptedMessages in _queuedEncryptedMessages) { - val decryptedMessage = decryptMessage(_aesKey!!, queuedEncryptedMessages) - val o = Opcode.find(decryptedMessage.opcode.toByte()) - handlePacket(o, decryptedMessage.message) - } - - _queuedEncryptedMessages.clear() - } - } Opcode.Ping -> send(Opcode.Pong) - Opcode.Encrypted -> { - val encryptedMessage: EncryptedMessage = json.decodeFromString(body!!) - if (_aesKey != null) { - val decryptedMessage = decryptMessage(_aesKey!!, encryptedMessage) - val o = Opcode.find(decryptedMessage.opcode.toByte()) - handlePacket(o, decryptedMessage.message) - } else { - synchronized(_queuedEncryptedMessages) { - if (_queuedEncryptedMessages.size == 15) { - _queuedEncryptedMessages.removeAt(0) - } - - _queuedEncryptedMessages.add(encryptedMessage) - } - } - } - Opcode.StartEncryption -> { - _encryptionStarted = true - //TODO: Send decrypted messages waiting for encryption to be established - } else -> { } } } catch (e: Throwable) { @@ -292,63 +224,5 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress: companion object { const val TAG = "FCastSession" private val json = Json { ignoreUnknownKeys = true } - - fun getKeyExchangeMessage(keyPair: KeyPair): KeyExchangeMessage { - return KeyExchangeMessage(1, Base64.encodeToString(keyPair.public.encoded, Base64.NO_WRAP)) - } - - fun generateKeyPair(): KeyPair { - //modp14 - val p = BigInteger("ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff", 16) - val g = BigInteger("2", 16) - val dhSpec = DHParameterSpec(p, g) - - val keyGen = KeyPairGenerator.getInstance("DH") - keyGen.initialize(dhSpec) - - return keyGen.generateKeyPair() - } - - fun computeSharedSecret(privateKey: PrivateKey, keyExchangeMessage: KeyExchangeMessage): SecretKeySpec { - val keyFactory = KeyFactory.getInstance("DH") - val receivedPublicKeyBytes = Base64.decode(keyExchangeMessage.publicKey, Base64.NO_WRAP) - val receivedPublicKeySpec = X509EncodedKeySpec(receivedPublicKeyBytes) - val receivedPublicKey = keyFactory.generatePublic(receivedPublicKeySpec) - - val keyAgreement = KeyAgreement.getInstance("DH") - keyAgreement.init(privateKey) - keyAgreement.doPhase(receivedPublicKey, true) - - val sharedSecret = keyAgreement.generateSecret() - Log.i(TAG, "sharedSecret ${Base64.encodeToString(sharedSecret, Base64.NO_WRAP)}") - val sha256 = MessageDigest.getInstance("SHA-256") - val hashedSecret = sha256.digest(sharedSecret) - Log.i(TAG, "hashedSecret ${Base64.encodeToString(hashedSecret, Base64.NO_WRAP)}") - - return SecretKeySpec(hashedSecret, "AES") - } - - fun encryptMessage(aesKey: SecretKeySpec, decryptedMessage: DecryptedMessage): EncryptedMessage { - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.ENCRYPT_MODE, aesKey) - val iv = cipher.iv - val json = Json.encodeToString(decryptedMessage) - val encrypted = cipher.doFinal(json.toByteArray(Charsets.UTF_8)) - return EncryptedMessage( - version = 1, - iv = Base64.encodeToString(iv, Base64.NO_WRAP), - blob = Base64.encodeToString(encrypted, Base64.NO_WRAP) - ) - } - - fun decryptMessage(aesKey: SecretKeySpec, encryptedMessage: EncryptedMessage): DecryptedMessage { - val iv = Base64.decode(encryptedMessage.iv, Base64.NO_WRAP) - val encrypted = Base64.decode(encryptedMessage.blob, Base64.NO_WRAP) - - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv)) - val decryptedJson = cipher.doFinal(encrypted) - return Json.decodeFromString(String(decryptedJson, Charsets.UTF_8)) - } } } \ No newline at end of file diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt index 7c27837..e335b4f 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/NetworkService.kt @@ -1,5 +1,6 @@ package com.futo.fcast.receiver +import SslKeyManager import WebSocketListenerService import android.app.* import android.content.Context @@ -11,13 +12,16 @@ import android.util.Log import android.widget.Toast import androidx.core.app.NotificationCompat import kotlinx.coroutines.* +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security class NetworkService : Service() { private var _discoveryService: DiscoveryService? = null + private var _stopped = false private var _tcpListenerService: TcpListenerService? = null + private var _tlsListenerService: TlsListenerService? = null private var _webSocketListenerService: WebSocketListenerService? = null private var _scope: CoroutineScope? = null - private var _stopped = false override fun onBind(intent: Intent?): IBinder? { return null @@ -63,7 +67,7 @@ class NetworkService : Service() { withContext(Dispatchers.IO) { try { Log.i(TAG, "Sending version ${session.id}") - session.sendVersion(VersionMessage(2)) + session.send(Opcode.Version, VersionMessage(2)) } catch (e: Throwable) { Log.e(TAG, "Failed to send version ${session.id}") } @@ -75,7 +79,7 @@ class NetworkService : Service() { val updateMessage = generateUpdateMessage() withContext(Dispatchers.IO) { try { - session.sendPlaybackUpdate(updateMessage) + session.send(Opcode.PlaybackUpdate, updateMessage) Log.i(TAG, "Update sent ${session.id}") } catch (eSend: Throwable) { Log.e(TAG, "Unhandled error sending update ${session.id}", eSend) @@ -106,12 +110,18 @@ class NetworkService : Service() { start() } + val sslKeyManager = SslKeyManager("fcast_receiver") + _tlsListenerService = TlsListenerService(this) { onNewSession(it) }.apply { + start(sslKeyManager) + } + Log.i(TAG, "Started NetworkService") Toast.makeText(this, "Started FCast service", Toast.LENGTH_LONG).show() return START_STICKY } + @Suppress("DEPRECATION") private fun createNotificationBuilder(): NotificationCompat.Builder { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationCompat.Builder(this, CHANNEL_ID) @@ -134,6 +144,9 @@ class NetworkService : Service() { _tcpListenerService?.stop() _tcpListenerService = null + _tlsListenerService?.stop() + _tlsListenerService = null + try { _webSocketListenerService?.stop() } catch (e: Throwable) { @@ -170,12 +183,11 @@ class NetworkService : Service() { } } - fun sendPlaybackError(error: String) { - val message = PlaybackErrorMessage(error) - _tcpListenerService?.forEachSession { session -> + private inline fun send(opcode: Opcode, message: T) { + val sender: (FCastSession) -> Unit = { session: FCastSession -> _scope?.launch(Dispatchers.IO) { try { - session.sendPlaybackError(message) + session.send(opcode, message) Log.i(TAG, "Playback error sent ${session.id}") } catch (e: Throwable) { Log.w(TAG, "Failed to send playback error", e) @@ -183,64 +195,22 @@ class NetworkService : Service() { } } - _webSocketListenerService?.forEachSession { session -> - _scope?.launch(Dispatchers.IO) { - try { - session.sendPlaybackError(message) - Log.i(TAG, "Playback error sent ${session.id}") - } catch (e: Throwable) { - Log.w(TAG, "Failed to send playback error", e) - } - } - } + _tcpListenerService?.forEachSession(sender) + _webSocketListenerService?.forEachSession(sender) + _tlsListenerService?.forEachSession(sender) + } + + fun sendPlaybackError(error: String) { + val message = PlaybackErrorMessage(error) + send(Opcode.PlaybackError, message) } fun sendPlaybackUpdate(message: PlaybackUpdateMessage) { - _tcpListenerService?.forEachSession { session -> - _scope?.launch(Dispatchers.IO) { - try { - session.sendPlaybackUpdate(message) - Log.i(TAG, "Playback update sent ${session.id}") - } catch (e: Throwable) { - Log.w(TAG, "Failed to send playback update", e) - } - } - } - - _webSocketListenerService?.forEachSession { session -> - _scope?.launch(Dispatchers.IO) { - try { - session.sendPlaybackUpdate(message) - Log.i(TAG, "Playback update sent ${session.id}") - } catch (e: Throwable) { - Log.w(TAG, "Failed to send playback update", e) - } - } - } + send(Opcode.PlaybackUpdate, message) } fun sendCastVolumeUpdate(value: VolumeUpdateMessage) { - _tcpListenerService?.forEachSession { session -> - _scope?.launch(Dispatchers.IO) { - try { - session.sendVolumeUpdate(value) - Log.i(TAG, "Volume update sent ${session.id}") - } catch (e: Throwable) { - Log.w(TAG, "Failed to send volume update", e) - } - } - } - - _webSocketListenerService?.forEachSession { session -> - _scope?.launch(Dispatchers.IO) { - try { - session.sendVolumeUpdate(value) - Log.i(TAG, "Volume update sent ${session.id}") - } catch (e: Throwable) { - Log.w(TAG, "Failed to send volume update", e) - } - } - } + send(Opcode.VolumeUpdate, value) } fun onCastPlay(playMessage: PlayMessage) { @@ -361,7 +331,7 @@ class NetworkService : Service() { private const val CHANNEL_ID = "NetworkListenerServiceChannel" private const val NOTIFICATION_ID = 1 private const val PLAY_NOTIFICATION_ID = 2 - private const val TAG = "NetworkService" + const val TAG = "NetworkService" var activityCount = 0 var instance: NetworkService? = null } diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/Packets.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/Packets.kt index ca336f0..44e37aa 100644 --- a/receivers/android/app/src/main/java/com/futo/fcast/receiver/Packets.kt +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/Packets.kt @@ -49,23 +49,4 @@ data class SetVolumeMessage( @Serializable data class VersionMessage( val version: Long -) - -@Serializable -data class KeyExchangeMessage( - val version: Long, - val publicKey: String -) - -@Serializable -data class DecryptedMessage( - val opcode: Long, - val message: String? -) - -@Serializable -data class EncryptedMessage( - val version: Long, - val iv: String?, - val blob: String ) \ No newline at end of file diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/SslKeyManager.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/SslKeyManager.kt new file mode 100644 index 0000000..e70594c --- /dev/null +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/SslKeyManager.kt @@ -0,0 +1,80 @@ +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import java.io.FileInputStream +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.security.PublicKey +import java.util.Calendar +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLServerSocketFactory +import java.security.cert.X509Certificate +import javax.net.ssl.TrustManagerFactory + +class SslKeyManager(private val alias: String) { + + fun getSslServerSocketFactory(): SSLServerSocketFactory { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + //if (!keyStore.containsAlias(alias)) { + generateKeyPairAndCertificate(keyStore) + //} + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply { + init(keyStore) + } + + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply { + init(keyStore, null) + } + + val sslContext = SSLContext.getInstance("TLS").apply { + init(keyManagerFactory.keyManagers, trustManagerFactory.trustManagers, null) + } + + return sslContext.serverSocketFactory + } + + private fun generateKeyPairAndCertificate(keyStore: KeyStore) { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore") + val parameterSpec = KeyGenParameterSpec + .Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY) + //.setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) + .build() + + keyPairGenerator.initialize(parameterSpec) + + val keyPair = keyPairGenerator.generateKeyPair() + val privateKey = keyPair.private + val publicKey = keyPair.public + val cert = generateSelfSignedCertificate(privateKey, publicKey) + keyStore.setKeyEntry(alias, privateKey, null, arrayOf(cert)) + } + + private fun generateSelfSignedCertificate(privateKey: PrivateKey, publicKey: PublicKey): X509Certificate { + val start = Calendar.getInstance().time + val end = Calendar.getInstance().apply { add(Calendar.YEAR, 1000) }.time + + val certInfo = X509v3CertificateBuilder( + X500Name("CN=FCastReceiver"), + BigInteger.ONE, + start, + end, + X500Name("CN=FCastReceiver"), + SubjectPublicKeyInfo.getInstance(publicKey.encoded) + ) + + val signer = JcaContentSignerBuilder("SHA256withRSA").build(privateKey) + return JcaX509CertificateConverter().getCertificate(certInfo.build(signer)) + } +} \ No newline at end of file diff --git a/receivers/android/app/src/main/java/com/futo/fcast/receiver/TlsListenerService.kt b/receivers/android/app/src/main/java/com/futo/fcast/receiver/TlsListenerService.kt new file mode 100644 index 0000000..c8e275d --- /dev/null +++ b/receivers/android/app/src/main/java/com/futo/fcast/receiver/TlsListenerService.kt @@ -0,0 +1,134 @@ +package com.futo.fcast.receiver + +import SslKeyManager +import android.util.Log +import java.io.BufferedInputStream +import java.net.Socket +import java.security.KeyStore +import java.security.cert.Certificate +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLServerSocket +import javax.net.ssl.TrustManagerFactory + +class TlsListenerService(private val _networkService: NetworkService, private val _onNewSession: (session: FCastSession) -> Unit) { + private var _serverSocket: SSLServerSocket? = null + private var _stopped: Boolean = false + private var _listenThread: Thread? = null + private var _clientThreads: ArrayList = arrayListOf() + private var _sessions: ArrayList = arrayListOf() + + fun start(sslKeyManager: SslKeyManager) { + Log.i(TAG, "Starting TlsListenerService") + + val serverSocketFactory = sslKeyManager.getSslServerSocketFactory() + _serverSocket = (serverSocketFactory.createServerSocket(PORT) as SSLServerSocket) + + _listenThread = Thread { + Log.i(TAG, "Starting TLS listener") + + try { + listenForIncomingConnections() + } catch (e: Throwable) { + Log.e(TAG, "Stopped TLS listening for connections due to an unexpected error", e) + } + } + + _listenThread?.start() + + Log.i(TAG, "Started TlsListenerService") + } + + fun stop() { + Log.i(TAG, "Stopping TlsListenerService") + + _stopped = true + + _serverSocket?.close() + _serverSocket = null + + _listenThread?.join() + _listenThread = null + + synchronized(_clientThreads) { + _clientThreads.clear() + } + + Log.i(TAG, "Stopped TlsListenerService") + } + + fun forEachSession(handler: (FCastSession) -> Unit) { + synchronized(_sessions) { + for (session in _sessions) { + handler(session) + } + } + } + + private fun listenForIncomingConnections() { + Log.i(TAG, "Started TLS listening for incoming connections") + + while (!_stopped) { + val clientSocket = _serverSocket?.accept() ?: break + + val clientThread = Thread { + try { + handleClientConnection(clientSocket) + } catch (e: Throwable) { + Log.e(TAG, "Failed handle TLS client connection due to an error", e) + } + } + + synchronized(_clientThreads) { + _clientThreads.add(clientThread) + } + + clientThread.start() + } + + Log.i(TAG, "Stopped TLS listening for incoming connections") + } + + private fun handleClientConnection(socket: Socket) { + Log.i(TAG, "New TLS connection received from ${socket.remoteSocketAddress}") + + val session = FCastSession(socket.getOutputStream(), socket.remoteSocketAddress, _networkService) + synchronized(_sessions) { + _sessions.add(session) + } + _onNewSession(session) + + Log.i(TAG, "Waiting for data from ${socket.remoteSocketAddress}") + + val bufferSize = 4096 + val buffer = ByteArray(bufferSize) + val inputStream = BufferedInputStream(socket.getInputStream()) + + var bytesRead: Int + while (!_stopped) { + bytesRead = inputStream.read(buffer, 0, bufferSize) + if (bytesRead == -1) { + break + } + + session.processBytes(buffer, bytesRead) + } + + socket.close() + + synchronized(_sessions) { + _sessions.remove(session) + } + + synchronized(_clientThreads) { + _clientThreads.remove(Thread.currentThread()) + } + + Log.i(TAG, "Disconnected ${socket.remoteSocketAddress}") + } + + companion object { + const val TAG = "TlsListenerService" + const val PORT = 46897 + } +} \ No newline at end of file diff --git a/receivers/android/build.gradle b/receivers/android/build.gradle index ab2b874..a7ea160 100644 --- a/receivers/android/build.gradle +++ b/receivers/android/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.4.2' apply false - id 'com.android.library' version '7.4.2' apply false + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.0' apply false } \ No newline at end of file diff --git a/receivers/android/gradle.properties b/receivers/android/gradle.properties index 3c5031e..f19c7b9 100644 --- a/receivers/android/gradle.properties +++ b/receivers/android/gradle.properties @@ -20,4 +20,5 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/receivers/android/gradle/wrapper/gradle-wrapper.properties b/receivers/android/gradle/wrapper/gradle-wrapper.properties index 3fa9882..14ad7bf 100644 --- a/receivers/android/gradle/wrapper/gradle-wrapper.properties +++ b/receivers/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon May 01 06:24:43 CDT 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/receivers/electron/package-lock.json b/receivers/electron/package-lock.json index cd875ed..3fdf0f5 100644 --- a/receivers/electron/package-lock.json +++ b/receivers/electron/package-lock.json @@ -10,15 +10,16 @@ "license": "MIT", "dependencies": { "bufferutil": "^4.0.8", - "crypto": "^1.0.1", - "libsodium-wrappers": "^0.7.13", + "https": "^1.0.0", + "node-forge": "^1.3.1", "qrcode": "^1.5.3", - "utf-8-validate": "^6.0.3", + "tls": "^0.0.1", "ws": "^8.14.2" }, "devDependencies": { "@types/jest": "^29.5.11", - "@types/libsodium-wrappers": "^0.7.13", + "@types/mdns": "^0.0.38", + "@types/node-forge": "^1.3.10", "@types/workerpool": "^6.1.1", "@types/ws": "^8.5.10", "electron": "^22.2.0", @@ -1311,11 +1312,14 @@ "@types/node": "*" } }, - "node_modules/@types/libsodium-wrappers": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", - "integrity": "sha512-KeAKtlObirLJk/na6jHBFEdTDjDfFS6Vcr0eG2FjiHKn3Nw8axJFfIu0Y9TpwaauRldQBj/pZm/MHtK76r6OWg==", - "dev": true + "node_modules/@types/mdns": { + "version": "0.0.38", + "resolved": "https://registry.npmjs.org/@types/mdns/-/mdns-0.0.38.tgz", + "integrity": "sha512-uiDl+FWeO2JYStfiPsyPpU7bHK3VYETquPo3A8bj6h2+iqDIEfXpaZaLvyGDGL9ilcrGc1vp+ek3Ab+QtDBXPA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/node": { "version": "18.13.0", @@ -1323,6 +1327,15 @@ "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", "dev": true }, + "node_modules/@types/node-forge": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.10.tgz", + "integrity": "sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -2174,12 +2187,6 @@ "node": ">= 8" } }, - "node_modules/crypto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", - "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", - "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2970,6 +2977,11 @@ "node": ">=10.19.0" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4060,19 +4072,6 @@ "node": ">=6" } }, - "node_modules/libsodium": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", - "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==" - }, - "node_modules/libsodium-wrappers": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.13.tgz", - "integrity": "sha512-kasvDsEi/r1fMzKouIDv7B8I6vNmknXwGiYodErGuESoFTohGSKZplFtVxZqHaoQ217AynyIFgnOVRitpHs0Qw==", - "dependencies": { - "libsodium": "^0.7.13" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4310,6 +4309,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-gyp-build": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.1.tgz", @@ -5151,6 +5158,11 @@ "node": ">=8" } }, + "node_modules/tls": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tls/-/tls-0.0.1.tgz", + "integrity": "sha512-GzHpG+hwupY8VMR6rYsnAhTHqT/97zT45PG8WD5eTT1lq+dFE0nN+1PYpsoBcHJgSmTz5ceK2Cv88IkPmIPOtQ==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5367,6 +5379,8 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, diff --git a/receivers/electron/package.json b/receivers/electron/package.json index 763b920..25d5e83 100644 --- a/receivers/electron/package.json +++ b/receivers/electron/package.json @@ -12,6 +12,8 @@ }, "devDependencies": { "@types/jest": "^29.5.11", + "@types/mdns": "^0.0.38", + "@types/node-forge": "^1.3.10", "@types/workerpool": "^6.1.1", "@types/ws": "^8.5.10", "electron": "^22.2.0", @@ -25,9 +27,10 @@ }, "dependencies": { "bufferutil": "^4.0.8", - "crypto": "^1.0.1", + "https": "^1.0.0", + "node-forge": "^1.3.1", "qrcode": "^1.5.3", - "utf-8-validate": "^6.0.3", + "tls": "^0.0.1", "ws": "^8.14.2" } } diff --git a/receivers/electron/src/DiscoveryService.ts b/receivers/electron/src/DiscoveryService.ts index 594a3d6..94378df 100644 --- a/receivers/electron/src/DiscoveryService.ts +++ b/receivers/electron/src/DiscoveryService.ts @@ -3,7 +3,10 @@ const cp = require('child_process'); const os = require('os'); export class DiscoveryService { - private service: any; + private serviceTcp: any; + private serviceTls: any; + private serviceWs: any; + private serviceWss: any; private static getComputerName() { switch (process.platform) { @@ -20,23 +23,42 @@ export class DiscoveryService { } start() { - if (this.service) { + if (this.serviceTcp || this.serviceTls || this.serviceWs || this.serviceWss) { return; } const name = `FCast-${DiscoveryService.getComputerName()}`; console.log("Discovery service started.", name); - this.service = mdns.createAdvertisement(mdns.tcp('_fcast'), 46899, { name: name }); - this.service.start(); + this.serviceTcp = mdns.createAdvertisement(mdns.tcp('_fcast'), 46899, { name: name }); + this.serviceTcp.start(); + this.serviceTls = mdns.createAdvertisement(mdns.tcp('_fcast-tls'), 46897, { name: name }); + this.serviceTls.start(); + this.serviceWs = mdns.createAdvertisement(mdns.tcp('_fcast-ws'), 46898, { name: name }); + this.serviceWs.start(); + this.serviceWss = mdns.createAdvertisement(mdns.tcp('_fcast-wss'), 46896, { name: name }); + this.serviceWss.start(); } stop() { - if (!this.service) { - return; + if (this.serviceTcp) { + this.serviceTcp.stop(); + this.serviceTcp = null; } - this.service.stop(); - this.service = null; + if (this.serviceTls) { + this.serviceTls.stop(); + this.serviceTls = null; + } + + if (this.serviceWs) { + this.serviceWs.stop(); + this.serviceWs = null; + } + + if (this.serviceWss) { + this.serviceWss.stop(); + this.serviceWss = null; + } } } \ No newline at end of file diff --git a/receivers/electron/src/FCastSession.ts b/receivers/electron/src/FCastSession.ts index d465b28..cbd8a82 100644 --- a/receivers/electron/src/FCastSession.ts +++ b/receivers/electron/src/FCastSession.ts @@ -1,7 +1,6 @@ import net = require('net'); -import * as crypto from 'crypto'; import { EventEmitter } from 'node:events'; -import { DecryptedMessage, EncryptedMessage, KeyExchangeMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets'; +import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets'; import { WebSocket } from 'ws'; enum SessionState { @@ -24,11 +23,8 @@ export enum Opcode { PlaybackError = 9, SetSpeed = 10, Version = 11, - KeyExchange = 12, - Encrypted = 13, - Ping = 14, - Pong = 15, - StartEncryption = 16 + Ping = 12, + Pong = 13 }; const LENGTH_BYTES = 4; @@ -42,52 +38,17 @@ export class FCastSession { writer: (data: Buffer) => void; state: SessionState; emitter = new EventEmitter(); - encryptionStarted = false; - - private aesKey: Buffer; - private dh: crypto.DiffieHellman; - private queuedEncryptedMessages: EncryptedMessage[] = []; constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) { this.socket = socket; this.writer = writer; this.state = SessionState.WaitingForLength; - - this.dh = generateKeyPair(); - - const keyExchangeMessage = getKeyExchangeMessage(this.dh); - console.log(`Sending KeyExchangeMessage: ${keyExchangeMessage}`); - this.send(Opcode.KeyExchange, keyExchangeMessage); } - sendVersion(value: VersionMessage) { - this.send(Opcode.Version, value); - } - - sendPlaybackError(value: PlaybackErrorMessage) { - this.send(Opcode.PlaybackError, value); - } - - sendPlaybackUpdate(value: PlaybackUpdateMessage) { - this.send(Opcode.PlaybackUpdate, value); - } - - sendVolumeUpdate(value: VolumeUpdateMessage) { - this.send(Opcode.VolumeUpdate, value); - } - - private send(opcode: number, message = null) { - if (this.encryptionStarted && opcode != Opcode.Encrypted && opcode != Opcode.KeyExchange && opcode != Opcode.StartEncryption) { - const decryptedMessage: DecryptedMessage = { - opcode, - message - }; - - this.send(Opcode.Encrypted, encryptMessage(this.aesKey, decryptedMessage)); - return; - } - + send(opcode: number, message = null) { const json = message ? JSON.stringify(message) : null; + console.log(`send (opcode: ${opcode}, body: ${json})`); + let data: Uint8Array; if (json) { const utf8Encode = new TextEncoder(); @@ -215,35 +176,9 @@ export class FCastSession { case Opcode.SetSpeed: this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage); break; - case Opcode.KeyExchange: - const keyExchangeMessage = JSON.parse(body) as KeyExchangeMessage; - this.aesKey = computeSharedSecret(this.dh, keyExchangeMessage); - this.send(Opcode.StartEncryption); - - for (const encryptedMessage of this.queuedEncryptedMessages) { - const decryptedMessage = decryptMessage(this.aesKey, encryptedMessage); - this.handlePacket(decryptedMessage.opcode, decryptedMessage.message); - } - - this.queuedEncryptedMessages = []; - break; case Opcode.Ping: this.send(Opcode.Pong); break; - case Opcode.Encrypted: - const encryptedMessage = JSON.parse(body) as EncryptedMessage; - - if (this.aesKey) { - const decryptedMessage = decryptMessage(this.aesKey, encryptedMessage); - this.handlePacket(decryptedMessage.opcode, decryptedMessage.message); - } else { - if (this.queuedEncryptedMessages.length === 15) { - this.queuedEncryptedMessages.shift(); - } - - this.queuedEncryptedMessages.push(encryptedMessage); - } - break; } } catch (e) { console.warn(`Error handling packet from.`, e); @@ -258,52 +193,14 @@ export class FCastSession { console.log('body', body); this.handlePacket(opcode, body); } -} -export function getKeyExchangeMessage(dh: crypto.DiffieHellman): KeyExchangeMessage { - return { version: 1, publicKey: dh.getPublicKey().toString('base64') }; -} - -export function computeSharedSecret(dh: crypto.DiffieHellman, keyExchangeMessage: KeyExchangeMessage): Buffer { - console.log("private", dh.getPrivateKey().toString('base64')); - - const theirPublicKey = Buffer.from(keyExchangeMessage.publicKey, 'base64'); - console.log("theirPublicKey", theirPublicKey.toString('base64')); - const secret = dh.computeSecret(theirPublicKey); - console.log("secret", secret.toString('base64')); - const digest = crypto.createHash('sha256').update(secret).digest(); - console.log("digest", digest.toString('base64')); - return digest; -} - -export function encryptMessage(aesKey: Buffer, decryptedMessage: DecryptedMessage): EncryptedMessage { - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv); - let encrypted = cipher.update(JSON.stringify(decryptedMessage), 'utf8', 'base64'); - encrypted += cipher.final('base64'); - return { - version: 1, - iv: iv.toString('base64'), - blob: encrypted - }; -} - -export function decryptMessage(aesKey: Buffer, encryptedMessage: EncryptedMessage): DecryptedMessage { - const iv = Buffer.from(encryptedMessage.iv, 'base64'); - const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv); - let decrypted = decipher.update(encryptedMessage.blob, 'base64', 'utf8'); - decrypted += decipher.final('utf8'); - return JSON.parse(decrypted) as DecryptedMessage; -} - -export function generateKeyPair() { - const dh = createDiffieHellman(); - dh.generateKeys(); - return dh; -} - -export function createDiffieHellman(): crypto.DiffieHellman { - const p = Buffer.from('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff', 'hex'); - const g = Buffer.from('02', 'hex'); - return crypto.createDiffieHellman(p, g); + bindEvents(emitter: EventEmitter) { + this.emitter.on("play", (body: PlayMessage) => { emitter.emit("play", body) }); + this.emitter.on("pause", () => { emitter.emit("pause") }); + this.emitter.on("resume", () => { emitter.emit("resume") }); + this.emitter.on("stop", () => { emitter.emit("stop") }); + this.emitter.on("seek", (body: SeekMessage) => { emitter.emit("seek", body) }); + this.emitter.on("setvolume", (body: SetVolumeMessage) => { emitter.emit("setvolume", body) }); + this.emitter.on("setspeed", (body: SetSpeedMessage) => { emitter.emit("setspeed", body) }); + } } \ No newline at end of file diff --git a/receivers/electron/src/Main.ts b/receivers/electron/src/Main.ts index 463e8fa..779d041 100644 --- a/receivers/electron/src/Main.ts +++ b/receivers/electron/src/Main.ts @@ -6,7 +6,11 @@ import { DiscoveryService } from './DiscoveryService'; import { Updater } from './Updater'; import { WebSocketListenerService } from './WebSocketListenerService'; import * as os from 'os'; -import * as sodium from 'libsodium-wrappers'; +import { Opcode } from './FCastSession'; +import fs = require('fs'); +import forge = require('node-forge'); +import { TlsListenerService } from './TlsTcpListenerService'; +import { WebSocketSecureListenerService } from './WebSocketSecureListenerService'; export default class Main { static shouldOpenMainWindow = true; @@ -15,8 +19,12 @@ export default class Main { static application: Electron.App; static tcpListenerService: TcpListenerService; static webSocketListenerService: WebSocketListenerService; + static tlsListenerService: TlsListenerService; + static webSocketSecureListenerService: WebSocketSecureListenerService; static discoveryService: DiscoveryService; static tray: Tray; + static key: string = null; + static cert: string = null; private static createTray() { const icon = (process.platform === 'win32') ? path.join(__dirname, 'app.ico') : path.join(__dirname, 'app.png'); @@ -100,8 +108,10 @@ export default class Main { Main.tcpListenerService = new TcpListenerService(); Main.webSocketListenerService = new WebSocketListenerService(); - const listeners = [Main.tcpListenerService, Main.webSocketListenerService]; + Main.tlsListenerService = new TlsListenerService(Main.key, Main.cert); + Main.webSocketSecureListenerService = new WebSocketSecureListenerService(Main.key, Main.cert); + const listeners = [Main.tcpListenerService, Main.webSocketListenerService, Main.tlsListenerService, Main.webSocketSecureListenerService]; listeners.forEach(l => { l.emitter.on("play", (message) => { if (Main.playerWindow == null) { @@ -142,15 +152,15 @@ export default class Main { l.start(); ipcMain.on('send-playback-error', (event: IpcMainEvent, value: PlaybackErrorMessage) => { - l.sendPlaybackError(value); + l.send(Opcode.PlaybackError, value); }); ipcMain.on('send-playback-update', (event: IpcMainEvent, value: PlaybackUpdateMessage) => { - l.sendPlaybackUpdate(value); + l.send(Opcode.PlaybackUpdate, value); }); ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => { - l.sendVolumeUpdate(value); + l.send(Opcode.VolumeUpdate, value); }); }); @@ -196,12 +206,6 @@ export default class Main { } static openMainWindow() { - (async () => { - console.log("waiting for sodium..."); - await sodium.ready; - console.log("sodium ready"); - })(); - if (Main.mainWindow) { Main.mainWindow.focus(); return; @@ -230,6 +234,50 @@ export default class Main { } static main(app: Electron.App) { + if (!fs.existsSync('./cert.pem') || !fs.existsSync('./key.pem')) { + try { + const keys = forge.pki.rsa.generateKeyPair(2048); + + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(9999, 11, 31); + cert.sign(keys.privateKey); + + const pemCert = forge.pki.certificateToPem(cert); + const pemKey = forge.pki.privateKeyToPem(keys.privateKey); + fs.writeFileSync('./cert.pem', pemCert); + fs.writeFileSync('./key.pem', pemKey); + } catch { + console.error("Failed to generate key pair."); + } + } + + try { + Main.key = fs.readFileSync('./key.pem', 'utf8'); + Main.cert = fs.readFileSync('./cert.pem', 'utf8'); + } catch (e) { + console.error("Failed to load key pair.", e); + + dialog.showMessageBox({ + type: 'error', + title: 'Failed to initialize crypto', + message: `The application failed to start properly '${JSON.stringify(e)}'.`, + buttons: ['Restart', 'Close'], + defaultId: 0, + cancelId: 1 + }).then((p) => { + if (p.response === 0) { + app.relaunch(); + app.exit(0); + } else { + app.exit(0); + } + }); + + return; + } + Main.application = app; const argv = process.argv; if (argv.includes('--no-main-window')) { diff --git a/receivers/electron/src/Packets.ts b/receivers/electron/src/Packets.ts index 67f9c00..f907493 100644 --- a/receivers/electron/src/Packets.ts +++ b/receivers/electron/src/Packets.ts @@ -53,26 +53,4 @@ export class VersionMessage { constructor( public version: number, ) {} -} - -export class KeyExchangeMessage { - constructor( - public version: number, - public publicKey: string - ) {} -} - -export class DecryptedMessage { - constructor( - public opcode: number, - public message: string | undefined - ) {} -} - -export class EncryptedMessage { - constructor( - public version: number, - public iv: string | undefined, - public blob: string - ) {} } \ No newline at end of file diff --git a/receivers/electron/src/TcpListenerService.ts b/receivers/electron/src/TcpListenerService.ts index 100da56..213271c 100644 --- a/receivers/electron/src/TcpListenerService.ts +++ b/receivers/electron/src/TcpListenerService.ts @@ -1,11 +1,12 @@ import net = require('net'); -import { FCastSession } from './FCastSession'; +import { FCastSession, Opcode } from './FCastSession'; import { EventEmitter } from 'node:events'; -import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets'; import { dialog } from 'electron'; import Main from './Main'; export class TcpListenerService { + public static PORT = 46899; + emitter = new EventEmitter(); private server: net.Server; @@ -17,7 +18,7 @@ export class TcpListenerService { } this.server = net.createServer() - .listen(46899) + .listen(TcpListenerService.PORT) .on("connection", this.handleConnection.bind(this)) .on("error", this.handleServerError.bind(this)); } @@ -33,14 +34,10 @@ export class TcpListenerService { server.close(); } - - - sendPlaybackError(value: PlaybackErrorMessage) { - console.info("Sending playback error.", value); - + send(opcode: number, message = null) { this.sessions.forEach(session => { try { - session.sendPlaybackError(value); + session.send(opcode, message); } catch (e) { console.warn("Failed to send error.", e); session.close(); @@ -48,32 +45,6 @@ export class TcpListenerService { }); } - sendPlaybackUpdate(value: PlaybackUpdateMessage) { - console.info("Sending playback update.", value); - - this.sessions.forEach(session => { - try { - session.sendPlaybackUpdate(value); - } catch (e) { - console.warn("Failed to send update.", e); - session.close(); - } - }); - } - - sendVolumeUpdate(value: VolumeUpdateMessage) { - console.info("Sending volume update.", value); - - this.sessions.forEach(session => { - try { - session.sendVolumeUpdate(value); - } catch (e) { - console.warn("Failed to send update.", e); - session.close(); - } - }); - } - private async handleServerError(err: NodeJS.ErrnoException) { console.error("Server error:", err); @@ -98,13 +69,7 @@ export class TcpListenerService { console.log(`new connection from ${socket.remoteAddress}:${socket.remotePort}`); const session = new FCastSession(socket, (data) => socket.write(data)); - session.emitter.on("play", (body: PlayMessage) => { this.emitter.emit("play", body) }); - session.emitter.on("pause", () => { this.emitter.emit("pause") }); - session.emitter.on("resume", () => { this.emitter.emit("resume") }); - session.emitter.on("stop", () => { this.emitter.emit("stop") }); - session.emitter.on("seek", (body: SeekMessage) => { this.emitter.emit("seek", body) }); - session.emitter.on("setvolume", (body: SetVolumeMessage) => { this.emitter.emit("setvolume", body) }); - session.emitter.on("setspeed", (body: SetSpeedMessage) => { this.emitter.emit("setspeed", body) }); + session.bindEvents(this.emitter); this.sessions.push(session); socket.on("error", (err) => { @@ -130,7 +95,7 @@ export class TcpListenerService { try { console.log('Sending version'); - session.sendVersion({version: 2}); + session.send(Opcode.Version, {version: 2}); } catch (e) { console.log('Failed to send version'); } diff --git a/receivers/electron/src/TlsTcpListenerService.ts b/receivers/electron/src/TlsTcpListenerService.ts new file mode 100644 index 0000000..e3916ac --- /dev/null +++ b/receivers/electron/src/TlsTcpListenerService.ts @@ -0,0 +1,105 @@ +import tls = require('tls'); +import { FCastSession, Opcode } from './FCastSession'; +import { EventEmitter } from 'node:events'; +import { dialog } from 'electron'; +import Main from './Main'; + +export class TlsListenerService { + public static PORT = 46897; + + emitter = new EventEmitter(); + + private server: tls.Server; + private sessions: FCastSession[] = []; + + constructor(private key: string, private cert: string) {} + + start() { + if (this.server != null) { + return; + } + + const options: tls.TlsOptions = {key: this.key, cert: this.cert}; + this.server = tls.createServer(options).listen(TlsListenerService.PORT) + .on("secureConnection", this.handleConnection.bind(this)) + .on("error", this.handleServerError.bind(this)); + } + + stop() { + if (this.server == null) { + return; + } + + const server = this.server; + this.server = null; + + server.close(); + } + + send(opcode: number, message = null) { + this.sessions.forEach(session => { + try { + session.send(opcode, message); + } catch (e) { + console.warn("Failed to send error.", e); + session.close(); + } + }); + } + + private async handleServerError(err: NodeJS.ErrnoException) { + console.error("Server error:", err); + + const restartPrompt = await dialog.showMessageBox({ + type: 'error', + title: 'Failed to start', + message: 'The application failed to start properly.', + buttons: ['Restart', 'Close'], + defaultId: 0, + cancelId: 1 + }); + + if (restartPrompt.response === 0) { + Main.application.relaunch(); + Main.application.exit(0); + } else { + Main.application.exit(0); + } + } + + private handleConnection(socket: tls.TLSSocket) { + console.log(`new secure connection from ${socket.remoteAddress}:${socket.remotePort}`); + + const session = new FCastSession(socket, (data) => socket.write(data)); + session.bindEvents(this.emitter); + this.sessions.push(session); + + socket.on("error", (err) => { + console.warn(`Error from ${socket.remoteAddress}:${socket.remotePort}.`, err); + socket.destroy(); + }); + + socket.on("data", buffer => { + try { + session.processBytes(buffer); + } catch (e) { + console.warn(`Error while handling packet from ${socket.remoteAddress}:${socket.remotePort}.`, e); + socket.end(); + } + }); + + socket.on("close", () => { + const index = this.sessions.indexOf(session); + if (index != -1) { + this.sessions.splice(index, 1); + } + }); + + try { + console.log('Sending version'); + session.send(Opcode.Version, {version: 2}); + } catch (e) { + console.log('Failed to send version'); + } + } +} \ No newline at end of file diff --git a/receivers/electron/src/WebSocketListenerService.ts b/receivers/electron/src/WebSocketListenerService.ts index 44d0135..6667fbe 100644 --- a/receivers/electron/src/WebSocketListenerService.ts +++ b/receivers/electron/src/WebSocketListenerService.ts @@ -1,11 +1,12 @@ -import { FCastSession } from './FCastSession'; +import { FCastSession, Opcode } from './FCastSession'; import { EventEmitter } from 'node:events'; -import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets'; import { dialog } from 'electron'; import Main from './Main'; import { WebSocket, WebSocketServer } from 'ws'; export class WebSocketListenerService { + public static PORT = 46898; + emitter = new EventEmitter(); private server: WebSocketServer; @@ -16,7 +17,7 @@ export class WebSocketListenerService { return; } - this.server = new WebSocketServer({ port: 46898 }) + this.server = new WebSocketServer({ port: WebSocketListenerService.PORT }) .on("connection", this.handleConnection.bind(this)) .on("error", this.handleServerError.bind(this)); } @@ -32,12 +33,10 @@ export class WebSocketListenerService { server.close(); } - sendPlaybackError(value: PlaybackErrorMessage) { - console.info("Sending playback error.", value); - + send(opcode: number, message = null) { this.sessions.forEach(session => { try { - session.sendPlaybackError(value); + session.send(opcode, message); } catch (e) { console.warn("Failed to send error.", e); session.close(); @@ -45,32 +44,6 @@ export class WebSocketListenerService { }); } - sendPlaybackUpdate(value: PlaybackUpdateMessage) { - console.info("Sending playback update.", value); - - this.sessions.forEach(session => { - try { - session.sendPlaybackUpdate(value); - } catch (e) { - console.warn("Failed to send update.", e); - session.close(); - } - }); - } - - sendVolumeUpdate(value: VolumeUpdateMessage) { - console.info("Sending volume update.", value); - - this.sessions.forEach(session => { - try { - session.sendVolumeUpdate(value); - } catch (e) { - console.warn("Failed to send update.", e); - session.close(); - } - }); - } - private async handleServerError(err: NodeJS.ErrnoException) { console.error("Server error:", err); @@ -95,13 +68,7 @@ export class WebSocketListenerService { console.log('New WebSocket connection'); const session = new FCastSession(socket, (data) => socket.send(data)); - session.emitter.on("play", (body: PlayMessage) => { this.emitter.emit("play", body) }); - session.emitter.on("pause", () => { this.emitter.emit("pause") }); - session.emitter.on("resume", () => { this.emitter.emit("resume") }); - session.emitter.on("stop", () => { this.emitter.emit("stop") }); - session.emitter.on("seek", (body: SeekMessage) => { this.emitter.emit("seek", body) }); - session.emitter.on("setvolume", (body: SetVolumeMessage) => { this.emitter.emit("setvolume", body) }); - session.emitter.on("setspeed", (body: SetSpeedMessage) => { this.emitter.emit("setspeed", body) }); + session.bindEvents(this.emitter); this.sessions.push(session); socket.on("error", (err) => { @@ -133,7 +100,7 @@ export class WebSocketListenerService { try { console.log('Sending version'); - session.sendVersion({version: 2}); + session.send(Opcode.Version, {version: 2}); } catch (e) { console.log('Failed to send version'); } diff --git a/receivers/electron/src/WebSocketSecureListenerService.ts b/receivers/electron/src/WebSocketSecureListenerService.ts new file mode 100644 index 0000000..173870f --- /dev/null +++ b/receivers/electron/src/WebSocketSecureListenerService.ts @@ -0,0 +1,118 @@ +import { FCastSession, Opcode } from './FCastSession'; +import { EventEmitter } from 'node:events'; +import { dialog } from 'electron'; +import Main from './Main'; +import { WebSocket, WebSocketServer } from 'ws'; +import * as https from 'https'; + +export class WebSocketSecureListenerService { + public static PORT = 46896; + + emitter = new EventEmitter(); + + private server: WebSocketServer; + private sessions: FCastSession[] = []; + private httpsServer: https.Server; + + constructor(private key: string, private cert: string) {} + + start() { + if (this.server != null || this.httpsServer != null) { + return; + } + + this.httpsServer = https.createServer({key: this.key, cert: this.cert}); + this.httpsServer.listen(WebSocketSecureListenerService.PORT); + + this.server = new WebSocketServer({server: this.httpsServer}) + .on("connection", this.handleConnection.bind(this)) + .on("error", this.handleServerError.bind(this)); + } + + stop() { + if (this.server != null) { + const server = this.server; + this.server = null; + server.close(); + } + + if (this.httpsServer != null) { + const httpsServer = this.httpsServer; + this.httpsServer = null; + httpsServer.close(); + } + } + + send(opcode: number, message = null) { + this.sessions.forEach(session => { + try { + session.send(opcode, message); + } catch (e) { + console.warn("Failed to send error.", e); + session.close(); + } + }); + } + + private async handleServerError(err: NodeJS.ErrnoException) { + console.error("Server error:", err); + + const restartPrompt = await dialog.showMessageBox({ + type: 'error', + title: 'Failed to start', + message: 'The application failed to start properly.', + buttons: ['Restart', 'Close'], + defaultId: 0, + cancelId: 1 + }); + + if (restartPrompt.response === 0) { + Main.application.relaunch(); + Main.application.exit(0); + } else { + Main.application.exit(0); + } + } + + private handleConnection(socket: WebSocket) { + console.log('New WebSocketSecure connection'); + + const session = new FCastSession(socket, (data) => socket.send(data)); + session.bindEvents(this.emitter); + this.sessions.push(session); + + socket.on("error", (err) => { + console.warn(`Error.`, err); + session.close(); + }); + + socket.on('message', data => { + try { + if (data instanceof Buffer) { + session.processBytes(data); + } else { + console.warn("Received unhandled string message", data); + } + } catch (e) { + console.warn(`Error while handling packet.`, e); + session.close(); + } + }); + + socket.on("close", () => { + console.log('WebSocketSecure connection closed'); + + const index = this.sessions.indexOf(session); + if (index != -1) { + this.sessions.splice(index, 1); + } + }); + + try { + console.log('Sending version'); + session.send(Opcode.Version, {version: 2}); + } catch (e) { + console.log('Failed to send version'); + } + } +} \ No newline at end of file diff --git a/receivers/electron/src/main/index.html b/receivers/electron/src/main/index.html index 3345d1d..40a8f69 100644 --- a/receivers/electron/src/main/index.html +++ b/receivers/electron/src/main/index.html @@ -22,8 +22,8 @@
Manual connection information
IPs

-
Port
46899 (TCP), 46898 (WS)
-
+
Port
46899 (TCP), 46898 (WS), 46897 (TLS), 46896 (WSS)
+
Automatic discovery is available via mDNS
Scan to connect
diff --git a/receivers/electron/src/main/renderer.js b/receivers/electron/src/main/renderer.js index 4448bb5..d6e4f9a 100644 --- a/receivers/electron/src/main/renderer.js +++ b/receivers/electron/src/main/renderer.js @@ -22,8 +22,10 @@ window.electronAPI.onDeviceInfo((_event, value) => { name: value.name, addresses: value.addresses, services: [ - { port: 46899, type: 0 }, - { port: 46898, type: 1 } + { port: 46899, type: 0 }, //TCP + { port: 46898, type: 1 }, //WS + { port: 46897, type: 2 }, //TCP-TLS + { port: 46896, type: 3 } //WSS ] }; diff --git a/receivers/electron/test/EncryptionTests.test.ts b/receivers/electron/test/EncryptionTests.test.ts deleted file mode 100644 index 6c538b7..0000000 --- a/receivers/electron/test/EncryptionTests.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { EncryptedMessage, DecryptedMessage, KeyExchangeMessage } from '../src/Packets'; -import { generateKeyPair, computeSharedSecret, encryptMessage, decryptMessage, createDiffieHellman, Opcode } from '../src/FCastSession'; - -/*test("testDHEncryptionSelf", () => { - const keyPair1 = generateKeyPair(); - const keyPair2 = generateKeyPair(); - - const aesKey1 = computeSharedSecret(keyPair1, { version:1, publicKey: keyPair2.getPublicKey().toString('base64') }); - const aesKey2 = computeSharedSecret(keyPair2, { version:1, publicKey: keyPair1.getPublicKey().toString('base64') }); - - expect(aesKey1.toString('base64')).toBe(aesKey2.toString('base64')); - - const message: DecryptedMessage = { opcode: 1, message: 'text/html' }; - const encryptedMessage: EncryptedMessage = encryptMessage(aesKey1, message); - const decryptedMessage: DecryptedMessage = decryptMessage(aesKey1, encryptedMessage); - - expect(decryptedMessage.opcode).toBe(message.opcode); - expect(decryptedMessage.message).toBe(message.message); -});*/ - -test("testDHEncryptionKnown", () => { - const encodedPrivateKey1 = "MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA=="; - const keyExchangeMessage2: KeyExchangeMessage = { version: 1, publicKey: "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=" }; - - const dh = createDiffieHellman(); - dh.setPrivateKey(Buffer.from(encodedPrivateKey1, 'base64')); - - const aesKey1 = computeSharedSecret(dh, keyExchangeMessage2); - expect(aesKey1.toString('base64')).toBe("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE="); - - const message = { opcode: 1, message: 'text/html' }; - const serializedBody = JSON.stringify(message); - const encryptedMessage = encryptMessage(aesKey1, message as DecryptedMessage); - const decryptedMessage = decryptMessage(aesKey1, encryptedMessage as EncryptedMessage); - - expect(decryptedMessage.opcode).toBe(1); - expect(decryptedMessage.message).toBe(serializedBody); -}); - -/*test("testAESKeyGeneration", () => { - const testCases = [ - { - publicKey: "MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF", - privateKey: "MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=", - expectedAES: "7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk=" - }, - { - publicKey: "MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK", - privateKey: "MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=", - expectedAES: "a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q=" - } - ]; - - testCases.forEach(({ publicKey, privateKey, expectedAES }) => { - const theirPublicKey = Buffer.from(publicKey, 'base64'); - const dh = createDiffieHellman(); - dh.setPrivateKey(Buffer.from(privateKey, 'base64')); - const aesKey = computeSharedSecret(dh, { version: 1, publicKey: theirPublicKey.toString('base64') }); - expect(aesKey.toString('base64')).toBe(expectedAES); - }); -});*/ - -/*test("testDecryptMessageKnown", () => { - const encryptedMessage: EncryptedMessage = { - version: 1, - iv: "C4H70VC5FWrNtkty9/cLIA==", - blob: "K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==" - }; - const aesKeyBase64 = "+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU="; - - const aesKey = Buffer.from(aesKeyBase64, 'base64'); - const decryptedMessage = decryptMessage(aesKey, encryptedMessage); - - expect(decryptedMessage.opcode).toBe(Opcode.Play); - expect(decryptedMessage.message).toBe("{\"container\":\"text/html\"}"); -});*/ \ No newline at end of file