pub const RECORD_HANDSHAKE: u8 = 0x16;
pub const RECORD_CHANGE_CIPHER_SPEC: u8 = 0x14;
pub const RECORD_APPLICATION_DATA: u8 = 0x17;
pub const RECORD_HEADER_LEN: usize = 5;
pub const RECORD_MAX_CHUNK: usize = 16384;
pub fn wrap_application_data(ciphertext: &[u8], out: &mut Vec<u8>) {
if ciphertext.is_empty() {
return;
}
for chunk in ciphertext.chunks(RECORD_MAX_CHUNK) {
out.push(RECORD_APPLICATION_DATA);
out.extend_from_slice(&[0x03, 0x03]);
out.extend_from_slice(&(chunk.len() as u16).to_be_bytes());
out.extend_from_slice(chunk);
}
}
pub fn change_cipher_spec_record() -> [u8; 6] {
[0x14, 0x03, 0x03, 0x00, 0x01, 0x01]
}
pub struct Unwrapped {
pub ciphertext: Vec<u8>,
pub consumed: usize,
}
pub fn unwrap_records(pending: &[u8]) -> Result<Unwrapped, String> {
let mut offset = 0usize;
let mut ciphertext = Vec::new();
loop {
if pending.len() < offset + RECORD_HEADER_LEN {
break;
}
let rec_type = pending[offset];
let len = u16::from_be_bytes([pending[offset + 3], pending[offset + 4]]) as usize;
let total = RECORD_HEADER_LEN + len;
if pending.len() < offset + total {
break;
}
match rec_type {
RECORD_APPLICATION_DATA | RECORD_CHANGE_CIPHER_SPEC => {
ciphertext.extend_from_slice(&pending[offset + RECORD_HEADER_LEN..offset + total]);
}
RECORD_HANDSHAKE => {
return Err(
"FakeTLS: unexpected Handshake record after handshake completed".into(),
);
}
other => {
return Err(format!("FakeTLS: unexpected TLS record type 0x{other:02x}"));
}
}
offset += total;
}
Ok(Unwrapped {
ciphertext,
consumed: offset,
})
}
pub struct RawRecord {
pub rec_type: u8,
pub bytes: Vec<u8>,
}
pub async fn read_one_record(stream: &mut tokio::net::TcpStream) -> std::io::Result<RawRecord> {
use tokio::io::AsyncReadExt;
let mut hdr = [0u8; RECORD_HEADER_LEN];
stream.read_exact(&mut hdr).await?;
let len = u16::from_be_bytes([hdr[3], hdr[4]]) as usize;
let mut bytes = Vec::with_capacity(RECORD_HEADER_LEN + len);
bytes.extend_from_slice(&hdr);
let mut body = vec![0u8; len];
stream.read_exact(&mut body).await?;
bytes.extend_from_slice(&body);
Ok(RawRecord {
rec_type: hdr[0],
bytes,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_then_unwrap_roundtrip() {
let data = vec![7u8; 40000]; let mut wire = Vec::new();
wrap_application_data(&data, &mut wire);
let result = unwrap_records(&wire).expect("unwrap");
assert_eq!(result.consumed, wire.len());
assert_eq!(result.ciphertext, data);
}
#[test]
fn partial_record_left_unconsumed() {
let data = vec![1u8, 2, 3, 4, 5];
let mut wire = Vec::new();
wrap_application_data(&data, &mut wire);
wire.truncate(wire.len() - 1);
let result = unwrap_records(&wire).expect("unwrap");
assert_eq!(result.consumed, 0);
assert!(result.ciphertext.is_empty());
}
#[test]
fn change_cipher_spec_payload_is_folded_in() {
let mut wire = Vec::new();
wire.extend_from_slice(&change_cipher_spec_record());
wrap_application_data(&[9, 9, 9], &mut wire);
let result = unwrap_records(&wire).expect("unwrap");
assert_eq!(result.consumed, wire.len());
assert_eq!(result.ciphertext, vec![1, 9, 9, 9]);
}
}