pub(crate) use super::omikron_connection::{OmikronConnection, OmikronResult}; use mtp::codec::{CommunicationValue, DataValue}; pub(crate) trait RequiredMtpFields { /// Require a nonzero request correlation ID. `None` and `Some(0)` are /// distinct MTP wire states, but both are invalid for Omega requests. fn require_id(&self) -> OmikronResult; /// Require a nonzero authenticated peer identity. fn require_sender(&self) -> OmikronResult; /// Require a nonzero application routing target. fn require_receiver(&self) -> OmikronResult; fn require_sender_i64(&self) -> OmikronResult; fn require_receiver_i64(&self) -> OmikronResult; } impl RequiredMtpFields for CommunicationValue { fn require_id(&self) -> OmikronResult { self.id().filter(|id| *id != 0).ok_or_else(|| { crate::OmegaError::Validation("MTP message is missing request id".into()) }) } fn require_sender(&self) -> OmikronResult { self.sender() .filter(|sender| *sender != 0) .ok_or_else(|| crate::OmegaError::Validation("MTP message is missing sender".into())) } fn require_receiver(&self) -> OmikronResult { self.receiver() .filter(|receiver| *receiver != 0) .ok_or_else(|| crate::OmegaError::Validation("MTP message is missing receiver".into())) } fn require_sender_i64(&self) -> OmikronResult { positive_i64(self.require_sender()?, "sender") } fn require_receiver_i64(&self) -> OmikronResult { positive_i64(self.require_receiver()?, "receiver") } } pub(crate) fn positive_i64(value: impl TryInto, field: &str) -> OmikronResult { let value = value .try_into() .map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?; let value = i64::try_from(value) .map_err(|_| crate::OmegaError::Validation(format!("invalid {field}")))?; if value <= 0 { return Err(crate::OmegaError::Validation(format!("invalid {field}"))); } Ok(value) } pub(crate) fn validate_dispatch_fields(value: &CommunicationValue) -> OmikronResult<()> { let Some(message_type) = value.get_comm_type_enum() else { return Err(crate::OmegaError::Validation( "MTP message has an unknown communication type".into(), )); }; if !matches!( message_type, mtp::codec::CommunicationType::ClientChanged | mtp::codec::CommunicationType::PushNotification ) { value.require_id()?; } if matches!( message_type, mtp::codec::CommunicationType::GetUserData | mtp::codec::CommunicationType::ChangeUserData | mtp::codec::CommunicationType::DeleteUser | mtp::codec::CommunicationType::AttachUserBegin | mtp::codec::CommunicationType::AttachUserComplete | mtp::codec::CommunicationType::DeleteUserCredentialBegin | mtp::codec::CommunicationType::DeleteUserCredentialComplete | mtp::codec::CommunicationType::EraseHostedUserDataAck | mtp::codec::CommunicationType::ReleaseUserFromIota | mtp::codec::CommunicationType::DeleteIota | mtp::codec::CommunicationType::GetNotifications | mtp::codec::CommunicationType::ReadNotification | mtp::codec::CommunicationType::StateSubscribe ) { value.require_sender()?; } Ok(()) } pub(crate) trait OptionalDataValueCompat { fn as_number(&self) -> Option; fn as_signed_number(&self) -> Option; fn as_str(&self) -> Option<&str>; fn as_bytes(&self) -> Option>; } impl OptionalDataValueCompat for Option<&DataValue> { fn as_number(&self) -> Option { self.and_then(|value| value.as_number()) } fn as_signed_number(&self) -> Option { self.and_then(|value| value.as_signed_number()) } fn as_str(&self) -> Option<&str> { self.and_then(|value| value.as_str()) } fn as_bytes(&self) -> Option> { self.and_then(|value| value.as_bytes()) } } #[cfg(test)] mod tests { use super::{RequiredMtpFields, validate_dispatch_fields}; use mtp::codec::{CommunicationType, CommunicationValue}; #[test] fn missing_request_id_is_rejected_without_a_zero_fallback() { let value = CommunicationValue::new(CommunicationType::GetUserData).without_id(); assert!(value.require_id().is_err()); assert!(validate_dispatch_fields(&value).is_err()); } #[test] fn zero_routing_values_are_rejected_by_omega_contract() { let value = CommunicationValue::new(CommunicationType::GetUserData) .with_id(0) .with_sender(0) .with_receiver(0); assert!(value.require_id().is_err()); assert!(value.require_sender().is_err()); assert!(value.require_receiver().is_err()); assert!(validate_dispatch_fields(&value).is_err()); } #[test] fn security_sensitive_messages_require_sender() { for message_type in [ CommunicationType::GetUserData, CommunicationType::ChangeUserData, CommunicationType::DeleteUser, CommunicationType::AttachUserBegin, CommunicationType::AttachUserComplete, CommunicationType::DeleteUserCredentialBegin, CommunicationType::DeleteUserCredentialComplete, CommunicationType::EraseHostedUserDataAck, CommunicationType::ReleaseUserFromIota, CommunicationType::DeleteIota, CommunicationType::GetNotifications, CommunicationType::ReadNotification, CommunicationType::StateSubscribe, ] { let value = CommunicationValue::new(message_type).with_id(1); assert!(value.require_sender().is_err()); assert!(validate_dispatch_fields(&value).is_err()); } } #[test] fn optional_client_changed_id_remains_optional() { let value = CommunicationValue::new(CommunicationType::ClientChanged).without_id(); assert!(validate_dispatch_fields(&value).is_ok()); } #[test] fn push_notification_uses_its_logical_sender_field() { let value = CommunicationValue::new(CommunicationType::PushNotification); assert!(validate_dispatch_fields(&value).is_ok()); } }