use brynja_core::ProtocolVersion;
use super::RecordError;
pub const HEARTBEAT_EXTENSION_TYPE: u16 = 15;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ContentType {
ChangeCipherSpec,
Alert,
Handshake,
ApplicationData,
Heartbeat,
Tls12Cid,
Ack,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ContentTypeClass {
Assigned(ContentType),
Unassigned,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ContentTypeCode(u8);
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct WirePolicy {
version: ProtocolVersion,
}
impl ContentType {
#[must_use]
pub const fn code(self) -> u8 {
match self {
Self::ChangeCipherSpec => 20,
Self::Alert => 21,
Self::Handshake => 22,
Self::ApplicationData => 23,
Self::Heartbeat => 24,
Self::Tls12Cid => 25,
Self::Ack => 26,
}
}
}
impl ContentTypeCode {
#[must_use]
pub const fn classify(code: u8) -> Self {
Self(code)
}
#[must_use]
pub const fn code(self) -> u8 {
self.0
}
#[must_use]
pub const fn class(self) -> ContentTypeClass {
let assigned = match self.0 {
20 => Some(ContentType::ChangeCipherSpec),
21 => Some(ContentType::Alert),
22 => Some(ContentType::Handshake),
23 => Some(ContentType::ApplicationData),
24 => Some(ContentType::Heartbeat),
25 => Some(ContentType::Tls12Cid),
26 => Some(ContentType::Ack),
_ => None,
};
match assigned {
Some(content_type) => ContentTypeClass::Assigned(content_type),
None => ContentTypeClass::Unassigned,
}
}
}
impl WirePolicy {
#[must_use]
pub const fn for_version(version: ProtocolVersion) -> Self {
Self { version }
}
#[must_use]
pub const fn version(self) -> ProtocolVersion {
self.version
}
pub const fn reject_heartbeat_negotiation(
self,
extension_type: u16,
) -> Result<(), RecordError> {
let _ = self;
if extension_type == HEARTBEAT_EXTENSION_TYPE {
Err(RecordError::HeartbeatRejected)
} else {
Ok(())
}
}
pub fn admit_inner_content_type(
self,
code: ContentTypeCode,
) -> Result<ContentType, RecordError> {
let content_type = assigned(code)?;
if matches!(content_type, ContentType::Heartbeat) {
return Err(RecordError::HeartbeatRejected);
}
let admitted = match self.version {
ProtocolVersion::Tls13 => matches!(
content_type,
ContentType::Alert | ContentType::Handshake | ContentType::ApplicationData
),
ProtocolVersion::Dtls13 => matches!(
content_type,
ContentType::Alert
| ContentType::Handshake
| ContentType::ApplicationData
| ContentType::Ack
),
_ => return Err(RecordError::ProfileMismatch),
};
if admitted {
Ok(content_type)
} else {
Err(RecordError::UnsupportedContentType)
}
}
pub(crate) fn admit_plaintext(self, code: ContentTypeCode) -> Result<ContentType, RecordError> {
let content_type = assigned(code)?;
if matches!(content_type, ContentType::Heartbeat) {
return Err(RecordError::HeartbeatRejected);
}
let admitted = match self.version {
ProtocolVersion::Tls12 => matches!(
content_type,
ContentType::ChangeCipherSpec
| ContentType::Alert
| ContentType::Handshake
| ContentType::ApplicationData
),
ProtocolVersion::Tls13 => {
if matches!(content_type, ContentType::ApplicationData) {
return Err(RecordError::UnprotectedApplicationData);
}
matches!(
content_type,
ContentType::ChangeCipherSpec | ContentType::Alert | ContentType::Handshake
)
}
ProtocolVersion::Dtls12 => matches!(
content_type,
ContentType::ChangeCipherSpec
| ContentType::Alert
| ContentType::Handshake
| ContentType::ApplicationData
),
ProtocolVersion::Dtls13 => matches!(
content_type,
ContentType::Alert | ContentType::Handshake | ContentType::Ack
),
_ => false,
};
if admitted {
Ok(content_type)
} else {
Err(RecordError::UnsupportedContentType)
}
}
pub(crate) fn admit_ciphertext(
self,
code: ContentTypeCode,
) -> Result<ContentType, RecordError> {
if matches!(
code.class(),
ContentTypeClass::Assigned(ContentType::Heartbeat)
) {
return Err(RecordError::HeartbeatRejected);
}
match self.version {
ProtocolVersion::Tls13 => {
if code.code() == ContentType::ApplicationData.code() {
Ok(ContentType::ApplicationData)
} else {
Err(RecordError::InvalidCiphertextType)
}
}
ProtocolVersion::Tls12 | ProtocolVersion::Dtls12 => self.admit_plaintext(code),
ProtocolVersion::Dtls13 => Err(RecordError::ProfileMismatch),
_ => Err(RecordError::ProfileMismatch),
}
}
}
fn assigned(code: ContentTypeCode) -> Result<ContentType, RecordError> {
match code.class() {
ContentTypeClass::Assigned(content_type) => Ok(content_type),
ContentTypeClass::Unassigned => Err(RecordError::UnsupportedContentType),
}
}