use brynja_core::{ProtocolFamily, ProtocolVersion, ReadCursor, WriteCursor};
use super::{
ContentType, ContentTypeCode, MAX_PLAINTEXT_LENGTH, MAX_TLS12_CIPHERTEXT_LENGTH,
MAX_TLS13_CIPHERTEXT_LENGTH, RecordError, WirePolicy,
};
const HEADER_LENGTH: usize = 5;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct LegacyRecordVersion([u8; 2]);
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct TlsPlaintext<'input> {
content_type: ContentType,
legacy_record_version: LegacyRecordVersion,
fragment: &'input [u8],
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct TlsCiphertext<'input> {
content_type: ContentType,
legacy_record_version: LegacyRecordVersion,
fragment: &'input [u8],
}
impl LegacyRecordVersion {
#[must_use]
pub const fn from_bytes(bytes: [u8; 2]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn bytes(self) -> [u8; 2] {
self.0
}
#[must_use]
pub const fn tls13_default() -> Self {
Self([3, 3])
}
#[must_use]
pub const fn tls13_initial_client_hello() -> Self {
Self([3, 1])
}
#[must_use]
pub const fn dtls13_default() -> Self {
Self([254, 253])
}
#[must_use]
pub const fn dtls13_initial_client_hello() -> Self {
Self([254, 255])
}
}
impl<'input> TlsPlaintext<'input> {
pub fn parse(
policy: WirePolicy,
input: &'input [u8],
) -> Result<(Self, &'input [u8]), RecordError> {
require_tls(policy)?;
let mut cursor = ReadCursor::new(input);
let content_type = read_content_type(&mut cursor, policy, false)?;
let version = read_version(&mut cursor)?;
let length = read_u16(&mut cursor)?;
validate_plaintext_length(content_type, length)?;
let fragment = take(&mut cursor, length)?;
let remaining = cursor.remaining();
Ok((
Self {
content_type,
legacy_record_version: version,
fragment,
},
remaining,
))
}
pub fn new(
policy: WirePolicy,
content_type: ContentTypeCode,
legacy_record_version: LegacyRecordVersion,
fragment: &'input [u8],
) -> Result<Self, RecordError> {
require_tls(policy)?;
let content_type = policy.admit_plaintext(content_type)?;
validate_plaintext_length(content_type, fragment.len())?;
if matches!(policy.version(), ProtocolVersion::Tls13)
&& !matches!(legacy_record_version.bytes(), [3, 3] | [3, 1])
{
return Err(RecordError::InvalidPlaintextVersion);
}
Ok(Self {
content_type,
legacy_record_version,
fragment,
})
}
#[must_use]
pub const fn content_type(&self) -> ContentType {
self.content_type
}
#[must_use]
pub const fn legacy_record_version(&self) -> LegacyRecordVersion {
self.legacy_record_version
}
#[must_use]
pub const fn fragment(&self) -> &'input [u8] {
self.fragment
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
HEADER_LENGTH.saturating_add(self.fragment.len())
}
pub fn encode(&self, output: &mut [u8]) -> Result<usize, RecordError> {
encode_record(
self.content_type,
self.legacy_record_version,
self.fragment,
output,
)
}
}
impl<'input> TlsCiphertext<'input> {
pub fn parse(
policy: WirePolicy,
input: &'input [u8],
) -> Result<(Self, &'input [u8]), RecordError> {
require_tls(policy)?;
let mut cursor = ReadCursor::new(input);
let content_type = read_content_type(&mut cursor, policy, true)?;
let version = read_version(&mut cursor)?;
validate_ciphertext_version(policy, version)?;
let length = read_u16(&mut cursor)?;
validate_ciphertext_length(policy, length)?;
let fragment = take(&mut cursor, length)?;
let remaining = cursor.remaining();
Ok((
Self {
content_type,
legacy_record_version: version,
fragment,
},
remaining,
))
}
pub fn new(
policy: WirePolicy,
content_type: ContentTypeCode,
legacy_record_version: LegacyRecordVersion,
fragment: &'input [u8],
) -> Result<Self, RecordError> {
require_tls(policy)?;
let content_type = policy.admit_ciphertext(content_type)?;
validate_ciphertext_version(policy, legacy_record_version)?;
validate_ciphertext_length(policy, fragment.len())?;
Ok(Self {
content_type,
legacy_record_version,
fragment,
})
}
#[must_use]
pub const fn content_type(&self) -> ContentType {
self.content_type
}
#[must_use]
pub const fn legacy_record_version(&self) -> LegacyRecordVersion {
self.legacy_record_version
}
#[must_use]
pub const fn fragment(&self) -> &'input [u8] {
self.fragment
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
HEADER_LENGTH.saturating_add(self.fragment.len())
}
pub fn encode(&self, output: &mut [u8]) -> Result<usize, RecordError> {
encode_record(
self.content_type,
self.legacy_record_version,
self.fragment,
output,
)
}
}
fn require_tls(policy: WirePolicy) -> Result<(), RecordError> {
if matches!(policy.version().family(), ProtocolFamily::Tls) {
Ok(())
} else {
Err(RecordError::ProfileMismatch)
}
}
fn read_content_type(
cursor: &mut ReadCursor<'_>,
policy: WirePolicy,
ciphertext: bool,
) -> Result<ContentType, RecordError> {
let bytes = take(cursor, 1)?;
let code = match bytes.first() {
Some(code) => ContentTypeCode::classify(*code),
None => return Err(RecordError::Truncated),
};
if ciphertext {
policy.admit_ciphertext(code)
} else {
policy.admit_plaintext(code)
}
}
fn read_version(cursor: &mut ReadCursor<'_>) -> Result<LegacyRecordVersion, RecordError> {
let bytes = cursor
.take_array::<2>()
.map_err(|_| RecordError::Truncated)?;
Ok(LegacyRecordVersion::from_bytes(*bytes))
}
fn read_u16(cursor: &mut ReadCursor<'_>) -> Result<usize, RecordError> {
let bytes = cursor
.take_array::<2>()
.map_err(|_| RecordError::Truncated)?;
Ok(usize::from(u16::from_be_bytes(*bytes)))
}
fn take<'input>(
cursor: &mut ReadCursor<'input>,
length: usize,
) -> Result<&'input [u8], RecordError> {
cursor.take(length).map_err(|_| RecordError::Truncated)
}
fn validate_plaintext_length(content_type: ContentType, length: usize) -> Result<(), RecordError> {
if length > MAX_PLAINTEXT_LENGTH {
return Err(RecordError::RecordOverflow);
}
if length == 0 && !matches!(content_type, ContentType::ApplicationData) {
return Err(RecordError::EmptyFragment);
}
Ok(())
}
fn validate_ciphertext_version(
policy: WirePolicy,
version: LegacyRecordVersion,
) -> Result<(), RecordError> {
if matches!(policy.version(), ProtocolVersion::Tls13) && version.bytes() != [3, 3] {
Err(RecordError::InvalidCiphertextVersion)
} else {
Ok(())
}
}
fn validate_ciphertext_length(policy: WirePolicy, length: usize) -> Result<(), RecordError> {
let maximum = if matches!(policy.version(), ProtocolVersion::Tls13) {
MAX_TLS13_CIPHERTEXT_LENGTH
} else {
MAX_TLS12_CIPHERTEXT_LENGTH
};
if length > maximum {
Err(RecordError::RecordOverflow)
} else if length == 0 && matches!(policy.version(), ProtocolVersion::Tls13) {
Err(RecordError::EmptyFragment)
} else {
Ok(())
}
}
fn encode_record(
content_type: ContentType,
version: LegacyRecordVersion,
fragment: &[u8],
output: &mut [u8],
) -> Result<usize, RecordError> {
let total = HEADER_LENGTH
.checked_add(fragment.len())
.ok_or(RecordError::LengthOverflow)?;
if output.len() < total {
return Err(RecordError::InsufficientOutput);
}
let length = u16::try_from(fragment.len()).map_err(|_| RecordError::RecordOverflow)?;
let type_bytes = [content_type.code()];
let version_bytes = version.bytes();
let length_bytes = length.to_be_bytes();
let mut cursor = WriteCursor::new(output);
cursor
.write_parts(&[&type_bytes, &version_bytes, &length_bytes, fragment])
.map_err(|_| RecordError::InsufficientOutput)?;
Ok(total)
}