pub fn encode(plaintext: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(plaintext.len() + 4);
#[allow(clippy::cast_possible_truncation)]
out.extend_from_slice(&(plaintext.len() as u32).to_be_bytes());
out.extend_from_slice(&encode_raw(plaintext));
out
}
pub fn encode_raw(plaintext: &[u8]) -> Vec<u8> {
let mut key: u8 = 171; let mut out = Vec::with_capacity(plaintext.len());
for &b in plaintext {
let c = b ^ key;
key = c; out.push(c);
}
out
}
pub fn decode(ciphertext: &[u8]) -> Vec<u8> {
let mut key: u8 = 171;
let mut out = Vec::with_capacity(ciphertext.len());
for &b in ciphertext {
out.push(b ^ key);
key = b; }
out
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use rstest::rstest;
#[test]
fn encode_raw_round_trips_with_decode() {
let plaintext = br#"{"system":{"get_sysinfo":{}}}"#;
assert_eq!(decode(&encode_raw(plaintext)), plaintext);
}
#[test]
fn encode_strips_prefix_before_decode() {
let plaintext = b"hello kasa";
let framed = encode(plaintext);
let len = u32::from_be_bytes(framed[..4].try_into().unwrap()) as usize;
assert_eq!(len, plaintext.len());
assert_eq!(decode(&framed[4..]), plaintext);
}
#[test]
fn tcp_frame_is_four_bytes_longer_than_raw() {
let plain = b"ping";
assert_eq!(encode(plain).len(), encode_raw(plain).len() + 4);
}
#[rstest]
#[case(&[][..], &[][..])]
#[case(b"1", &[0x9A])]
#[case(b"12", &[0x9A, 0xA8])]
#[case(b"{", &[0xD0])]
#[case(&[0x00], &[0xAB])]
fn encode_raw_known_output(#[case] input: &[u8], #[case] expected: &[u8]) {
assert_eq!(encode_raw(input), expected);
}
#[test]
fn tcp_frame_for_known_input_has_correct_prefix_and_body() {
let framed = encode(b"12");
assert_eq!(&framed[..4], &[0x00, 0x00, 0x00, 0x02]); assert_eq!(&framed[4..], &[0x9A, 0xA8]);
}
proptest! {
#[test]
fn encode_raw_decode_round_trips_for_any_input(data in proptest::collection::vec(any::<u8>(), 0..=512)) {
prop_assert_eq!(decode(&encode_raw(&data)), data);
}
#[test]
fn tcp_length_prefix_always_matches_plaintext_len(data in proptest::collection::vec(any::<u8>(), 0..=512)) {
let framed = encode(&data);
let prefix_len = u32::from_be_bytes(framed[..4].try_into().unwrap()) as usize;
prop_assert_eq!(prefix_len, data.len());
}
#[test]
fn decode_is_inverse_of_encode_raw(data in proptest::collection::vec(any::<u8>(), 0..=512)) {
let ciphertext = encode_raw(&data);
prop_assert_eq!(encode_raw(&decode(&ciphertext)), ciphertext);
}
}
}