//! Call key derivation for encrypted voice/video. use pyo3::prelude::*; use pyo3::types::PyBytes; use crate::errors::to_py_err; /// Call keys for encrypted voice/video media. /// /// Derived from a ratchet session's root key and an ephemeral KEM shared secret. /// Keys are role-assigned by fingerprint order — both parties get the same /// send/recv assignment without coordination. /// /// Use as a context manager for automatic zeroization:: /// /// with ratchet.derive_call_keys(kem_ss, call_id) as keys: /// send = keys.send_key() /// recv = keys.recv_key() /// keys.advance() # derive next round #[pyclass] pub struct CallKeys { inner: Option, } #[pymethods] impl CallKeys { /// Current send key (32 bytes). fn send_key<'py>(&self, py: Python<'py>) -> PyResult> { let inner = self .inner .as_ref() .ok_or_else(|| crate::errors::InvalidDataError::new_err("call keys consumed"))?; Ok(PyBytes::new(py, inner.send_key()).into()) } /// Current recv key (32 bytes). fn recv_key<'py>(&self, py: Python<'py>) -> PyResult> { let inner = self .inner .as_ref() .ok_or_else(|| crate::errors::InvalidDataError::new_err("call keys consumed"))?; Ok(PyBytes::new(py, inner.recv_key()).into()) } /// Advance the call chain — derives fresh keys, zeroizes old ones. /// /// Raises ``ChainExhaustedError`` after 2^24 advances. fn advance(&mut self) -> PyResult<()> { let inner = self .inner .as_mut() .ok_or_else(|| crate::errors::InvalidDataError::new_err("call keys consumed"))?; inner.advance().map_err(to_py_err) } /// Zeroize all key material. fn close(&mut self) { self.inner = None; } fn __enter__(slf: Py) -> Py { slf } fn __exit__( &mut self, _exc_type: Option<&Bound<'_, PyAny>>, _exc_val: Option<&Bound<'_, PyAny>>, _exc_tb: Option<&Bound<'_, PyAny>>, ) { self.close(); } } impl CallKeys { pub fn from_inner(inner: soliton::call::CallKeys) -> Self { Self { inner: Some(inner) } } } pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) }