use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::api::client::security::ClientSecurityContext;
use crate::api::common::config::{KeyExchangeMethod, SecurityConfig};
use crate::api::common::error::SecurityError;
use crate::api::common::unified_security::{
SecurityContextFactory, UnifiedSecurityContext,
};
use crate::api::server::security::ServerSecurityContext;
pub struct SecurityContextManager {
contexts: Arc<RwLock<HashMap<KeyExchangeMethod, SecurityContextType>>>,
method_preference: Vec<KeyExchangeMethod>,
active_method: Arc<RwLock<Option<KeyExchangeMethod>>>,
config: SecurityConfig,
}
#[derive(Clone)]
pub enum SecurityContextType {
Unified(Arc<UnifiedSecurityContext>),
DtlsClient(Arc<dyn ClientSecurityContext>),
DtlsServer(Arc<dyn ServerSecurityContext>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NegotiationStrategy {
FirstAvailable,
PreferenceWithFallback,
Strict,
AutoDetect,
}
#[derive(Debug, Clone)]
pub struct SecurityCapabilities {
pub supported_methods: Vec<KeyExchangeMethod>,
pub can_offer: bool,
pub can_answer: bool,
pub srtp_profiles: Vec<crate::api::common::config::SrtpProfile>,
}
impl SecurityContextManager {
pub fn new(config: SecurityConfig) -> Self {
let method_preference = vec![
KeyExchangeMethod::DtlsSrtp, KeyExchangeMethod::Sdes, KeyExchangeMethod::Zrtp, KeyExchangeMethod::Mikey, KeyExchangeMethod::PreSharedKey, ];
Self {
contexts: Arc::new(RwLock::new(HashMap::new())),
method_preference,
active_method: Arc::new(RwLock::new(None)),
config,
}
}
pub fn with_method_preference(
config: SecurityConfig,
preference: Vec<KeyExchangeMethod>,
) -> Self {
Self {
contexts: Arc::new(RwLock::new(HashMap::new())),
method_preference: preference,
active_method: Arc::new(RwLock::new(None)),
config,
}
}
pub async fn initialize(&self) -> Result<(), SecurityError> {
let mut contexts = self.contexts.write().await;
for method in &self.method_preference {
match method {
KeyExchangeMethod::DtlsSrtp => {
}
KeyExchangeMethod::Sdes
| KeyExchangeMethod::Mikey
| KeyExchangeMethod::Zrtp
| KeyExchangeMethod::PreSharedKey => {
let method_config = self.create_method_config(*method)?;
match SecurityContextFactory::create_context(method_config) {
Ok(unified_context) => {
contexts.insert(
*method,
SecurityContextType::Unified(Arc::new(unified_context)),
);
}
Err(e) => {
eprintln!(
"Warning: Failed to initialize {} context: {}",
self.method_name(*method),
e
);
}
}
}
}
}
Ok(())
}
fn create_method_config(
&self,
method: KeyExchangeMethod,
) -> Result<SecurityConfig, SecurityError> {
let mut config = self.config.clone();
config.mode = method.to_security_mode();
Ok(config)
}
fn method_name(&self, method: KeyExchangeMethod) -> &'static str {
match method {
KeyExchangeMethod::DtlsSrtp => "DTLS-SRTP",
KeyExchangeMethod::Sdes => "SDES-SRTP",
KeyExchangeMethod::Mikey => "MIKEY-SRTP",
KeyExchangeMethod::Zrtp => "ZRTP-SRTP",
KeyExchangeMethod::PreSharedKey => "PSK-SRTP",
}
}
pub async fn add_dtls_client_context(&self, context: Arc<dyn ClientSecurityContext>) {
let mut contexts = self.contexts.write().await;
contexts.insert(
KeyExchangeMethod::DtlsSrtp,
SecurityContextType::DtlsClient(context),
);
}
pub async fn add_dtls_server_context(&self, context: Arc<dyn ServerSecurityContext>) {
let mut contexts = self.contexts.write().await;
contexts.insert(
KeyExchangeMethod::DtlsSrtp,
SecurityContextType::DtlsServer(context),
);
}
pub async fn start_negotiation(&self, method: KeyExchangeMethod) -> Result<(), SecurityError> {
let contexts = self.contexts.read().await;
let context = contexts.get(&method).ok_or_else(|| {
SecurityError::Configuration(format!(
"Method {} not available",
self.method_name(method)
))
})?;
match context {
SecurityContextType::Unified(unified) => {
unified.initialize().await?;
*self.active_method.write().await = Some(method);
}
SecurityContextType::DtlsClient(_) | SecurityContextType::DtlsServer(_) => {
*self.active_method.write().await = Some(method);
}
}
Ok(())
}
pub async fn auto_negotiate(
&self,
strategy: NegotiationStrategy,
) -> Result<KeyExchangeMethod, SecurityError> {
let contexts = self.contexts.read().await;
match strategy {
NegotiationStrategy::FirstAvailable => {
for method in &self.method_preference {
if contexts.contains_key(method) {
let selected_method = *method;
drop(contexts);
self.start_negotiation(selected_method).await?;
return Ok(selected_method);
}
}
Err(SecurityError::Configuration(
"No security methods available".to_string(),
))
}
NegotiationStrategy::PreferenceWithFallback => {
let available_methods: Vec<KeyExchangeMethod> = self
.method_preference
.iter()
.filter(|method| contexts.contains_key(method))
.copied()
.collect();
drop(contexts);
for method in available_methods {
match self.start_negotiation(method).await {
Ok(_) => return Ok(method),
Err(_) => {
continue;
}
}
}
Err(SecurityError::Configuration(
"All security methods failed".to_string(),
))
}
NegotiationStrategy::Strict => {
let primary_method = self.config.mode.key_exchange_method().ok_or_else(|| {
SecurityError::Configuration("No primary method configured".to_string())
})?;
if contexts.contains_key(&primary_method) {
drop(contexts);
self.start_negotiation(primary_method).await?;
Ok(primary_method)
} else {
Err(SecurityError::Configuration(format!(
"Primary method {} not available",
self.method_name(primary_method)
)))
}
}
NegotiationStrategy::AutoDetect => {
drop(contexts);
Box::pin(self.auto_negotiate(NegotiationStrategy::FirstAvailable)).await
}
}
}
pub async fn process_signaling(
&self,
data: &[u8],
method: Option<KeyExchangeMethod>,
) -> Result<Option<Vec<u8>>, SecurityError> {
let method = match method {
Some(m) => m,
None => {
self.detect_method_from_signaling(data)?
}
};
let contexts = self.contexts.read().await;
let context = contexts.get(&method).ok_or_else(|| {
SecurityError::Configuration(format!(
"Method {} not available",
self.method_name(method)
))
})?;
match context {
SecurityContextType::Unified(unified) => unified.process_message(data).await,
SecurityContextType::DtlsClient(_) | SecurityContextType::DtlsServer(_) => {
Err(SecurityError::Configuration(
"DTLS signaling should be handled by DTLS contexts".to_string(),
))
}
}
}
fn detect_method_from_signaling(
&self,
data: &[u8],
) -> Result<KeyExchangeMethod, SecurityError> {
let data_str = std::str::from_utf8(data).unwrap_or("");
if data_str.contains("a=crypto:") {
Ok(KeyExchangeMethod::Sdes)
} else if data_str.contains("MIKEY") {
Ok(KeyExchangeMethod::Mikey)
} else if data_str.contains("zrtp-version") {
Ok(KeyExchangeMethod::Zrtp)
} else {
Ok(KeyExchangeMethod::Sdes)
}
}
pub async fn get_active_method(&self) -> Option<KeyExchangeMethod> {
*self.active_method.read().await
}
pub async fn is_established(&self) -> Result<bool, SecurityError> {
let active_method = self.get_active_method().await.ok_or_else(|| {
SecurityError::NotInitialized("No active security method".to_string())
})?;
let contexts = self.contexts.read().await;
let context = contexts.get(&active_method).ok_or_else(|| {
SecurityError::NotInitialized("Active method context not found".to_string())
})?;
match context {
SecurityContextType::Unified(unified) => Ok(unified.is_established().await),
SecurityContextType::DtlsClient(client) => client
.is_handshake_complete()
.await
.map_err(|e| SecurityError::CryptoError(format!("DTLS client error: {}", e))),
SecurityContextType::DtlsServer(server) => {
server
.is_ready()
.await
.map_err(|e| SecurityError::CryptoError(format!("DTLS server error: {}", e)))
}
}
}
pub async fn get_capabilities(&self) -> SecurityCapabilities {
let contexts = self.contexts.read().await;
let supported_methods: Vec<KeyExchangeMethod> = contexts.keys().copied().collect();
SecurityCapabilities {
supported_methods,
can_offer: true, can_answer: true, srtp_profiles: self.config.srtp_profiles.clone(),
}
}
pub async fn create_security_offer(
&self,
method: KeyExchangeMethod,
) -> Result<Vec<String>, SecurityError> {
let contexts = self.contexts.read().await;
let context = contexts.get(&method).ok_or_else(|| {
SecurityError::Configuration(format!(
"Method {} not available",
self.method_name(method)
))
})?;
match context {
SecurityContextType::Unified(_unified) => {
if method == KeyExchangeMethod::Sdes {
Ok(vec![
"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:placeholder".to_string(),
])
} else {
Err(SecurityError::Configuration(
"Offer generation not implemented for this method".to_string(),
))
}
}
SecurityContextType::DtlsClient(client) => {
let fingerprint = client.get_fingerprint().await.map_err(|e| {
SecurityError::CryptoError(format!("Failed to get fingerprint: {}", e))
})?;
Ok(vec![
format!("a=fingerprint:sha-256 {}", fingerprint),
"a=setup:actpass".to_string(),
])
}
SecurityContextType::DtlsServer(server) => {
let fingerprint = server.get_fingerprint().await.map_err(|e| {
SecurityError::CryptoError(format!("Failed to get fingerprint: {}", e))
})?;
Ok(vec![
format!("a=fingerprint:sha-256 {}", fingerprint),
"a=setup:passive".to_string(),
])
}
}
}
pub async fn list_available_methods(&self) -> Vec<KeyExchangeMethod> {
let contexts = self.contexts.read().await;
contexts.keys().copied().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common::config::{SecurityConfig, SecurityMode, SrtpProfile};
fn test_srtp_key() -> Vec<u8> {
vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
]
}
#[tokio::test]
async fn test_create_security_manager() {
let config = SecurityConfig::sdes_srtp();
let manager = SecurityContextManager::new(config);
assert_eq!(manager.get_active_method().await, None);
}
#[tokio::test]
async fn test_initialize_manager() {
let config = SecurityConfig::srtp_with_key(test_srtp_key());
let manager = SecurityContextManager::new(config);
let result = manager.initialize().await;
assert!(result.is_ok());
let methods = manager.list_available_methods().await;
assert!(methods.contains(&KeyExchangeMethod::PreSharedKey));
}
#[tokio::test]
async fn test_custom_method_preference() {
let config = SecurityConfig::sdes_srtp();
let preference = vec![
KeyExchangeMethod::Sdes,
KeyExchangeMethod::PreSharedKey,
KeyExchangeMethod::DtlsSrtp,
];
let manager = SecurityContextManager::with_method_preference(config, preference);
assert_eq!(manager.method_preference[0], KeyExchangeMethod::Sdes);
assert_eq!(
manager.method_preference[1],
KeyExchangeMethod::PreSharedKey
);
}
#[tokio::test]
async fn test_method_detection() {
let config = SecurityConfig::sdes_srtp();
let manager = SecurityContextManager::new(config);
let sdes_sdp = b"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:test";
let detected = manager.detect_method_from_signaling(sdes_sdp).unwrap();
assert_eq!(detected, KeyExchangeMethod::Sdes);
let mikey_data = b"MIKEY message content";
let detected = manager.detect_method_from_signaling(mikey_data).unwrap();
assert_eq!(detected, KeyExchangeMethod::Mikey);
let zrtp_data = b"zrtp-version: 1.10";
let detected = manager.detect_method_from_signaling(zrtp_data).unwrap();
assert_eq!(detected, KeyExchangeMethod::Zrtp);
let unknown_data = b"random signaling data";
let detected = manager.detect_method_from_signaling(unknown_data).unwrap();
assert_eq!(detected, KeyExchangeMethod::Sdes); }
#[tokio::test]
async fn test_method_name_mapping() {
let config = SecurityConfig::sdes_srtp();
let manager = SecurityContextManager::new(config);
assert_eq!(
manager.method_name(KeyExchangeMethod::DtlsSrtp),
"DTLS-SRTP"
);
assert_eq!(manager.method_name(KeyExchangeMethod::Sdes), "SDES-SRTP");
assert_eq!(manager.method_name(KeyExchangeMethod::Mikey), "MIKEY-SRTP");
assert_eq!(manager.method_name(KeyExchangeMethod::Zrtp), "ZRTP-SRTP");
assert_eq!(
manager.method_name(KeyExchangeMethod::PreSharedKey),
"PSK-SRTP"
);
}
#[tokio::test]
async fn test_security_capabilities() {
let config = SecurityConfig::srtp_with_key(test_srtp_key());
let manager = SecurityContextManager::new(config);
manager.initialize().await.unwrap();
let capabilities = manager.get_capabilities().await;
assert!(capabilities.can_offer);
assert!(capabilities.can_answer);
assert!(!capabilities.supported_methods.is_empty());
assert!(!capabilities.srtp_profiles.is_empty());
}
#[test]
fn test_negotiation_strategy_enum() {
let _ = NegotiationStrategy::FirstAvailable;
let _ = NegotiationStrategy::PreferenceWithFallback;
let _ = NegotiationStrategy::Strict;
let _ = NegotiationStrategy::AutoDetect;
}
#[test]
fn test_security_context_type_variants() {
use crate::api::common::unified_security::SecurityContextFactory;
use std::sync::Arc;
let unified_context = SecurityContextFactory::create_sdes_context().unwrap();
let _context_type = SecurityContextType::Unified(Arc::new(unified_context));
}
#[tokio::test]
async fn test_psk_negotiation() {
let config = SecurityConfig::srtp_with_key(test_srtp_key());
let manager = SecurityContextManager::new(config);
manager.initialize().await.unwrap();
let result = manager
.start_negotiation(KeyExchangeMethod::PreSharedKey)
.await;
assert!(result.is_ok());
assert_eq!(
manager.get_active_method().await,
Some(KeyExchangeMethod::PreSharedKey)
);
}
#[tokio::test]
async fn test_create_method_config() {
let config = SecurityConfig::sdes_srtp();
let manager = SecurityContextManager::new(config);
let method_config = manager
.create_method_config(KeyExchangeMethod::Sdes)
.unwrap();
assert_eq!(method_config.mode, SecurityMode::SdesSrtp);
}
#[tokio::test]
async fn test_auto_negotiate_no_methods() {
let config = SecurityConfig::sdes_srtp();
let manager = SecurityContextManager::with_method_preference(config, vec![]);
let result = manager
.auto_negotiate(NegotiationStrategy::FirstAvailable)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_manager_initialization_warnings() {
let config = SecurityConfig::zrtp_p2p(); let manager = SecurityContextManager::new(config);
let result = manager.initialize().await;
assert!(result.is_ok()); }
#[test]
fn test_security_capabilities_struct() {
let capabilities = SecurityCapabilities {
supported_methods: vec![KeyExchangeMethod::Sdes],
can_offer: true,
can_answer: false,
srtp_profiles: vec![SrtpProfile::AesCm128HmacSha1_80],
};
assert_eq!(capabilities.supported_methods.len(), 1);
assert!(capabilities.can_offer);
assert!(!capabilities.can_answer);
assert_eq!(capabilities.srtp_profiles.len(), 1);
}
}