diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 01e7d4d0..11939ca0 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -57,4 +57,4 @@ jobs: - name: Dependency Review uses: actions/dependency-review-action@v4 with: - allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, Unicode-3.0 + allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, Unicode-3.0, Unlicense diff --git a/Cargo.lock b/Cargo.lock index 75f0841e..79b8136b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -115,6 +150,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -142,6 +201,17 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "4.5.47" @@ -277,9 +347,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "deranged" version = "0.5.3" @@ -332,6 +412,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.3" @@ -360,6 +461,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hpack-patched" version = "0.3.0" @@ -373,8 +480,11 @@ dependencies = [ name = "huginn-net" version = "1.4.5" dependencies = [ + "aes-gcm", + "chacha20poly1305", "clap", "criterion", + "hex", "hpack-patched", "lazy_static", "nom 8.0.0", @@ -399,6 +509,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnetwork" version = "0.20.0" @@ -604,6 +723,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "pcap-file" version = "3.0.0-rc1" @@ -779,6 +904,29 @@ dependencies = [ "pnet_sys", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -826,6 +974,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rayon" @@ -976,6 +1127,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1212,6 +1369,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -1240,6 +1407,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.101" @@ -1327,11 +1500,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -1346,6 +1519,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-sys" version = "0.52.0" @@ -1364,6 +1543,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -1386,7 +1574,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -1501,3 +1689,9 @@ checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index 47afaa90..cac66034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ pcap-file = "3.0.0-rc1" sha2 = "0.10.9" tls-parser = "0.12.2" hpack-patched = "0.3.0" +hex = "0.4.3" +aes-gcm = "0.10.3" +chacha20poly1305 = "0.10.1" [dev-dependencies] clap = { version = "4.5.47", features = ["derive"] } diff --git a/deny.toml b/deny.toml index edd3f6ad..ade42871 100644 --- a/deny.toml +++ b/deny.toml @@ -8,6 +8,7 @@ allow = [ "MIT", "Apache-2.0", "Unicode-3.0", + "BSD-3-Clause" ] confidence-threshold = 1.0 diff --git a/src/http_process.rs b/src/http_process.rs index e0f71baf..83b982f8 100644 --- a/src/http_process.rs +++ b/src/http_process.rs @@ -1,6 +1,7 @@ use crate::error::HuginnNetError; use crate::http_common::HttpProcessor; use crate::observable_signals::{ObservableHttpRequest, ObservableHttpResponse}; +use crate::tls_decryption::{CipherSuite, TlsConnectionState, TlsDecryptor}; use crate::{http1_process, http2_process}; use pnet::packet::ip::IpNextHeaderProtocols; use pnet::packet::ipv4::Ipv4Packet; @@ -9,12 +10,118 @@ use pnet::packet::tcp::TcpPacket; use pnet::packet::Packet; use std::net::IpAddr; use std::time::Duration; +use tls_parser::{parse_tls_plaintext, TlsMessage, TlsMessageHandshake, TlsRecordType}; use tracing::debug; use ttl_cache::TtlCache; /// FlowKey: (Client IP, Server IP, Client Port, Server Port) pub type FlowKey = (IpAddr, IpAddr, u16, u16); +/// HTTPS processor that handles TLS decryption and HTTP processing +pub struct HttpsProcessor { + tls_decryptor: Option, + http_processors: HttpProcessors, +} + +impl HttpsProcessor { + /// Create new HTTPS processor + pub fn new(tls_decryptor: Option) -> Self { + Self { + tls_decryptor, + http_processors: HttpProcessors::new(), + } + } + + /// Process HTTPS data by decrypting and then parsing HTTP + pub fn process_https_data( + &mut self, + flow_key: &FlowKey, + encrypted_data: &[u8], + is_client_data: bool, + ) -> Result, HuginnNetError> { + let decryptor = match &mut self.tls_decryptor { + Some(d) => d, + None => { + return Err(HuginnNetError::Parse( + "No TLS decryptor available".to_string(), + )) + } + }; + + // Create connection ID from flow key + let connection_id = format!( + "{}:{}->{}:{}", + flow_key.0, flow_key.2, flow_key.1, flow_key.3 + ); + + // Decrypt the data + let decrypted_data = + decryptor.decrypt_record(&connection_id, encrypted_data, is_client_data)?; + + // Parse the decrypted HTTP data + let request = if is_client_data { + self.http_processors.parse_request(&decrypted_data) + } else { + None + }; + + let response = if !is_client_data { + self.http_processors.parse_response(&decrypted_data) + } else { + None + }; + + Ok(match (request, response) { + (Some(req), None) => Some(( + req, + ObservableHttpResponse { + version: crate::http::Version::V11, + horder: Vec::new(), + habsent: Vec::new(), + expsw: String::new(), + headers: Vec::new(), + status_code: None, + }, + )), + (None, Some(resp)) => Some(( + ObservableHttpRequest { + lang: None, + user_agent: None, + version: crate::http::Version::V11, + horder: Vec::new(), + habsent: Vec::new(), + expsw: String::new(), + headers: Vec::new(), + cookies: Vec::new(), + referer: None, + method: None, + uri: None, + }, + resp, + )), + (Some(req), Some(resp)) => Some((req, resp)), + (None, None) => None, + }) + } + + /// Setup TLS connection for decryption + pub fn setup_tls_connection( + &mut self, + flow_key: &FlowKey, + connection_state: TlsConnectionState, + ) -> Result<(), HuginnNetError> { + if let Some(decryptor) = &mut self.tls_decryptor { + let connection_id = format!( + "{}:{}->{}:{}", + flow_key.0, flow_key.2, flow_key.1, flow_key.3 + ); + decryptor.add_connection(connection_id, connection_state); + debug!("Setup TLS connection for decryption: {:?}", flow_key); + } + Ok(()) + } +} + use crate::http_common::HttpParser; /// HTTP parser that automatically detects and processes different HTTP versions @@ -145,7 +252,7 @@ pub struct ObservableHttpPackage { #[derive(Clone)] struct TcpData { - sequence: u32, + timestamp: std::time::Instant, data: Vec, } @@ -158,6 +265,13 @@ pub struct TcpFlow { server_data: Vec, client_http_parsed: bool, server_http_parsed: bool, + // TLS/HTTPS support + is_tls: bool, + tls_handshake_complete: bool, + client_random: Option>, + server_random: Option>, + cipher_suite: Option, + tls_version: Option, } /// Quick check if HTTP data is complete for parsing (supports HTTP/1.x and HTTP/2) @@ -182,6 +296,9 @@ impl TcpFlow { dst_port: u16, tcp_data: TcpData, ) -> TcpFlow { + // Detect if this might be TLS based on destination port + let is_tls = dst_port == 443 || src_port == 443; + TcpFlow { client_ip: src_ip, server_ip: dst_ip, @@ -191,9 +308,18 @@ impl TcpFlow { server_data: Vec::new(), client_http_parsed: false, server_http_parsed: false, + // TLS/HTTPS fields + is_tls, + tls_handshake_complete: false, + client_random: None, + server_random: None, + cipher_suite: None, + tls_version: None, } } - /// Traversing all the data in sequence in the correct order to build the full data + /// Traversing all the data in arrival order to preserve original packet sequence + /// This preserves HTTP header order as seen on the wire, which is critical for + /// accurate fingerprinting when packets pass through proxies or load balancers. /// /// # Parameters /// - `is_client`: If the data comes from the client. @@ -206,7 +332,7 @@ impl TcpFlow { let mut sorted_data = data.clone(); - sorted_data.sort_by_key(|tcp_data| tcp_data.sequence); + sorted_data.sort_by_key(|tcp_data| tcp_data.timestamp); let mut full_data = Vec::new(); for tcp_data in sorted_data { @@ -214,12 +340,126 @@ impl TcpFlow { } full_data } + + /// Check if this flow is TLS/HTTPS + fn is_tls_flow(&self) -> bool { + self.is_tls + } + + /// Process TLS handshake data to extract connection parameters + fn process_tls_handshake( + &mut self, + data: &[u8], + is_client_data: bool, + ) -> Result<(), HuginnNetError> { + if !self.is_tls { + return Ok(()); + } + + // Parse TLS records from the data + let mut remaining = data; + while !remaining.is_empty() { + match parse_tls_plaintext(remaining) { + Ok((rest, tls_record)) => { + remaining = rest; + + // Process handshake messages + if tls_record.hdr.record_type == TlsRecordType::Handshake { + for message in &tls_record.msg { + if let TlsMessage::Handshake(handshake_msg) = message { + self.extract_handshake_info(handshake_msg, is_client_data)?; + } + } + } + } + Err(_) => { + // Not valid TLS data or incomplete, break + break; + } + } + } + + Ok(()) + } + + /// Extract handshake information from TLS messages + fn extract_handshake_info( + &mut self, + handshake_msg: &TlsMessageHandshake, + is_client_data: bool, + ) -> Result<(), HuginnNetError> { + match handshake_msg { + TlsMessageHandshake::ClientHello(client_hello) => { + if is_client_data { + // Extract client random + self.client_random = Some(client_hello.random.to_vec()); + debug!("Extracted client random from ClientHello"); + } + } + TlsMessageHandshake::ServerHello(server_hello) => { + if !is_client_data { + // Extract server random and cipher suite + self.server_random = Some(server_hello.random.to_vec()); + + // Convert cipher suite + if let Some(cipher) = CipherSuite::from_u16(server_hello.cipher.0) { + debug!("Extracted cipher suite: {:?}", cipher); + self.cipher_suite = Some(cipher); + } + + // Extract TLS version + self.tls_version = Some(server_hello.version.0); + + // Check if handshake is complete enough for decryption + if self.client_random.is_some() + && self.server_random.is_some() + && self.cipher_suite.is_some() + { + self.tls_handshake_complete = true; + debug!("TLS handshake parameters complete"); + } + } + } + _ => { + // Other handshake messages (certificates, key exchange, etc.) + // For now, we don't need to process these for decryption + } + } + + Ok(()) + } + + /// Get TLS connection state for decryption + #[allow(dead_code)] + fn get_tls_connection_state(&self) -> Option { + if !self.tls_handshake_complete { + return None; + } + + let client_random = self.client_random.as_ref()?.clone(); + let server_random = self.server_random.as_ref()?.clone(); + let cipher_suite = self.cipher_suite.as_ref()?.clone(); + let tls_version = self.tls_version?; + + Some(TlsConnectionState::new( + client_random, + server_random, + cipher_suite, + tls_version, + )) + } + + /// Check if TLS handshake is complete + fn is_tls_handshake_complete(&self) -> bool { + self.tls_handshake_complete + } } pub fn process_http_ipv4( packet: &Ipv4Packet, http_flows: &mut TtlCache, processors: &HttpProcessors, + config: &crate::AnalysisConfig, ) -> Result { if packet.get_next_level_protocol() != IpNextHeaderProtocols::Tcp { return Err(HuginnNetError::UnsupportedProtocol("IPv4".to_string())); @@ -231,6 +471,7 @@ pub fn process_http_ipv4( IpAddr::V4(packet.get_source()), IpAddr::V4(packet.get_destination()), processors, + config, ) } else { Ok(ObservableHttpPackage { @@ -244,6 +485,7 @@ pub fn process_http_ipv6( packet: &Ipv6Packet, http_flows: &mut TtlCache, processors: &HttpProcessors, + config: &crate::AnalysisConfig, ) -> Result { if packet.get_next_header() != IpNextHeaderProtocols::Tcp { return Err(HuginnNetError::UnsupportedProtocol("IPv6".to_string())); @@ -255,6 +497,7 @@ pub fn process_http_ipv6( IpAddr::V6(packet.get_source()), IpAddr::V6(packet.get_destination()), processors, + config, ) } else { Ok(ObservableHttpPackage { @@ -270,6 +513,7 @@ fn process_tcp_packet( src_ip: IpAddr, dst_ip: IpAddr, processors: &HttpProcessors, + config: &crate::AnalysisConfig, ) -> Result { let src_port: u16 = tcp.get_source(); let dst_port: u16 = tcp.get_destination(); @@ -295,25 +539,49 @@ fn process_tcp_packet( if let Some(flow) = tcp_flow { if !tcp.payload().is_empty() { let tcp_data = TcpData { - sequence: tcp.get_sequence(), + timestamp: std::time::Instant::now(), data: Vec::from(tcp.payload()), }; if is_client && src_ip == flow.client_ip && src_port == flow.client_port { // Only add data and parse if not already parsed if !flow.client_http_parsed { - flow.client_data.push(tcp_data); - let full_data = flow.get_full_data(is_client); - - // Quick check before expensive parsing (supports HTTP/1.x and HTTP/2) - if has_complete_http_data(&full_data, processors) { - match parse_http_request(&full_data, processors) { - Ok(Some(http_request_parsed)) => { - observable_http_package.http_request = Some(http_request_parsed); - flow.client_http_parsed = true; + flow.client_data.push(tcp_data.clone()); + + // Handle TLS/HTTPS processing + if flow.is_tls_flow() && config.https_enabled { + // Process TLS handshake if not complete + if !flow.is_tls_handshake_complete() { + if let Err(e) = flow.process_tls_handshake(&tcp_data.data, true) { + debug!("Failed to process TLS handshake: {}", e); + } + } + + // If handshake is complete, try to decrypt and parse HTTP + if flow.is_tls_handshake_complete() { + // For HTTPS, we need the full encrypted data + let _full_data = flow.get_full_data(is_client); + + // Try to decrypt and parse as HTTP (this is a simplified approach) + // In a real implementation, we'd need to identify TLS application data records + debug!("HTTPS: Handshake complete, ready for decryption"); + // TODO: Implement actual HTTPS decryption here + } + } else { + // Regular HTTP processing + let full_data = flow.get_full_data(is_client); + + // Quick check before expensive parsing (supports HTTP/1.x and HTTP/2) + if has_complete_http_data(&full_data, processors) { + match parse_http_request(&full_data, processors) { + Ok(Some(http_request_parsed)) => { + observable_http_package.http_request = + Some(http_request_parsed); + flow.client_http_parsed = true; + } + Ok(None) => {} + Err(_e) => {} } - Ok(None) => {} - Err(_e) => {} } } } else { @@ -322,21 +590,44 @@ fn process_tcp_packet( } else if src_ip == flow.server_ip && src_port == flow.server_port { // Only add data and parse if not already parsed if !flow.server_http_parsed { - flow.server_data.push(tcp_data); - let full_data = flow.get_full_data(is_client); - - // Quick check before expensive parsing (supports HTTP/1.x and HTTP/2) - if has_complete_http_data(&full_data, processors) { - match parse_http_response(&full_data, processors) { - Ok(Some(http_response_parsed)) => { - observable_http_package.http_response = Some(http_response_parsed); - flow.server_http_parsed = true; + flow.server_data.push(tcp_data.clone()); + + // Handle TLS/HTTPS processing + if flow.is_tls_flow() && config.https_enabled { + // Process TLS handshake if not complete + if !flow.is_tls_handshake_complete() { + if let Err(e) = flow.process_tls_handshake(&tcp_data.data, false) { + debug!("Failed to process TLS handshake: {}", e); } - Ok(None) => {} - Err(_e) => {} + } + + // If handshake is complete, try to decrypt and parse HTTP + if flow.is_tls_handshake_complete() { + // For HTTPS, we need the full encrypted data + let _full_data = flow.get_full_data(is_client); + + // Try to decrypt and parse as HTTP (this is a simplified approach) + debug!("HTTPS: Server handshake complete, ready for decryption"); + // TODO: Implement actual HTTPS decryption here } } else { - debug!("SERVER: Data not complete yet, waiting for more"); + // Regular HTTP processing + let full_data = flow.get_full_data(is_client); + + // Quick check before expensive parsing (supports HTTP/1.x and HTTP/2) + if has_complete_http_data(&full_data, processors) { + match parse_http_response(&full_data, processors) { + Ok(Some(http_response_parsed)) => { + observable_http_package.http_response = + Some(http_response_parsed); + flow.server_http_parsed = true; + } + Ok(None) => {} + Err(_e) => {} + } + } else { + debug!("SERVER: Data not complete yet, waiting for more"); + } } } else { debug!("SERVER: HTTP already parsed, discarding additional data"); @@ -361,7 +652,7 @@ fn process_tcp_packet( } } else if tcp.get_flags() & pnet::packet::tcp::TcpFlags::SYN != 0 { let tcp_data: TcpData = TcpData { - sequence: tcp.get_sequence(), + timestamp: std::time::Instant::now(), data: Vec::from(tcp.payload()), }; let flow: TcpFlow = TcpFlow::init(src_ip, src_port, dst_ip, dst_port, tcp_data); diff --git a/src/lib.rs b/src/lib.rs index c78f0c6b..f5c6553f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,19 @@ pub use observable_signals::{ ObservableTlsClient, // TLS signals }; +// ============================================================================ +// TLS KEYLOG EXPORTS (for HTTPS decryption) +// ============================================================================ +pub use tls_keylog::{KeyMaterial, KeyType, TlsKeylog, TlsKeylogManager}; + +// TLS DECRYPTION EXPORTS +// ============================================================================ +pub use tls_decryption::{CipherSuite, TlsConnectionState, TlsDecryptor}; + +// HTTPS PROCESSING EXPORTS +// ============================================================================ +pub use http_process::HttpsProcessor; + // ============================================================================ // EXTERNAL CRATE IMPORTS // ============================================================================ @@ -100,6 +113,8 @@ mod observable_http_signals_matching; // TLS PROTOCOL MODULES (depends on TCP) // ============================================================================ pub mod tls; +pub mod tls_decryption; +pub mod tls_keylog; pub mod tls_process; // ============================================================================ @@ -115,21 +130,28 @@ pub mod signature_matcher; pub struct AnalysisConfig { /// Enable HTTP protocol analysis pub http_enabled: bool, + /// Enable HTTPS protocol analysis (requires TLS keylog file) + pub https_enabled: bool, /// Enable TCP protocol analysis pub tcp_enabled: bool, /// Enable TLS protocol analysis pub tls_enabled: bool, /// Enable fingerprint matching against the database. When false, all quality matched results will be Disabled. pub matcher_enabled: bool, + /// Paths to TLS keylog files for HTTPS decryption (SSLKEYLOGFILE format) + /// Supports multiple keylog files for different certificates/domains + pub tls_keylog_files: Vec, } impl Default for AnalysisConfig { fn default() -> Self { Self { http_enabled: true, + https_enabled: false, tcp_enabled: true, tls_enabled: true, matcher_enabled: true, + tls_keylog_files: Vec::new(), } } } @@ -144,6 +166,8 @@ pub struct HuginnNet<'a> { connection_tracker: TtlCache, http_flows: TtlCache, http_processors: http_process::HttpProcessors, + https_processor: Option, + tls_keylog_manager: Option, config: AnalysisConfig, } @@ -178,21 +202,117 @@ impl<'a> HuginnNet<'a> { } else { 0 }; - let http_flows_size = if config.http_enabled { + let http_flows_size = if config.http_enabled || config.https_enabled { max_connections } else { 0 }; + // Initialize TLS keylog manager if HTTPS is enabled + let tls_keylog_manager = if config.https_enabled && !config.tls_keylog_files.is_empty() { + match TlsKeylogManager::from_files(&config.tls_keylog_files) { + Ok(manager) => { + tracing::info!( + "Loaded {} TLS keylog files with {} total keys", + manager.keylog_count(), + manager.total_key_count() + ); + Some(manager) + } + Err(e) => { + tracing::warn!("Failed to load TLS keylog files: {}", e); + None + } + } + } else { + None + }; + + // Initialize HTTPS processor if enabled + let https_processor = if config.https_enabled { + let tls_decryptor = tls_keylog_manager + .as_ref() + .map(|manager| TlsDecryptor::new((*manager).clone())); + Some(HttpsProcessor::new(tls_decryptor)) + } else { + None + }; + Self { matcher, connection_tracker: TtlCache::new(connection_tracker_size), http_flows: TtlCache::new(http_flows_size), http_processors: crate::http_process::HttpProcessors::new(), + https_processor, + tls_keylog_manager, config, } } + /// Add TLS keylog file for HTTPS decryption + /// + /// This allows adding keylog files dynamically after initialization. + /// Useful when keylog files become available during runtime. + pub fn add_tls_keylog_file>( + &mut self, + path: P, + ) -> Result<(), crate::error::HuginnNetError> { + if !self.config.https_enabled { + return Err(crate::error::HuginnNetError::Parse( + "HTTPS is not enabled in configuration".to_string(), + )); + } + + // Initialize keylog manager if it doesn't exist + if self.tls_keylog_manager.is_none() { + self.tls_keylog_manager = Some(TlsKeylogManager::new()); + } + + // Load the new keylog file + let _keylog = TlsKeylog::from_file(path.as_ref())?; + let filename = path + .as_ref() + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + if let Some(manager) = &mut self.tls_keylog_manager { + let content = std::fs::read_to_string(path.as_ref()).map_err(|e| { + crate::error::HuginnNetError::Parse(format!("Failed to read keylog file: {e}")) + })?; + manager.add_keylog_from_string(filename, &content)?; + + // Update HTTPS processor with new keylog manager + if let Some(https_proc) = &mut self.https_processor { + let tls_decryptor = Some(TlsDecryptor::new(manager.clone())); + *https_proc = HttpsProcessor::new(tls_decryptor); + } + + tracing::info!("Added TLS keylog file: {:?}", path.as_ref()); + } + + Ok(()) + } + + /// Get HTTPS statistics + pub fn https_stats(&self) -> Option<(usize, usize, usize)> { + self.tls_keylog_manager.as_ref().map(|manager| { + ( + manager.keylog_count(), + manager.total_key_count(), + manager.total_client_count(), + ) + }) + } + + /// Check if HTTPS decryption is available + pub fn is_https_ready(&self) -> bool { + self.config.https_enabled + && self.tls_keylog_manager.is_some() + && self.https_processor.is_some() + } + fn process_with( &mut self, mut packet_fn: F, diff --git a/src/process.rs b/src/process.rs index d86fddfc..ff66755f 100644 --- a/src/process.rs +++ b/src/process.rs @@ -137,6 +137,7 @@ trait IpPacketProcessor: Packet { data: &[u8], http_flows: &mut TtlCache, http_processors: &HttpProcessors, + config: &AnalysisConfig, ) -> Result; fn process_tcp_with_data( data: &[u8], @@ -168,9 +169,10 @@ impl IpPacketProcessor for Ipv4Packet<'_> { data: &[u8], http_flows: &mut TtlCache, http_processors: &HttpProcessors, + config: &AnalysisConfig, ) -> Result { if let Some(packet) = Ipv4Packet::new(data) { - http_process::process_http_ipv4(&packet, http_flows, http_processors) + http_process::process_http_ipv4(&packet, http_flows, http_processors, config) } else { Err(HuginnNetError::UnexpectedPackage( "Invalid IPv4 packet data".to_string(), @@ -225,9 +227,10 @@ impl IpPacketProcessor for Ipv6Packet<'_> { data: &[u8], http_flows: &mut TtlCache, http_processors: &HttpProcessors, + config: &AnalysisConfig, ) -> Result { if let Some(packet) = Ipv6Packet::new(data) { - http_process::process_http_ipv6(&packet, http_flows, http_processors) + http_process::process_http_ipv6(&packet, http_flows, http_processors, config) } else { Err(HuginnNetError::UnexpectedPackage( "Invalid IPv6 packet data".to_string(), @@ -269,7 +272,7 @@ fn execute_analysis( destination: IpPort, ) -> Result { let http_response = if config.http_enabled { - P::process_http_with_data(packet_data, http_flows, http_processors)? + P::process_http_with_data(packet_data, http_flows, http_processors, config)? } else { ObservableHttpPackage { http_request: None, diff --git a/src/tls_decryption.rs b/src/tls_decryption.rs new file mode 100644 index 00000000..6bce85e8 --- /dev/null +++ b/src/tls_decryption.rs @@ -0,0 +1,443 @@ +//! TLS Decryption Module +//! +//! This module provides TLS decryption capabilities using keylog files. +//! It supports TLS 1.2 and TLS 1.3 decryption with various cipher suites. + +use crate::error::HuginnNetError; +use crate::tls_keylog::{KeyMaterial, KeyType, TlsKeylogManager}; +use aes_gcm::aead::Aead; +use aes_gcm::{Aes128Gcm, Aes256Gcm, KeyInit, Nonce}; +use chacha20poly1305::{ChaCha20Poly1305, Key}; +use std::collections::HashMap; +use tls_parser::TlsMessageHandshake; +use tracing::{debug, warn}; + +/// Supported TLS cipher suites for decryption +#[derive(Debug, Clone, PartialEq)] +pub enum CipherSuite { + /// TLS_AES_128_GCM_SHA256 (TLS 1.3) + Aes128GcmSha256, + /// TLS_AES_256_GCM_SHA384 (TLS 1.3) + Aes256GcmSha384, + /// TLS_CHACHA20_POLY1305_SHA256 (TLS 1.3) + ChaCha20Poly1305Sha256, + /// TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (TLS 1.2) + EcdheRsaAes128GcmSha256, + /// TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (TLS 1.2) + EcdheRsaAes256GcmSha384, +} + +impl CipherSuite { + /// Get cipher suite from TLS cipher suite identifier + pub fn from_u16(cipher: u16) -> Option { + match cipher { + 0x1301 => Some(Self::Aes128GcmSha256), + 0x1302 => Some(Self::Aes256GcmSha384), + 0x1303 => Some(Self::ChaCha20Poly1305Sha256), + 0xc02f => Some(Self::EcdheRsaAes128GcmSha256), + 0xc030 => Some(Self::EcdheRsaAes256GcmSha384), + _ => None, + } + } + + /// Get key length in bytes for this cipher suite + pub fn key_length(&self) -> usize { + match self { + Self::Aes128GcmSha256 | Self::EcdheRsaAes128GcmSha256 => 16, + Self::Aes256GcmSha384 | Self::EcdheRsaAes256GcmSha384 => 32, + Self::ChaCha20Poly1305Sha256 => 32, + } + } + + /// Get IV length in bytes for this cipher suite + pub fn iv_length(&self) -> usize { + match self { + Self::Aes128GcmSha256 | Self::Aes256GcmSha384 => 12, + Self::EcdheRsaAes128GcmSha256 | Self::EcdheRsaAes256GcmSha384 => 4, + Self::ChaCha20Poly1305Sha256 => 12, + } + } + + /// Check if this is a TLS 1.3 cipher suite + pub fn is_tls13(&self) -> bool { + matches!( + self, + Self::Aes128GcmSha256 | Self::Aes256GcmSha384 | Self::ChaCha20Poly1305Sha256 + ) + } +} + +/// TLS connection state for decryption +#[derive(Debug)] +pub struct TlsConnectionState { + /// Client random from handshake + pub client_random: Vec, + /// Server random from handshake + pub server_random: Vec, + /// Negotiated cipher suite + pub cipher_suite: CipherSuite, + /// TLS version (0x0303 for TLS 1.2, 0x0304 for TLS 1.3) + pub tls_version: u16, + /// Client sequence number for record decryption + pub client_seq_num: u64, + /// Server sequence number for record decryption + pub server_seq_num: u64, +} + +impl TlsConnectionState { + /// Create new TLS connection state + pub fn new( + client_random: Vec, + server_random: Vec, + cipher_suite: CipherSuite, + tls_version: u16, + ) -> Self { + Self { + client_random, + server_random, + cipher_suite, + tls_version, + client_seq_num: 0, + server_seq_num: 0, + } + } +} + +/// TLS decryption context +pub struct TlsDecryptor { + /// Keylog manager for finding decryption keys + keylog_manager: TlsKeylogManager, + /// Active TLS connections being tracked + connections: HashMap, +} + +impl TlsDecryptor { + /// Create new TLS decryptor with keylog manager + pub fn new(keylog_manager: TlsKeylogManager) -> Self { + Self { + keylog_manager, + connections: HashMap::new(), + } + } + + /// Process TLS handshake message to extract connection parameters + pub fn process_handshake( + &mut self, + connection_id: &str, + handshake_msg: &TlsMessageHandshake, + ) -> Result<(), HuginnNetError> { + match handshake_msg { + TlsMessageHandshake::ClientHello(_client_hello) => { + debug!("Processing ClientHello for connection: {}", connection_id); + // Store client random for later use + // Note: In a real implementation, we'd need to parse the full handshake + // This is a simplified version for demonstration + } + TlsMessageHandshake::ServerHello(_server_hello) => { + debug!("Processing ServerHello for connection: {}", connection_id); + // Extract cipher suite and create connection state + // This would need full handshake parsing in practice + } + _ => { + // Other handshake messages + } + } + + Ok(()) + } + + /// Decrypt TLS application data record + pub fn decrypt_record( + &mut self, + connection_id: &str, + encrypted_data: &[u8], + is_client_data: bool, + ) -> Result, HuginnNetError> { + // First, get the connection and extract needed data + let (key_material, cipher_suite) = { + let connection = self + .connections + .get(connection_id) + .ok_or_else(|| HuginnNetError::Parse("Connection not found".to_string()))?; + + let key_material = self.find_key_material(connection, is_client_data)?; + (key_material, connection.cipher_suite.clone()) + }; + + // Now get mutable reference to connection for decryption + let connection = self + .connections + .get_mut(connection_id) + .ok_or_else(|| HuginnNetError::Parse("Connection not found".to_string()))?; + + // Decrypt based on cipher suite + match &cipher_suite { + CipherSuite::Aes128GcmSha256 | CipherSuite::Aes256GcmSha384 => { + Self::decrypt_aes_gcm(encrypted_data, &key_material, connection, is_client_data) + } + CipherSuite::ChaCha20Poly1305Sha256 => Self::decrypt_chacha20_poly1305( + encrypted_data, + &key_material, + connection, + is_client_data, + ), + CipherSuite::EcdheRsaAes128GcmSha256 | CipherSuite::EcdheRsaAes256GcmSha384 => { + Self::decrypt_tls12_aes_gcm( + encrypted_data, + &key_material, + connection, + is_client_data, + ) + } + } + } + + /// Find appropriate key material for decryption + fn find_key_material( + &self, + connection: &TlsConnectionState, + is_client_data: bool, + ) -> Result { + let key_type = if connection.cipher_suite.is_tls13() { + if is_client_data { + KeyType::ClientTrafficSecret0 + } else { + KeyType::ServerTrafficSecret0 + } + } else { + // TLS 1.2 uses master secret + KeyType::ClientRandom + }; + + let (_, key_material) = self + .keylog_manager + .find_key_by_type(&connection.client_random, &key_type) + .ok_or_else(|| { + HuginnNetError::Parse(format!( + "No key material found for connection with key type: {key_type:?}" + )) + })?; + + Ok(key_material.clone()) + } + + /// Decrypt AES-GCM encrypted data (TLS 1.3) + fn decrypt_aes_gcm( + encrypted_data: &[u8], + key_material: &KeyMaterial, + connection: &mut TlsConnectionState, + is_client_data: bool, + ) -> Result, HuginnNetError> { + if encrypted_data.len() < 16 { + return Err(HuginnNetError::Parse( + "Encrypted data too short".to_string(), + )); + } + + // Extract nonce and ciphertext + let (nonce_bytes, ciphertext) = encrypted_data.split_at(12); + + // Get sequence number and increment + let _seq_num = if is_client_data { + let seq = connection.client_seq_num; + connection.client_seq_num = connection.client_seq_num.saturating_add(1); + seq + } else { + let seq = connection.server_seq_num; + connection.server_seq_num = connection.server_seq_num.saturating_add(1); + seq + }; + + match &connection.cipher_suite { + CipherSuite::Aes128GcmSha256 => { + let cipher = Aes128Gcm::new_from_slice(&key_material.key_data[..16]) + .map_err(|e| HuginnNetError::Parse(format!("Invalid AES-128 key: {e}")))?; + + let nonce = Nonce::from_slice(nonce_bytes); + + cipher + .decrypt(nonce, ciphertext) + .map_err(|e| HuginnNetError::Parse(format!("AES-GCM decryption failed: {e}"))) + } + CipherSuite::Aes256GcmSha384 => { + let cipher = Aes256Gcm::new_from_slice(&key_material.key_data[..32]) + .map_err(|e| HuginnNetError::Parse(format!("Invalid AES-256 key: {e}")))?; + + let nonce = Nonce::from_slice(nonce_bytes); + + cipher + .decrypt(nonce, ciphertext) + .map_err(|e| HuginnNetError::Parse(format!("AES-GCM decryption failed: {e}"))) + } + _ => Err(HuginnNetError::Parse( + "Invalid cipher suite for AES-GCM".to_string(), + )), + } + } + + /// Decrypt ChaCha20-Poly1305 encrypted data (TLS 1.3) + fn decrypt_chacha20_poly1305( + encrypted_data: &[u8], + key_material: &KeyMaterial, + connection: &mut TlsConnectionState, + is_client_data: bool, + ) -> Result, HuginnNetError> { + if encrypted_data.len() < 16 { + return Err(HuginnNetError::Parse( + "Encrypted data too short".to_string(), + )); + } + + // Extract nonce and ciphertext + let (nonce_bytes, ciphertext) = encrypted_data.split_at(12); + + // Get sequence number and increment + let _seq_num = if is_client_data { + let seq = connection.client_seq_num; + connection.client_seq_num = connection.client_seq_num.saturating_add(1); + seq + } else { + let seq = connection.server_seq_num; + connection.server_seq_num = connection.server_seq_num.saturating_add(1); + seq + }; + + let key = Key::from_slice(&key_material.key_data[..32]); + let cipher = ChaCha20Poly1305::new(key); + + let nonce = chacha20poly1305::Nonce::from_slice(nonce_bytes); + + cipher + .decrypt(nonce, ciphertext) + .map_err(|e| HuginnNetError::Parse(format!("ChaCha20-Poly1305 decryption failed: {e}"))) + } + + /// Decrypt TLS 1.2 AES-GCM encrypted data + fn decrypt_tls12_aes_gcm( + _encrypted_data: &[u8], + _key_material: &KeyMaterial, + _connection: &mut TlsConnectionState, + _is_client_data: bool, + ) -> Result, HuginnNetError> { + // TLS 1.2 decryption is more complex as it requires deriving keys from master secret + // This is a simplified implementation + warn!("TLS 1.2 decryption not fully implemented yet"); + Err(HuginnNetError::Parse( + "TLS 1.2 decryption not implemented".to_string(), + )) + } + + /// Add a new TLS connection to track + pub fn add_connection(&mut self, connection_id: String, state: TlsConnectionState) { + debug!("Adding TLS connection: {}", connection_id); + self.connections.insert(connection_id, state); + } + + /// Remove a TLS connection + pub fn remove_connection(&mut self, connection_id: &str) { + debug!("Removing TLS connection: {}", connection_id); + self.connections.remove(connection_id); + } + + /// Get connection count + pub fn connection_count(&self) -> usize { + self.connections.len() + } + + /// Check if a connection exists + pub fn has_connection(&self, connection_id: &str) -> bool { + self.connections.contains_key(connection_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cipher_suite_from_u16() { + assert_eq!( + CipherSuite::from_u16(0x1301), + Some(CipherSuite::Aes128GcmSha256) + ); + assert_eq!( + CipherSuite::from_u16(0x1302), + Some(CipherSuite::Aes256GcmSha384) + ); + assert_eq!( + CipherSuite::from_u16(0x1303), + Some(CipherSuite::ChaCha20Poly1305Sha256) + ); + assert_eq!(CipherSuite::from_u16(0x9999), None); + } + + #[test] + fn test_cipher_suite_properties() { + let aes128 = CipherSuite::Aes128GcmSha256; + assert_eq!(aes128.key_length(), 16); + assert_eq!(aes128.iv_length(), 12); + assert!(aes128.is_tls13()); + + let aes256 = CipherSuite::Aes256GcmSha384; + assert_eq!(aes256.key_length(), 32); + assert_eq!(aes256.iv_length(), 12); + assert!(aes256.is_tls13()); + + let tls12_cipher = CipherSuite::EcdheRsaAes128GcmSha256; + assert_eq!(tls12_cipher.key_length(), 16); + assert_eq!(tls12_cipher.iv_length(), 4); + assert!(!tls12_cipher.is_tls13()); + } + + #[test] + fn test_tls_connection_state() { + let client_random = vec![1u8; 32]; + let server_random = vec![2u8; 32]; + let cipher_suite = CipherSuite::Aes128GcmSha256; + let tls_version = 0x0304; // TLS 1.3 + + let state = TlsConnectionState::new( + client_random.clone(), + server_random.clone(), + cipher_suite.clone(), + tls_version, + ); + + assert_eq!(state.client_random, client_random); + assert_eq!(state.server_random, server_random); + assert_eq!(state.cipher_suite, cipher_suite); + assert_eq!(state.tls_version, tls_version); + assert_eq!(state.client_seq_num, 0); + assert_eq!(state.server_seq_num, 0); + } + + #[test] + fn test_tls_decryptor_creation() { + let keylog_manager = TlsKeylogManager::new(); + let decryptor = TlsDecryptor::new(keylog_manager); + + assert_eq!(decryptor.connection_count(), 0); + assert!(!decryptor.has_connection("test")); + } + + #[test] + fn test_connection_management() { + let keylog_manager = TlsKeylogManager::new(); + let mut decryptor = TlsDecryptor::new(keylog_manager); + + let state = TlsConnectionState::new( + vec![1u8; 32], + vec![2u8; 32], + CipherSuite::Aes128GcmSha256, + 0x0304, + ); + + decryptor.add_connection("test_conn".to_string(), state); + assert_eq!(decryptor.connection_count(), 1); + assert!(decryptor.has_connection("test_conn")); + + decryptor.remove_connection("test_conn"); + assert_eq!(decryptor.connection_count(), 0); + assert!(!decryptor.has_connection("test_conn")); + } +} diff --git a/src/tls_keylog.rs b/src/tls_keylog.rs new file mode 100644 index 00000000..f679b006 --- /dev/null +++ b/src/tls_keylog.rs @@ -0,0 +1,633 @@ +use crate::error::HuginnNetError; +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use tracing::{debug, warn}; + +/// Types of TLS key material found in keylog files +#[derive(Debug, Clone, PartialEq)] +pub enum KeyType { + /// TLS 1.2 master secret + ClientRandom, + /// TLS 1.3 client traffic secret + ClientTrafficSecret0, + /// TLS 1.3 server traffic secret + ServerTrafficSecret0, + /// TLS 1.3 client handshake traffic secret + ClientHandshakeTrafficSecret, + /// TLS 1.3 server handshake traffic secret + ServerHandshakeTrafficSecret, + /// Unknown key type + Unknown(String), +} + +impl From<&str> for KeyType { + fn from(s: &str) -> Self { + match s { + "CLIENT_RANDOM" => KeyType::ClientRandom, + "CLIENT_TRAFFIC_SECRET_0" => KeyType::ClientTrafficSecret0, + "SERVER_TRAFFIC_SECRET_0" => KeyType::ServerTrafficSecret0, + "CLIENT_HANDSHAKE_TRAFFIC_SECRET" => KeyType::ClientHandshakeTrafficSecret, + "SERVER_HANDSHAKE_TRAFFIC_SECRET" => KeyType::ServerHandshakeTrafficSecret, + other => KeyType::Unknown(other.to_string()), + } + } +} + +/// TLS key material extracted from keylog files +#[derive(Debug, Clone)] +pub struct KeyMaterial { + /// Type of key material + pub key_type: KeyType, + /// Client random (32 bytes) + pub client_random: Vec, + /// Key material (varies by type) + pub key_data: Vec, +} + +/// Parser for TLS keylog files (SSLKEYLOGFILE format) +/// +/// Supports the standard format used by browsers and applications: +/// ```text +/// CLIENT_RANDOM +/// CLIENT_TRAFFIC_SECRET_0 +/// SERVER_TRAFFIC_SECRET_0 +/// ``` +#[derive(Debug, Clone)] +pub struct TlsKeylog { + /// Map from client_random to key material + keys: HashMap, Vec>, + /// Total number of keys loaded + key_count: usize, +} + +impl TlsKeylog { + /// Create a new empty keylog + pub fn new() -> Self { + Self { + keys: HashMap::new(), + key_count: 0, + } + } + + /// Load keylog from file + /// + /// # Arguments + /// * `path` - Path to the keylog file + /// + /// # Returns + /// * `Ok(TlsKeylog)` - Successfully loaded keylog + /// * `Err(HuginnNetError)` - Failed to load or parse keylog + /// + /// # Example + /// ```rust + /// use std::path::Path; + /// use huginn_net::tls_keylog::TlsKeylog; + /// + /// let keylog = TlsKeylog::from_file(Path::new("/tmp/sslkeylog.txt"))?; + /// ``` + pub fn from_file>(path: P) -> Result { + let content = fs::read_to_string(path.as_ref()) + .map_err(|e| HuginnNetError::Parse(format!("Failed to read keylog file: {e}")))?; + + Self::from_string(&content) + } + + /// Load keylog from string content + /// + /// # Arguments + /// * `content` - Keylog file content + /// + /// # Returns + /// * `Ok(TlsKeylog)` - Successfully parsed keylog + /// * `Err(HuginnNetError)` - Failed to parse keylog + pub fn from_string(content: &str) -> Result { + let mut keylog = Self::new(); + + for (line_num, line) in content.lines().enumerate() { + let line = line.trim(); + + // Skip empty lines and comments + if line.is_empty() || line.starts_with('#') { + continue; + } + + match keylog.parse_line(line) { + Ok(Some(key_material)) => { + keylog.add_key_material(key_material); + } + Ok(None) => { + // Line was skipped (unknown format but not an error) + debug!( + "Skipped keylog line {}: {}", + line_num.saturating_add(1), + line + ); + } + Err(e) => { + warn!( + "Error parsing keylog line {}: {} - {}", + line_num.saturating_add(1), + line, + e + ); + // Continue parsing other lines instead of failing completely + } + } + } + + debug!("Loaded {} key entries from keylog", keylog.key_count); + Ok(keylog) + } + + /// Parse a single keylog line + /// + /// # Arguments + /// * `line` - Single line from keylog file + /// + /// # Returns + /// * `Ok(Some(KeyMaterial))` - Successfully parsed key material + /// * `Ok(None)` - Line was skipped (unknown format) + /// * `Err(HuginnNetError)` - Parse error + fn parse_line(&self, line: &str) -> Result, HuginnNetError> { + let parts: Vec<&str> = line.split_whitespace().collect(); + + if parts.len() != 3 { + return Ok(None); // Skip malformed lines + } + + let key_type = KeyType::from(parts[0]); + + // Only process known key types + if let KeyType::Unknown(_) = key_type { + return Ok(None); + } + + let client_random = hex::decode(parts[1]) + .map_err(|e| HuginnNetError::Parse(format!("Invalid client_random hex: {e}")))?; + + let key_data = hex::decode(parts[2]) + .map_err(|e| HuginnNetError::Parse(format!("Invalid key_data hex: {e}")))?; + + // Validate client_random length (should be 32 bytes) + if client_random.len() != 32 { + return Err(HuginnNetError::Parse(format!( + "Invalid client_random length: expected 32, got {}", + client_random.len() + ))); + } + + Ok(Some(KeyMaterial { + key_type, + client_random, + key_data, + })) + } + + /// Add key material to the keylog + fn add_key_material(&mut self, key_material: KeyMaterial) { + let client_random = key_material.client_random.clone(); + + self.keys + .entry(client_random) + .or_default() + .push(key_material); + + self.key_count = self.key_count.saturating_add(1); + } + + /// Find key material by client random + /// + /// # Arguments + /// * `client_random` - 32-byte client random from TLS handshake + /// + /// # Returns + /// * `Some(&[KeyMaterial])` - Found key material for this client random + /// * `None` - No key material found + pub fn find_keys(&self, client_random: &[u8]) -> Option<&[KeyMaterial]> { + self.keys.get(client_random).map(|v| v.as_slice()) + } + + /// Find specific key type by client random + /// + /// # Arguments + /// * `client_random` - 32-byte client random from TLS handshake + /// * `key_type` - Type of key to find + /// + /// # Returns + /// * `Some(&KeyMaterial)` - Found key material of specified type + /// * `None` - No key material of this type found + pub fn find_key_by_type( + &self, + client_random: &[u8], + key_type: &KeyType, + ) -> Option<&KeyMaterial> { + self.find_keys(client_random)? + .iter() + .find(|key| &key.key_type == key_type) + } + + /// Get total number of keys loaded + pub fn key_count(&self) -> usize { + self.key_count + } + + /// Get number of unique client randoms + pub fn client_count(&self) -> usize { + self.keys.len() + } + + /// Check if keylog is empty + pub fn is_empty(&self) -> bool { + self.keys.is_empty() + } +} + +impl Default for TlsKeylog { + fn default() -> Self { + Self::new() + } +} + +/// Manager for multiple TLS keylog files +/// +/// Handles multiple keylog files for different certificates/domains. +/// Searches across all loaded keylogs to find the appropriate key material. +#[derive(Debug, Clone)] +pub struct TlsKeylogManager { + /// List of loaded keylogs with their source information + keylogs: Vec<(String, TlsKeylog)>, // (source_name, keylog) + /// Total number of keys across all keylogs + total_keys: usize, +} + +impl TlsKeylogManager { + /// Create a new empty keylog manager + pub fn new() -> Self { + Self { + keylogs: Vec::new(), + total_keys: 0, + } + } + + /// Load keylogs from multiple files + /// + /// # Arguments + /// * `paths` - Vector of paths to keylog files + /// + /// # Returns + /// * `Ok(TlsKeylogManager)` - Successfully loaded manager + /// * `Err(HuginnNetError)` - Failed to load one or more keylogs + /// + /// # Example + /// ```rust + /// use std::path::PathBuf; + /// use huginn_net::tls_keylog::TlsKeylogManager; + /// + /// let paths = vec![ + /// PathBuf::from("/tmp/example.com.keylog"), + /// PathBuf::from("/tmp/api.example.com.keylog"), + /// PathBuf::from("/tmp/cdn.example.com.keylog"), + /// ]; + /// let manager = TlsKeylogManager::from_files(&paths)?; + /// ``` + pub fn from_files>(paths: &[P]) -> Result { + let mut manager = Self::new(); + + for path in paths { + let path_ref = path.as_ref(); + let source_name = path_ref + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("unknown") + .to_string(); + + match TlsKeylog::from_file(path_ref) { + Ok(keylog) => { + debug!( + "Loaded keylog from {}: {} keys", + source_name, + keylog.key_count() + ); + manager.add_keylog(source_name, keylog); + } + Err(e) => { + warn!("Failed to load keylog from {}: {}", source_name, e); + // Continue loading other keylogs instead of failing completely + } + } + } + + if manager.keylogs.is_empty() { + return Err(HuginnNetError::Parse( + "No keylog files could be loaded".to_string(), + )); + } + + debug!( + "Loaded {} keylog files with {} total keys", + manager.keylogs.len(), + manager.total_keys + ); + + Ok(manager) + } + + /// Add a keylog from string content with a source name + /// + /// # Arguments + /// * `source_name` - Name/identifier for this keylog (e.g., domain name) + /// * `content` - Keylog file content + /// + /// # Returns + /// * `Ok(())` - Successfully added keylog + /// * `Err(HuginnNetError)` - Failed to parse keylog + pub fn add_keylog_from_string( + &mut self, + source_name: String, + content: &str, + ) -> Result<(), HuginnNetError> { + let keylog = TlsKeylog::from_string(content)?; + self.add_keylog(source_name, keylog); + Ok(()) + } + + /// Add a pre-loaded keylog + fn add_keylog(&mut self, source_name: String, keylog: TlsKeylog) { + self.total_keys = self.total_keys.saturating_add(keylog.key_count()); + self.keylogs.push((source_name, keylog)); + } + + /// Find key material by client random across all keylogs + /// + /// # Arguments + /// * `client_random` - 32-byte client random from TLS handshake + /// + /// # Returns + /// * `Some((&str, &[KeyMaterial]))` - Found key material with source name + /// * `None` - No key material found in any keylog + pub fn find_keys(&self, client_random: &[u8]) -> Option<(&str, &[KeyMaterial])> { + for (source_name, keylog) in &self.keylogs { + if let Some(keys) = keylog.find_keys(client_random) { + return Some((source_name, keys)); + } + } + None + } + + /// Find specific key type by client random across all keylogs + /// + /// # Arguments + /// * `client_random` - 32-byte client random from TLS handshake + /// * `key_type` - Type of key to find + /// + /// # Returns + /// * `Some((&str, &KeyMaterial))` - Found key material with source name + /// * `None` - No key material of this type found in any keylog + pub fn find_key_by_type( + &self, + client_random: &[u8], + key_type: &KeyType, + ) -> Option<(&str, &KeyMaterial)> { + for (source_name, keylog) in &self.keylogs { + if let Some(key) = keylog.find_key_by_type(client_random, key_type) { + return Some((source_name, key)); + } + } + None + } + + /// Get total number of keys across all keylogs + pub fn total_key_count(&self) -> usize { + self.total_keys + } + + /// Get number of loaded keylog files + pub fn keylog_count(&self) -> usize { + self.keylogs.len() + } + + /// Get total number of unique client randoms across all keylogs + pub fn total_client_count(&self) -> usize { + self.keylogs + .iter() + .map(|(_, keylog)| keylog.client_count()) + .sum() + } + + /// Check if manager has any keylogs loaded + pub fn is_empty(&self) -> bool { + self.keylogs.is_empty() + } + + /// Get information about loaded keylogs + pub fn keylog_info(&self) -> Vec<(String, usize, usize)> { + self.keylogs + .iter() + .map(|(name, keylog)| (name.clone(), keylog.key_count(), keylog.client_count())) + .collect() + } + + /// Find all keylogs that contain keys for a specific client random + /// + /// This is useful for debugging when multiple keylogs might have keys for the same session + pub fn find_all_matching_keylogs(&self, client_random: &[u8]) -> Vec<(&str, &[KeyMaterial])> { + self.keylogs + .iter() + .filter_map(|(source_name, keylog)| { + keylog + .find_keys(client_random) + .map(|keys| (source_name.as_str(), keys)) + }) + .collect() + } +} + +impl Default for TlsKeylogManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_client_random_line() { + let keylog = TlsKeylog::new(); + let line = "CLIENT_RANDOM 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; + + match keylog.parse_line(line) { + Ok(Some(result)) => { + assert_eq!(result.key_type, KeyType::ClientRandom); + assert_eq!(result.client_random.len(), 32); + assert_eq!(result.key_data.len(), 32); + } + _ => panic!("Should parse line and have key material"), + } + } + + #[test] + fn test_parse_tls13_line() { + let keylog = TlsKeylog::new(); + let line = "CLIENT_TRAFFIC_SECRET_0 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; + + match keylog.parse_line(line) { + Ok(Some(result)) => { + assert_eq!(result.key_type, KeyType::ClientTrafficSecret0); + } + _ => panic!("Should parse line and have key material"), + } + } + + #[test] + fn test_skip_unknown_line() { + let keylog = TlsKeylog::new(); + let line = "UNKNOWN_KEY_TYPE 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; + + match keylog.parse_line(line) { + Ok(None) => { + // Expected: unknown key types should be skipped + } + _ => panic!("Should parse line but return None for unknown key type"), + } + } + + #[test] + fn test_skip_comment_and_empty_lines() { + let content = r#" +# This is a comment +CLIENT_RANDOM 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 + +# Another comment +CLIENT_TRAFFIC_SECRET_0 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 +"#; + + match TlsKeylog::from_string(content) { + Ok(keylog) => { + assert_eq!(keylog.key_count(), 2); + assert_eq!(keylog.client_count(), 1); + } + Err(_) => panic!("Should parse keylog content"), + } + } + + #[test] + fn test_find_keys() { + let content = r#" +CLIENT_RANDOM 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 +CLIENT_TRAFFIC_SECRET_0 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 +"#; + + let keylog = match TlsKeylog::from_string(content) { + Ok(keylog) => keylog, + Err(_) => panic!("Should parse keylog"), + }; + + let client_random = + match hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") { + Ok(bytes) => bytes, + Err(_) => panic!("Should decode hex"), + }; + + let keys = match keylog.find_keys(&client_random) { + Some(keys) => keys, + None => panic!("Should find keys"), + }; + assert_eq!(keys.len(), 2); + + let master_secret = keylog.find_key_by_type(&client_random, &KeyType::ClientRandom); + assert!(master_secret.is_some()); + + let traffic_secret = + keylog.find_key_by_type(&client_random, &KeyType::ClientTrafficSecret0); + assert!(traffic_secret.is_some()); + } + + #[test] + fn test_keylog_manager_multiple_files() { + let content1 = r#" +CLIENT_RANDOM 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 +"#; + let content2 = r#" +CLIENT_RANDOM abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba +CLIENT_TRAFFIC_SECRET_0 abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876 +"#; + + let mut manager = TlsKeylogManager::new(); + + if manager + .add_keylog_from_string("example.com".to_string(), content1) + .is_err() + { + panic!("Should add keylog"); + } + if manager + .add_keylog_from_string("api.example.com".to_string(), content2) + .is_err() + { + panic!("Should add keylog"); + } + + assert_eq!(manager.keylog_count(), 2); + assert_eq!(manager.total_key_count(), 3); + assert_eq!(manager.total_client_count(), 2); + + // Test finding keys from first keylog + let client_random1 = + match hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") { + Ok(bytes) => bytes, + Err(_) => panic!("Should decode hex"), + }; + let (source, keys) = match manager.find_keys(&client_random1) { + Some(result) => result, + None => panic!("Should find keys"), + }; + assert_eq!(source, "example.com"); + assert_eq!(keys.len(), 1); + + // Test finding keys from second keylog + let client_random2 = + match hex::decode("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789") { + Ok(bytes) => bytes, + Err(_) => panic!("Should decode hex"), + }; + let (source, keys) = match manager.find_keys(&client_random2) { + Some(result) => result, + None => panic!("Should find keys"), + }; + assert_eq!(source, "api.example.com"); + assert_eq!(keys.len(), 2); + + // Test finding specific key type + let (source, _key) = + match manager.find_key_by_type(&client_random2, &KeyType::ClientTrafficSecret0) { + Some(result) => result, + None => panic!("Should find key"), + }; + assert_eq!(source, "api.example.com"); + } + + #[test] + fn test_keylog_manager_info() { + let content = r#" +CLIENT_RANDOM 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210 +CLIENT_TRAFFIC_SECRET_0 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 +"#; + + let mut manager = TlsKeylogManager::new(); + if manager + .add_keylog_from_string("test.com".to_string(), content) + .is_err() + { + panic!("Should add keylog"); + } + + let info = manager.keylog_info(); + assert_eq!(info.len(), 1); + assert_eq!(info[0].0, "test.com"); + assert_eq!(info[0].1, 2); // key count + assert_eq!(info[0].2, 1); // client count + } +}