1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Finished implementation of old crypto system as much until course change.

This commit is contained in:
Koen 2023-12-21 14:18:47 +01:00
parent 137a6f3178
commit b8bd78d90d
20 changed files with 4143 additions and 118 deletions

View file

@ -31,6 +31,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -52,6 +58,15 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@ -71,7 +86,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
dependencies = [
"atty",
"bitflags",
"bitflags 1.3.2",
"clap_lex",
"indexmap",
"strsim",
@ -139,6 +154,7 @@ version = "0.1.0"
dependencies = [
"clap",
"ctrlc",
"openssl",
"serde",
"serde_json",
"tiny_http",
@ -152,6 +168,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -264,12 +295,56 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"cfg-if",
"libc",
"static_assertions",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
version = "0.10.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-sys"
version = "0.9.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "os_str_bytes"
version = "6.5.0"
@ -282,6 +357,12 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -532,6 +613,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"

View file

@ -13,3 +13,4 @@ ctrlc = "3.1.9"
tungstenite = "0.21.0"
url = "2.5.0"
tiny_http = "0.12.0"
openssl = "0.10.61"

View file

@ -1,6 +1,7 @@
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
use std::{sync::{atomic::{AtomicBool, Ordering}, Arc}, collections::VecDeque};
use crate::{models::{PlaybackUpdateMessage, VolumeUpdateMessage, PlaybackErrorMessage, VersionMessage}, transport::Transport};
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 serde::Serialize;
#[derive(Debug)]
@ -11,7 +12,7 @@ enum SessionState {
Disconnected,
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Opcode {
None = 0,
Play = 1,
@ -24,7 +25,12 @@ pub enum Opcode {
SetVolume = 8,
PlaybackError = 9,
SetSpeed = 10,
Version = 11
Version = 11,
KeyExchange = 12,
Encrypted = 13,
Ping = 14,
Pong = 15,
StartEncryption = 16
}
impl Opcode {
@ -42,6 +48,11 @@ 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,
_ => panic!("Unknown value: {}", value),
}
}
@ -55,24 +66,60 @@ pub struct FCastSession<'a> {
bytes_read: usize,
packet_length: usize,
stream: Box<dyn Transport + 'a>,
state: SessionState
state: SessionState,
dh: Option<Dh<Private>>,
public_key: Option<String>,
aes_key: Option<Vec<u8>>,
decrypted_messages_queue: VecDeque<DecryptedMessage>,
encrypted_messages_queue: VecDeque<EncryptedMessage>,
encryption_started: bool,
wait_for_encryption: bool
}
impl<'a> FCastSession<'a> {
pub fn new<T: Transport + 'a>(stream: T) -> Self {
FCastSession {
pub fn new<T: Transport + 'a>(stream: T, encrypted: bool) -> Result<Self, Box<dyn std::error::Error>> {
let (dh, public_key) = if encrypted {
println!("Initialized DH.");
generate_key_pair()?
} else {
(None, None)
};
Ok(FCastSession {
buffer: vec![0; MAXIMUM_PACKET_LENGTH],
bytes_read: 0,
packet_length: 0,
stream: Box::new(stream),
state: SessionState::Idle
}
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
})
}
}
impl FCastSession<'_> {
pub fn send_message<T: Serialize>(&mut self, opcode: Opcode, message: T) -> Result<(), std::io::Error> {
pub fn send_message<T: Serialize>(&mut self, opcode: Opcode, message: T) -> Result<(), Box<dyn std::error::Error>> {
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;
@ -81,11 +128,25 @@ impl FCastSession<'_> {
header[LENGTH_BYTES] = opcode as u8;
let packet = [header, data.to_vec()].concat();
println!("Sent {} bytes with (header size: {}, body size: {}).", packet.len(), header_size, data.len());
return self.stream.transport_write(&packet);
println!("Sent {} bytes with (opcode: {:?}, header size: {}, body size: {}, body: {}).", packet.len(), opcode, header_size, data.len(), json);
self.stream.transport_write(&packet)?;
Ok(())
}
pub fn send_empty(&mut self, opcode: Opcode) -> Result<(), std::io::Error> {
pub fn send_empty(&mut self, opcode: Opcode) -> Result<(), Box<dyn std::error::Error>> {
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();
@ -94,14 +155,26 @@ impl FCastSession<'_> {
header[LENGTH_BYTES] = opcode as u8;
let packet = [header, data.to_vec()].concat();
return self.stream.transport_write(&packet);
self.stream.transport_write(&packet)?;
Ok(())
}
pub fn receive_loop(&mut self, running: &Arc<AtomicBool>) -> Result<(), Box<dyn std::error::Error>> {
pub fn receive_loop(&mut self, running: &Arc<AtomicBool>, until_queues_are_empty: bool) -> Result<(), Box<dyn std::error::Error>> {
if let Some(pk) = &self.public_key {
println!("Sending public key.");
self.send_message(Opcode::KeyExchange, &KeyExchangeMessage::new(1, pk.clone()))?;
}
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])?;
}
@ -110,7 +183,7 @@ impl FCastSession<'_> {
Ok(())
}
pub fn process_bytes(&mut self, received_bytes: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
fn process_bytes(&mut self, received_bytes: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
if received_bytes.is_empty() {
return Ok(());
}
@ -172,7 +245,7 @@ impl FCastSession<'_> {
if self.bytes_read >= self.packet_length {
println!("Packet finished receiving of {} bytes.", self.packet_length);
self.handle_packet()?;
self.handle_next_packet()?;
self.state = SessionState::WaitingForLength;
self.packet_length = 0;
@ -187,45 +260,129 @@ impl FCastSession<'_> {
Ok(())
}
fn handle_packet(&mut self) -> Result<(), std::str::Utf8Error> {
fn handle_next_packet(&mut self) -> Result<(), Box<dyn std::error::Error>> {
println!("Processing packet of {} bytes", self.bytes_read);
let opcode = Opcode::from_u8(self.buffer[0]);
let body = if self.packet_length > 1 {
Some(std::str::from_utf8(&self.buffer[1..self.packet_length])?)
let packet_length = self.packet_length;
let body = if packet_length > 1 {
Some(std::str::from_utf8(&self.buffer[1..packet_length])?.to_string())
} else {
None
};
println!("Received body: {:?}", body);
self.handle_packet(opcode, body)
}
fn handle_packet(&mut self, opcode: Opcode, body: Option<String>) -> Result<(), Box<dyn std::error::Error>> {
println!("Received message with opcode {:?}.", opcode);
match opcode {
Opcode::PlaybackUpdate => {
if let Some(body_str) = body {
if let Ok(playback_update_msg) = serde_json::from_str::<PlaybackUpdateMessage>(body_str) {
if let Ok(playback_update_msg) = serde_json::from_str::<PlaybackUpdateMessage>(body_str.as_str()) {
println!("Received playback update {:?}", playback_update_msg);
} else {
println!("Received playback update with malformed body.");
}
} else {
println!("Received playback update with no body.");
}
}
Opcode::VolumeUpdate => {
if let Some(body_str) = body {
if let Ok(volume_update_msg) = serde_json::from_str::<VolumeUpdateMessage>(body_str) {
if let Ok(volume_update_msg) = serde_json::from_str::<VolumeUpdateMessage>(body_str.as_str()) {
println!("Received volume update {:?}", volume_update_msg);
} else {
println!("Received volume update with malformed body.");
}
} else {
println!("Received volume update with no body.");
}
}
Opcode::PlaybackError => {
if let Some(body_str) = body {
if let Ok(playback_error_msg) = serde_json::from_str::<PlaybackErrorMessage>(body_str) {
if let Ok(playback_error_msg) = serde_json::from_str::<PlaybackErrorMessage>(body_str.as_str()) {
println!("Received playback error {:?}", playback_error_msg);
} else {
println!("Received playback error with malformed body.");
}
} else {
println!("Received playback error with no body.");
}
}
Opcode::Version => {
if let Some(body_str) = body {
if let Ok(version_msg) = serde_json::from_str::<VersionMessage>(body_str) {
if let Ok(version_msg) = serde_json::from_str::<VersionMessage>(body_str.as_str()) {
println!("Received version {:?}", version_msg);
} else {
println!("Received version with malformed body.");
}
} else {
println!("Received version with no body.");
}
}
Opcode::KeyExchange => {
if let Some(body_str) = body {
match serde_json::from_str::<KeyExchangeMessage>(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::<EncryptedMessage>(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)?;
}
}
_ => {
@ -236,7 +393,184 @@ 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<Dh<Private>>, Option<String>), Box<dyn std::error::Error>> {
//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<u8>, decrypted_message: &DecryptedMessage) -> Result<EncryptedMessage, Box<dyn std::error::Error>> {
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<u8>, encrypted_message: &EncryptedMessage) -> Result<DecryptedMessage, Box<dyn std::error::Error>> {
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<Private>, key_exchange_message: &KeyExchangeMessage) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
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");
}
}
}

View file

@ -37,6 +37,13 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.required(false)
.default_value("tcp")
.takes_value(true))
.arg(Arg::with_name("encrypted")
.short('e')
.long("encrypted")
.value_name("Encrypted")
.help("Use encryption")
.required(false)
.takes_value(false))
.arg(Arg::with_name("host")
.short('h')
.long("host")
@ -154,13 +161,15 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
}
};
let encrypted = matches.is_present("encrypted");
let local_ip: Option<IpAddr>;
let mut session = match connection_type {
"tcp" => {
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)
FCastSession::new(stream, encrypted)?
},
"ws" => {
println!("Connecting via WebSocket to host={} port={}...", host, port);
@ -170,7 +179,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
MaybeTlsStream::Plain(ref stream) => Some(stream.local_addr()?.ip()),
_ => None
};
FCastSession::new(stream)
FCastSession::new(stream, encrypted)?
},
_ => return Err("Invalid connection type. Use 'tcp' or 'websocket'.".into()),
};
@ -256,14 +265,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string(&play_message);
println!("Sent play {:?}", json);
session.send_message(Opcode::Play, Some(play_message))?;
session.send_message(Opcode::Play, &play_message)?;
} else if let Some(seek_matches) = matches.subcommand_matches("seek") {
let seek_message = SeekMessage::new(match seek_matches.value_of("timestamp") {
Some(s) => s.parse::<f64>()?,
_ => return Err("Timestamp is required.".into())
});
println!("Sent seek {:?}", seek_message);
session.send_message(Opcode::Seek, Some(seek_message))?;
session.send_message(Opcode::Seek, &seek_message)?;
} else if let Some(_) = matches.subcommand_matches("pause") {
println!("Sent pause");
session.send_empty(Opcode::Pause)?;
@ -286,7 +295,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
println!("Waiting for Ctrl+C...");
session.receive_loop(&running)?;
session.receive_loop(&running, false)?;
println!("Ctrl+C received, exiting...");
} else if let Some(setvolume_matches) = matches.subcommand_matches("setvolume") {
@ -295,19 +304,30 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
_ => return Err("Timestamp is required.".into())
});
println!("Sent setvolume {:?}", setvolume_message);
session.send_message(Opcode::SetVolume, Some(setvolume_message))?;
session.send_message(Opcode::SetVolume, &setvolume_message)?;
} else if let Some(setspeed_matches) = matches.subcommand_matches("setspeed") {
let setspeed_message = SetSpeedMessage::new(match setspeed_matches.value_of("speed") {
Some(s) => s.parse::<f64>()?,
_ => return Err("Speed is required.".into())
});
println!("Sent setspeed {:?}", setspeed_message);
session.send_message(Opcode::SetSpeed, Some(setspeed_message))?;
session.send_message(Opcode::SetSpeed, &setspeed_message)?;
} else {
println!("Invalid command. Use --help for more information.");
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() {

View file

@ -69,4 +69,42 @@ 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<String>,
}
impl DecryptedMessage {
pub fn new(opcode: u64, message: Option<String>) -> Self {
Self { opcode, message }
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct EncryptedMessage {
pub version: u64,
pub iv: Option<String>,
pub blob: String,
}
impl EncryptedMessage {
pub fn new(version: u64, iv: Option<String>, blob: String) -> Self {
Self { version, iv, blob }
}
}

View file

@ -4,15 +4,15 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View file

@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="19" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -0,0 +1,113 @@
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<EncryptedMessage>("{\"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)
}
}

View file

@ -1,24 +0,0 @@
package com.futo.fcast.receiver
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.futo.fcast.receiver", appContext.packageName)
}
}

View file

@ -1,14 +1,28 @@
package com.futo.fcast.receiver
import android.util.Base64
import android.util.Log
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.DataOutputStream
import java.io.OutputStream
import java.math.BigInteger
import java.net.SocketAddress
import java.nio.ByteBuffer
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.spec.X509EncodedKeySpec
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.KeyAgreement
import javax.crypto.spec.DHParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
enum class SessionState {
Idle,
@ -29,7 +43,17 @@ enum class Opcode(val value: Byte) {
SetVolume(8),
PlaybackError(9),
SetSpeed(10),
Version(11)
Version(11),
KeyExchange(12),
Encrypted(13),
Ping(14),
Pong(15),
StartEncryption(16);
companion object {
private val _map = values().associateBy { it.value }
fun find(value: Byte): Opcode = _map[value] ?: Opcode.None
}
}
const val LENGTH_BYTES = 4
@ -41,8 +65,16 @@ 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<EncryptedMessage>()
private var _encryptionStarted = false
val id = UUID.randomUUID()
init {
send(Opcode.KeyExchange, getKeyExchangeMessage(_keyPair))
}
fun sendVersion(value: VersionMessage) {
send(Opcode.Version, value)
}
@ -59,17 +91,15 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
send(Opcode.VolumeUpdate, value)
}
private inline fun <reified T> send(opcode: Opcode, message: T) {
try {
val data: ByteArray
var jsonString: String? = null
if (message != null) {
jsonString = Json.encodeToString(message)
data = jsonString.encodeToByteArray()
} else {
data = ByteArray(0)
}
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
}
try {
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
val size = 1 + data.size
val outputStream = _outputStream
if (outputStream == null) {
@ -92,13 +122,22 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
outputStream.write(data)
}
Log.d(TAG, "Sent $size bytes: '$jsonString'.")
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
} catch (e: Throwable) {
Log.i(TAG, "Failed to send message ${id}.", e)
throw e
}
}
private inline fun <reified T> send(opcode: Opcode, message: T) {
try {
send(opcode, message?.let { Json.encodeToString(it) })
} catch (e: Throwable) {
Log.i(TAG, "Failed to encode message to string ${id}.", e)
throw e
}
}
fun processBytes(data: ByteBuffer) {
Log.i(TAG, "${data.remaining()} bytes received from ${_remoteSocketAddress}")
if (!data.hasArray()) {
@ -171,7 +210,7 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
if (_bytesRead >= _packetLength) {
Log.i(TAG, "Packet finished receiving from ${_remoteSocketAddress} of $_packetLength bytes.")
handlePacket()
handleNextPacket()
_state = SessionState.WaitingForLength
_packetLength = 0
@ -184,14 +223,19 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
}
}
private fun handlePacket() {
private fun handleNextPacket() {
Log.i(TAG, "Processing packet of $_bytesRead bytes from ${_remoteSocketAddress}")
val opcode = Opcode.values().firstOrNull { it.value == _buffer[0] } ?: Opcode.None
val opcode = Opcode.find(_buffer[0])
val body = if (_packetLength > 1) _buffer.copyOfRange(1, _packetLength)
.toString(Charsets.UTF_8) else null
Log.i(TAG, "Received packet (opcode: ${opcode}, body: '${body}')")
handlePacket(opcode, body)
}
private fun handlePacket(opcode: Opcode, body: String?) {
Log.i(TAG, "Processing packet (opcode: $opcode, size: ${body?.length ?: 0}, from ${_remoteSocketAddress})")
try {
when (opcode) {
@ -202,15 +246,109 @@ 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) {
Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')")
Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')", e)
}
}
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))
}
}
}

View file

@ -49,4 +49,23 @@ 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
)

View file

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['<rootDir>/test/**/*.test.ts'],
modulePathIgnorePatterns: ["<rootDir>/packaging/fcast/fcast-receiver-linux-x64/resources/app/package.json"],
};

File diff suppressed because it is too large Load diff

View file

@ -7,13 +7,17 @@
"license": "MIT",
"scripts": {
"build": "rm -rf dist/ && webpack --config ./webpack.config.js && cp -r src/player dist/player && cp -r src/main dist/main && cp app.ico dist/ && cp app.png dist/",
"start": "npm run build && electron ."
"start": "npm run build && electron .",
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/workerpool": "^6.1.1",
"@types/ws": "^8.5.10",
"electron": "^22.2.0",
"jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
@ -21,6 +25,7 @@
},
"dependencies": {
"bufferutil": "^4.0.8",
"crypto": "^1.0.1",
"qrcode": "^1.5.3",
"utf-8-validate": "^6.0.3",
"ws": "^8.14.2"

View file

@ -1,6 +1,7 @@
import net = require('net');
import * as crypto from 'crypto';
import { EventEmitter } from 'node:events';
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
import { DecryptedMessage, EncryptedMessage, KeyExchangeMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
import { WebSocket } from 'ws';
enum SessionState {
@ -10,7 +11,7 @@ enum SessionState {
Disconnected,
};
enum Opcode {
export enum Opcode {
None = 0,
Play = 1,
Pause = 2,
@ -22,7 +23,12 @@ enum Opcode {
SetVolume = 8,
PlaybackError = 9,
SetSpeed = 10,
Version = 11
Version = 11,
KeyExchange = 12,
Encrypted = 13,
Ping = 14,
Pong = 15,
StartEncryption = 16
};
const LENGTH_BYTES = 4;
@ -36,11 +42,22 @@ 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) {
@ -60,6 +77,16 @@ export class FCastSession {
}
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;
}
const json = message ? JSON.stringify(message) : null;
let data: Uint8Array;
if (json) {
@ -149,7 +176,7 @@ export class FCastSession {
if (this.bytesRead >= this.packetLength) {
console.log(`Packet finished receiving from of ${this.packetLength} bytes.`);
this.handlePacket();
this.handleNextPacket();
this.state = SessionState.WaitingForLength;
this.packetLength = 0;
@ -162,12 +189,8 @@ export class FCastSession {
}
}
private handlePacket() {
console.log(`Processing packet of ${this.bytesRead} bytes from`);
const opcode = this.buffer[0];
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
console.log('body', body);
private handlePacket(opcode: number, body: string | undefined) {
console.log(`handlePacket (opcode: ${opcode}, body: ${body})`);
try {
switch (opcode) {
@ -192,9 +215,95 @@ 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);
}
}
private handleNextPacket() {
console.log(`Processing packet of ${this.bytesRead} bytes from`);
const opcode = this.buffer[0];
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
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);
}

View file

@ -6,6 +6,7 @@ import { DiscoveryService } from './DiscoveryService';
import { Updater } from './Updater';
import { WebSocketListenerService } from './WebSocketListenerService';
import * as os from 'os';
import * as sodium from 'libsodium-wrappers';
export default class Main {
static shouldOpenMainWindow = true;
@ -195,6 +196,12 @@ 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;

View file

@ -53,4 +53,26 @@ 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
) {}
}

View file

@ -33,6 +33,8 @@ export class TcpListenerService {
server.close();
}
sendPlaybackError(value: PlaybackErrorMessage) {
console.info("Sending playback error.", value);

View file

@ -0,0 +1,76 @@
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\"}");
});*/

View file

@ -11,5 +11,5 @@
"suppressImplicitAnyIndexErrors": true ,
"outDir": "dist"
},
"exclude": [ "node_modules" ]
"exclude": [ "node_modules", "test" ]
}