use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use futures::stream::{self, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use tokio::time::timeout;
use super::{CipherStrength, CipherSuite};
use crate::Result;
use crate::constants::{
BUFFER_SIZE_DEFAULT, CIPHER_TEST_READ_TIMEOUT, CONTENT_TYPE_HANDSHAKE, DEFAULT_CONNECT_TIMEOUT,
HANDSHAKE_TYPE_SERVER_HELLO,
};
use crate::data::CIPHER_DB;
use crate::protocols::{Protocol, handshake::ClientHelloBuilder};
use crate::utils::network::Target;
type CipherBatchResult = Vec<(CipherSuite, Result<(bool, Option<u64>)>)>;
#[async_trait::async_trait]
pub trait CipherTestable: Send + Sync {
async fn test_all_protocols(&self) -> Result<HashMap<Protocol, ProtocolCipherSummary>>;
}
const BATCH_SIZE_MULTIPLIER: usize = 5;
const BACKOFF_BASE_DELAY_MS: u64 = 100;
const BACKOFF_MAX_EXPONENT: u32 = 4;
const RETRY_BACKOFF_SECS: u64 = 3;
const SERVER_HELLO_MIN_SIZE: usize = 44;
const SESSION_ID_LENGTH_OFFSET: usize = 43;
const CIPHER_SUITE_BASE_OFFSET: usize = 44;
#[allow(dead_code)]
struct TlsConnectionPool {
pool: Arc<Mutex<Vec<TcpStream>>>,
addr: SocketAddr,
max_size: usize,
connect_timeout: Duration,
retry_config: Option<crate::utils::retry::RetryConfig>,
}
impl TlsConnectionPool {
fn new(
addr: SocketAddr,
max_size: usize,
connect_timeout: Duration,
retry_config: Option<crate::utils::retry::RetryConfig>,
) -> Self {
Self {
pool: Arc::new(Mutex::new(Vec::with_capacity(max_size))),
addr,
max_size,
connect_timeout,
retry_config,
}
}
async fn acquire(&self) -> Result<TcpStream> {
{
let mut pool = self.pool.lock().await;
if let Some(stream) = pool.pop() {
tracing::trace!("Connection pool hit (size: {})", pool.len());
return Ok(stream);
}
}
tracing::trace!(
"Connection pool miss, establishing new connection to {}",
self.addr
);
crate::utils::network::connect_with_timeout(
self.addr,
self.connect_timeout,
self.retry_config.as_ref(),
)
.await
.map_err(|e| crate::TlsError::Other(format!("Connection failed: {}", e)))
}
#[allow(dead_code)]
async fn release(&self, stream: TcpStream) {
let mut pool = self.pool.lock().await;
if pool.len() < self.max_size {
pool.push(stream);
tracing::trace!("Connection returned to pool (size: {})", pool.len());
} else {
tracing::trace!("Connection pool full, dropping connection");
drop(stream);
}
}
#[allow(dead_code)]
async fn size(&self) -> usize {
self.pool.lock().await.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CipherTestResult {
pub cipher: CipherSuite,
pub supported: bool,
pub protocol: Protocol,
pub server_preference: Option<usize>, pub handshake_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolCipherSummary {
pub protocol: Protocol,
pub supported_ciphers: Vec<CipherSuite>,
pub server_ordered: bool,
pub server_preference: Vec<String>, pub preferred_cipher: Option<CipherSuite>,
pub counts: CipherCounts,
pub avg_handshake_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CipherCounts {
pub total: usize,
pub null_ciphers: usize,
pub export_ciphers: usize,
pub low_strength: usize,
pub medium_strength: usize,
pub high_strength: usize,
pub forward_secrecy: usize,
pub aead: usize,
}
struct CipherPreferenceAnalyzer {
first_choice: Option<u16>,
second_choice: Option<u16>,
third_choice: Option<u16>,
cipher_hexcodes: Vec<u16>,
reversed: Vec<u16>,
rotated: Option<Vec<u16>>,
}
impl CipherPreferenceAnalyzer {
fn new(
first_choice: Option<u16>,
second_choice: Option<u16>,
third_choice: Option<u16>,
cipher_hexcodes: Vec<u16>,
reversed: Vec<u16>,
rotated: Option<Vec<u16>>,
) -> Self {
Self {
first_choice,
second_choice,
third_choice,
cipher_hexcodes,
reversed,
rotated,
}
}
fn is_client_preference(&self) -> bool {
let follows_original = self
.first_choice
.is_some_and(|c| self.cipher_hexcodes.first() == Some(&c));
let follows_reversed = self
.second_choice
.is_some_and(|c| self.reversed.first() == Some(&c));
let follows_rotated = match (&self.third_choice, &self.rotated) {
(Some(third), Some(offered)) => offered.first() == Some(third),
(None, _) => true, _ => false,
};
follows_original && follows_reversed && follows_rotated
}
fn all_choices_same(&self) -> bool {
match self.third_choice {
Some(third) => {
self.first_choice == self.second_choice && self.second_choice == Some(third)
}
None => self.first_choice == self.second_choice,
}
}
fn mostly_same_different_positions(&self) -> bool {
let (Some(second), Some(third)) = (self.second_choice, self.third_choice) else {
return false;
};
if second != third {
return false;
}
let pos_in_reversed = self.reversed.iter().position(|&c| c == second);
let pos_in_rotated = self
.rotated
.as_ref()
.and_then(|offered| offered.iter().position(|&c| c == second));
match (pos_in_reversed, pos_in_rotated) {
(Some(pos2), Some(pos3)) => pos2 != pos3,
_ => false,
}
}
fn is_server_preference(&self) -> bool {
if self.is_client_preference() {
tracing::debug!(
"Server respects client cipher preference (consistently picks client's first choice)"
);
return false;
}
if self.all_choices_same() {
tracing::debug!("Server enforces cipher preference (chose same cipher in all tests)");
return true;
}
if self.mostly_same_different_positions() {
tracing::debug!(
"Server enforces cipher preference (chose same cipher in multiple tests from different positions)"
);
return true;
}
tracing::debug!(
"Server cipher preference unclear (mixed behavior detected, assuming server preference)"
);
true
}
fn build_preference_order(&self, supported_ciphers: &[CipherSuite]) -> Vec<String> {
let mut preference_order = Vec::new();
if let Some(chosen) = self.first_choice {
preference_order.push(format!("{:04x}", chosen));
for cipher in supported_ciphers {
if !preference_order.iter().any(|h| h == &cipher.hexcode) {
preference_order.push(cipher.hexcode.clone());
}
}
}
preference_order
}
}
pub struct CipherTester {
target: Target,
connect_timeout: Duration,
read_timeout: Duration,
test_all_ciphers: bool,
sleep_duration: Option<Duration>,
use_rdp: bool,
starttls_protocol: Option<crate::starttls::StarttlsProtocol>,
test_all_ips: bool,
retry_config: Option<crate::utils::retry::RetryConfig>,
max_concurrent_tests: usize,
connection_pool_size: usize,
}
impl CipherTester {
pub fn new(target: Target) -> Self {
let use_rdp = crate::protocols::rdp::RdpPreamble::should_use_rdp(target.port);
Self {
target,
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
read_timeout: CIPHER_TEST_READ_TIMEOUT,
test_all_ciphers: false,
sleep_duration: None,
use_rdp,
starttls_protocol: None,
test_all_ips: false,
retry_config: None,
max_concurrent_tests: 10, connection_pool_size: 10, }
}
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn with_read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = timeout;
self
}
pub fn test_all(mut self, enable: bool) -> Self {
self.test_all_ciphers = enable;
self
}
pub fn with_sleep(mut self, duration: Duration) -> Self {
self.sleep_duration = Some(duration);
self
}
pub fn with_rdp(mut self, enable: bool) -> Self {
self.use_rdp = enable;
self
}
pub fn with_starttls(mut self, protocol: Option<crate::starttls::StarttlsProtocol>) -> Self {
self.starttls_protocol = protocol;
self
}
pub fn with_test_all_ips(mut self, enable: bool) -> Self {
self.test_all_ips = enable;
self
}
pub fn with_retry_config(mut self, config: Option<crate::utils::retry::RetryConfig>) -> Self {
self.retry_config = config;
self
}
pub fn with_max_concurrent_tests(mut self, max: usize) -> Self {
self.max_concurrent_tests = max.max(1); self
}
pub fn with_connection_pool_size(mut self, size: usize) -> Self {
self.connection_pool_size = size;
self
}
pub async fn test_protocol_ciphers(&self, protocol: Protocol) -> Result<ProtocolCipherSummary> {
let ciphers = if self.test_all_ciphers {
CIPHER_DB.get_all_ciphers()
} else {
CIPHER_DB.get_recommended_ciphers()
};
let compatible_ciphers: Vec<CipherSuite> = ciphers
.into_iter()
.filter(|c| self.is_cipher_compatible_with_protocol(c, protocol))
.collect();
tracing::debug!(
"Testing {} compatible ciphers for {:?} with max {} concurrent connections (pool size: {})",
compatible_ciphers.len(),
protocol,
self.max_concurrent_tests,
self.connection_pool_size
);
let connection_pool = if self.connection_pool_size > 0 {
let addr = self.target.socket_addrs()[0];
Some(Arc::new(TlsConnectionPool::new(
addr,
self.connection_pool_size,
self.connect_timeout,
self.retry_config.clone(),
)))
} else {
None
};
let semaphore = Arc::new(Semaphore::new(self.max_concurrent_tests));
let tester = Arc::new(self);
let mut results: Vec<CipherTestResult> = Vec::new();
let mut enetdown_count = 0;
let mut current_batch_size = self.max_concurrent_tests;
let mut cipher_queue: Vec<CipherSuite> = compatible_ciphers;
let mut retry_queue: Vec<CipherSuite> = Vec::new();
let max_enetdown_retries = 3;
let mut retry_round = 0;
while !cipher_queue.is_empty() || !retry_queue.is_empty() {
if cipher_queue.is_empty() && !retry_queue.is_empty() {
retry_round += 1;
if retry_round > max_enetdown_retries {
tracing::warn!(
"Max ENETDOWN retries ({}) reached, {} ciphers could not be tested",
max_enetdown_retries,
retry_queue.len()
);
break;
}
cipher_queue = retry_queue;
retry_queue = Vec::new();
tracing::info!(
"Retrying {} ciphers that failed with ENETDOWN (attempt {}/{})",
cipher_queue.len(),
retry_round,
max_enetdown_retries
);
tokio::time::sleep(std::time::Duration::from_secs(RETRY_BACKOFF_SECS)).await;
}
let batch_size = current_batch_size * BATCH_SIZE_MULTIPLIER;
let batch: Vec<_> = cipher_queue
.drain(..cipher_queue.len().min(batch_size))
.collect();
if batch.is_empty() {
break;
}
let batch_results: CipherBatchResult =
stream::iter(batch)
.map(|cipher| {
let sem = semaphore.clone();
let tester_clone = tester.clone();
let pool_clone = connection_pool.clone();
async move {
let _permit = sem.acquire().await.expect("semaphore closed");
let result = if let Some(pool) = pool_clone {
tester_clone
.test_cipher_handshake_only(&cipher, protocol, Some(&pool))
.await
} else {
tester_clone
.test_cipher_handshake_only(&cipher, protocol, None)
.await
};
if let Some(sleep_dur) = tester_clone.sleep_duration {
tokio::time::sleep(sleep_dur).await;
}
(cipher, result)
}
})
.buffer_unordered(current_batch_size)
.collect::<Vec<_>>()
.await;
let mut batch_enetdown = 0;
let mut batch_other_errors = 0;
for (cipher, result) in batch_results {
match result {
Ok((supported, handshake_time_ms)) => {
results.push(CipherTestResult {
cipher,
supported,
protocol,
server_preference: None,
handshake_time_ms,
});
}
Err(e) => {
let err_msg = e.to_string().to_lowercase();
if err_msg.contains("network is down") || err_msg.contains("os error 50") {
batch_enetdown += 1;
retry_queue.push(cipher);
} else {
batch_other_errors += 1;
tracing::debug!("Cipher test error (non-ENETDOWN): {}", e);
}
}
}
}
enetdown_count += batch_enetdown;
if batch_enetdown > 0 {
let old_size = current_batch_size;
if batch_enetdown > current_batch_size / 2 {
current_batch_size = (current_batch_size / 3).max(1);
} else {
current_batch_size = (current_batch_size / 2).max(1);
}
let error_level = batch_enetdown.min(10) as u32;
let base_delay_ms =
BACKOFF_BASE_DELAY_MS * 2u64.pow(error_level.min(BACKOFF_MAX_EXPONENT));
use rand::Rng;
let jitter = rand::thread_rng().gen_range(0..=(base_delay_ms / 4));
let total_delay_ms = base_delay_ms + jitter;
tracing::warn!(
"Detected {} ENETDOWN error(s), reducing concurrency from {} to {} with {}ms exponential backoff",
batch_enetdown,
old_size,
current_batch_size,
total_delay_ms
);
tokio::time::sleep(std::time::Duration::from_millis(total_delay_ms)).await;
} else if current_batch_size < self.max_concurrent_tests && batch_other_errors == 0 {
current_batch_size = (current_batch_size + 1).min(self.max_concurrent_tests);
tracing::debug!(
"Successful batch, increasing concurrency to {}",
current_batch_size
);
}
if batch_other_errors > 0 {
tracing::debug!("Batch had {} other errors (ignored)", batch_other_errors);
}
}
if enetdown_count > 0 {
tracing::info!(
"Completed cipher testing with {} ENETDOWN error(s) recovered via adaptive backoff",
enetdown_count
);
}
let handshake_times: Vec<u64> =
results.iter().filter_map(|r| r.handshake_time_ms).collect();
let avg_handshake_time_ms = if !handshake_times.is_empty() {
Some(handshake_times.iter().sum::<u64>() / handshake_times.len() as u64)
} else {
None
};
let supported: Vec<CipherSuite> = results
.into_iter()
.filter(|r| r.supported)
.map(|r| r.cipher)
.collect();
tracing::debug!(
"Found {} supported ciphers for {:?}",
supported.len(),
protocol
);
let server_preference = self
.determine_server_preference(protocol, &supported)
.await?;
let server_ordered = !server_preference.is_empty();
let preferred_cipher = if !server_preference.is_empty() {
CIPHER_DB.get_by_hexcode(&server_preference[0])
} else {
supported.first().cloned()
};
let counts = self.calculate_cipher_counts(&supported);
Ok(ProtocolCipherSummary {
protocol,
supported_ciphers: supported,
server_ordered,
server_preference,
preferred_cipher,
counts,
avg_handshake_time_ms,
})
}
async fn test_cipher_handshake_only(
&self,
cipher: &CipherSuite,
protocol: Protocol,
pool: Option<&Arc<TlsConnectionPool>>,
) -> Result<(bool, Option<u64>)> {
let hexcode = match u16::from_str_radix(&cipher.hexcode, 16) {
Ok(h) => h,
Err(_) => return Ok((false, None)),
};
let start = std::time::Instant::now();
let supported = if let Some(pool) = pool {
self.try_cipher_handshake_with_pool(protocol, hexcode, pool)
.await?
} else {
self.try_cipher_handshake(protocol, hexcode).await?
};
let handshake_time_ms = if supported {
Some(start.elapsed().as_millis() as u64)
} else {
None
};
Ok((supported, handshake_time_ms))
}
pub async fn test_single_cipher(
&self,
cipher: &CipherSuite,
protocol: Protocol,
) -> Result<CipherTestResult> {
let (supported, handshake_time_ms) = self
.test_cipher_handshake_only(cipher, protocol, None)
.await?;
Ok(CipherTestResult {
cipher: cipher.clone(),
supported,
protocol,
server_preference: None,
handshake_time_ms,
})
}
async fn try_cipher_handshake_with_pool(
&self,
protocol: Protocol,
cipher_hexcode: u16,
pool: &Arc<TlsConnectionPool>,
) -> Result<bool> {
if self.test_all_ips {
self.try_cipher_handshake_all_ips(protocol, cipher_hexcode)
.await
} else {
let addr = self.target.socket_addrs()[0];
self.try_cipher_handshake_on_ip_with_pool(protocol, cipher_hexcode, addr, pool)
.await
}
}
async fn try_cipher_handshake(&self, protocol: Protocol, cipher_hexcode: u16) -> Result<bool> {
if self.test_all_ips {
self.try_cipher_handshake_all_ips(protocol, cipher_hexcode)
.await
} else {
let addr = self.target.socket_addrs()[0];
self.try_cipher_handshake_on_ip(protocol, cipher_hexcode, addr)
.await
}
}
async fn try_cipher_handshake_all_ips(
&self,
protocol: Protocol,
cipher_hexcode: u16,
) -> Result<bool> {
let addrs = self.target.socket_addrs();
if addrs.is_empty() {
return Ok(false);
}
let mut all_support = true;
for addr in &addrs {
let ip_supports = self
.try_cipher_handshake_on_ip(protocol, cipher_hexcode, *addr)
.await?;
if !ip_supports {
all_support = false;
break; }
}
Ok(all_support)
}
async fn perform_cipher_handshake(
&self,
stream: &mut TcpStream,
protocol: Protocol,
cipher_hexcode: u16,
) -> Result<bool> {
if self.use_rdp
&& crate::protocols::rdp::RdpPreamble::send(stream)
.await
.is_err()
{
return Ok(false);
}
if let Some(starttls_proto) = self.starttls_protocol {
let negotiator = crate::starttls::protocols::get_negotiator(
starttls_proto,
self.target.hostname.clone(),
);
if negotiator.negotiate_starttls(stream).await.is_err() {
return Ok(false);
}
}
let mut builder = ClientHelloBuilder::new(protocol);
builder.add_cipher(cipher_hexcode);
let client_hello = builder.build_with_defaults(Some(&self.target.hostname))?;
match timeout(self.read_timeout, async {
stream.write_all(&client_hello).await?;
let mut response = vec![0u8; BUFFER_SIZE_DEFAULT];
let n = stream.read(&mut response).await?;
if n == 0 {
return Ok(false);
}
if n >= 6
&& response[0] == CONTENT_TYPE_HANDSHAKE
&& response[5] == HANDSHAKE_TYPE_SERVER_HELLO
{
return Ok(true);
}
Ok(false)
})
.await
{
Ok(result) => result,
Err(_) => Ok(false), }
}
async fn try_cipher_handshake_on_ip_with_pool(
&self,
protocol: Protocol,
cipher_hexcode: u16,
_addr: std::net::SocketAddr,
pool: &Arc<TlsConnectionPool>,
) -> Result<bool> {
let mut stream = match pool.acquire().await {
Ok(s) => s,
Err(_) => return Ok(false),
};
self.perform_cipher_handshake(&mut stream, protocol, cipher_hexcode)
.await
}
async fn try_cipher_handshake_on_ip(
&self,
protocol: Protocol,
cipher_hexcode: u16,
addr: std::net::SocketAddr,
) -> Result<bool> {
let mut stream = match crate::utils::network::connect_with_timeout(
addr,
self.connect_timeout,
self.retry_config.as_ref(),
)
.await
{
Ok(s) => s,
Err(_) => return Ok(false),
};
self.perform_cipher_handshake(&mut stream, protocol, cipher_hexcode)
.await
}
async fn determine_server_preference(
&self,
protocol: Protocol,
supported_ciphers: &[CipherSuite],
) -> Result<Vec<String>> {
if supported_ciphers.is_empty() {
return Ok(Vec::new());
}
let cipher_hexcodes: Vec<u16> = supported_ciphers
.iter()
.filter_map(|c| u16::from_str_radix(&c.hexcode, 16).ok())
.collect();
if cipher_hexcodes.len() < 2 {
return Ok(supported_ciphers
.iter()
.map(|c| c.hexcode.to_string())
.collect());
}
let first_choice = self
.get_server_chosen_cipher(protocol, &cipher_hexcodes)
.await?;
tracing::debug!(
"Cipher preference test 1 (original order): client offered {:04x?}, server chose {:04x?}",
cipher_hexcodes,
first_choice
);
let mut reversed = cipher_hexcodes.clone();
reversed.reverse();
let second_choice = self.get_server_chosen_cipher(protocol, &reversed).await?;
tracing::debug!(
"Cipher preference test 2 (reversed order): client offered {:04x?}, server chose {:04x?}",
reversed,
second_choice
);
let (third_choice, rotated) = if cipher_hexcodes.len() >= 3 {
let mut rotated = cipher_hexcodes.clone();
if let Some(last) = rotated.pop() {
rotated.insert(0, last);
}
let choice = self.get_server_chosen_cipher(protocol, &rotated).await?;
tracing::debug!(
"Cipher preference test 3 (rotated order): client offered {:04x?}, server chose {:04x?}",
rotated,
choice
);
(choice, Some(rotated))
} else {
(None, None)
};
let analyzer = CipherPreferenceAnalyzer::new(
first_choice,
second_choice,
third_choice,
cipher_hexcodes,
reversed,
rotated,
);
if analyzer.is_server_preference() {
Ok(analyzer.build_preference_order(supported_ciphers))
} else {
Ok(Vec::new())
}
}
async fn get_server_chosen_cipher(
&self,
protocol: Protocol,
cipher_hexcodes: &[u16],
) -> Result<Option<u16>> {
let addr = self.target.socket_addrs()[0];
let mut stream = match crate::utils::network::connect_with_timeout(
addr,
self.connect_timeout,
self.retry_config.as_ref(),
)
.await
{
Ok(s) => s,
Err(_) => return Ok(None),
};
if let Some(starttls_proto) = self.starttls_protocol {
let negotiator = crate::starttls::protocols::get_negotiator(
starttls_proto,
self.target.hostname.clone(),
);
if negotiator.negotiate_starttls(&mut stream).await.is_err() {
return Ok(None);
}
}
let mut builder = ClientHelloBuilder::new(protocol);
builder.add_ciphers(cipher_hexcodes);
let client_hello = builder.build_with_defaults(Some(&self.target.hostname))?;
match timeout(self.read_timeout, async {
stream.write_all(&client_hello).await?;
let mut response = vec![0u8; BUFFER_SIZE_DEFAULT];
let bytes_read = stream.read(&mut response).await?;
if bytes_read >= SERVER_HELLO_MIN_SIZE
&& response[0] == CONTENT_TYPE_HANDSHAKE
&& response[5] == HANDSHAKE_TYPE_SERVER_HELLO
{
let session_id_len = response[SESSION_ID_LENGTH_OFFSET] as usize;
let cipher_offset = CIPHER_SUITE_BASE_OFFSET + session_id_len;
tracing::debug!(
"ServerHello: session_id_len={}, cipher_offset={}, response_len={}",
session_id_len,
cipher_offset,
bytes_read
);
if bytes_read >= cipher_offset + 2 {
let cipher =
u16::from_be_bytes([response[cipher_offset], response[cipher_offset + 1]]);
tracing::debug!("Server chose cipher: 0x{:04x}", cipher);
return Ok(Some(cipher));
}
}
Ok(None)
})
.await
{
Ok(result) => result,
Err(_) => Ok(None),
}
}
fn is_cipher_compatible_with_protocol(&self, cipher: &CipherSuite, protocol: Protocol) -> bool {
if matches!(protocol, Protocol::TLS13) {
return cipher.protocol.contains("TLS13") || cipher.protocol.contains("TLSv1.3");
}
if matches!(protocol, Protocol::SSLv2) {
return cipher.protocol.contains("SSLv2");
}
!cipher.protocol.contains("TLS13") && !cipher.protocol.contains("SSLv2")
}
fn calculate_cipher_counts(&self, ciphers: &[CipherSuite]) -> CipherCounts {
let mut counts = CipherCounts {
total: ciphers.len(),
..Default::default()
};
for cipher in ciphers {
match cipher.strength() {
CipherStrength::NULL => counts.null_ciphers += 1,
CipherStrength::Export => counts.export_ciphers += 1,
CipherStrength::Low => counts.low_strength += 1,
CipherStrength::Medium => counts.medium_strength += 1,
CipherStrength::High => counts.high_strength += 1,
}
if cipher.has_forward_secrecy() {
counts.forward_secrecy += 1;
}
if cipher.is_aead() {
counts.aead += 1;
}
}
counts
}
pub async fn test_all_protocols(&self) -> Result<HashMap<Protocol, ProtocolCipherSummary>> {
let mut results = HashMap::new();
for protocol in Protocol::all() {
if matches!(protocol, Protocol::QUIC) {
continue;
}
let summary = self.test_protocol_ciphers(protocol).await?;
if !summary.supported_ciphers.is_empty() {
results.insert(protocol, summary);
}
}
Ok(results)
}
pub async fn quick_test(&self, protocol: Protocol) -> Result<Vec<CipherSuite>> {
let common_ciphers = CIPHER_DB.get_recommended_ciphers();
let mut supported = Vec::new();
for cipher in common_ciphers {
if self.is_cipher_compatible_with_protocol(&cipher, protocol) {
let result = self.test_single_cipher(&cipher, protocol).await?;
if result.supported {
supported.push(cipher);
}
}
}
Ok(supported)
}
}
#[async_trait::async_trait]
impl CipherTestable for CipherTester {
async fn test_all_protocols(&self) -> Result<HashMap<Protocol, ProtocolCipherSummary>> {
self.test_all_protocols().await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore] async fn test_cipher_detection() {
let target = Target::parse("www.google.com:443")
.await
.expect("test assertion should succeed");
let tester = CipherTester::new(target);
let summary = tester
.test_protocol_ciphers(Protocol::TLS12)
.await
.expect("test assertion should succeed");
assert!(!summary.supported_ciphers.is_empty());
assert!(summary.counts.forward_secrecy > 0);
assert_eq!(summary.counts.null_ciphers, 0);
assert_eq!(summary.counts.export_ciphers, 0);
}
#[tokio::test]
#[ignore] async fn test_server_preference() {
let target = Target::parse("www.google.com:443")
.await
.expect("test assertion should succeed");
let tester = CipherTester::new(target);
let summary = tester
.test_protocol_ciphers(Protocol::TLS12)
.await
.expect("test assertion should succeed");
assert!(summary.server_ordered);
assert!(!summary.server_preference.is_empty());
}
#[tokio::test]
#[ignore] async fn test_quick_scan() {
let target = Target::parse("www.google.com:443")
.await
.expect("test assertion should succeed");
let tester = CipherTester::new(target);
let ciphers = tester
.quick_test(Protocol::TLS12)
.await
.expect("test assertion should succeed");
assert!(!ciphers.is_empty());
}
#[test]
fn test_cipher_strength_calculation() {
let cipher = CipherSuite {
hexcode: "c030".to_string(),
openssl_name: "ECDHE-RSA-AES256-GCM-SHA384".to_string(),
iana_name: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384".to_string(),
protocol: "TLSv1.2".to_string(),
key_exchange: "ECDHE".to_string(),
authentication: "RSA".to_string(),
encryption: "AES256-GCM".to_string(),
mac: "SHA384".to_string(),
bits: 256,
export: false,
};
assert_eq!(cipher.strength(), CipherStrength::High);
assert!(cipher.has_forward_secrecy());
assert!(cipher.is_aead());
}
}