use crate::control::{ChallengeResponse, TransportResponse};
use crate::error::ProtocolError;
pub const TAG_LEN: usize = 1;
pub const TAG_APP: u8 = 0x09;
const CONTROL_TAGS: [u8; 8] = [
TAG_RESPONSE_HEARTBEAT,
TAG_BATCH_END,
TAG_ENGINE_ERROR,
TAG_SERVER_BUSY,
TAG_CHALLENGE,
TAG_CHALLENGE_RESPONSE,
TAG_AUTH_FAILED,
TAG_SERVER_READY,
];
const _: () = {
assert!(
TAG_APP != 0x00 && TAG_APP < 0x10,
"TAG_APP must be neither a zeroed byte nor an old application tag"
);
let mut i = 0;
while i < CONTROL_TAGS.len() {
assert!(
CONTROL_TAGS[i] != TAG_APP,
"TAG_APP must be no control frame's tag"
);
i += 1;
}
};
pub const TAG_RESPONSE_HEARTBEAT: u8 = 0x01;
pub const TAG_BATCH_END: u8 = 0x02;
pub const TAG_ENGINE_ERROR: u8 = 0x03;
pub const TAG_SERVER_BUSY: u8 = 0x04;
pub const TAG_CHALLENGE: u8 = 0x05;
pub const TAG_CHALLENGE_RESPONSE: u8 = 0x06;
pub const TAG_AUTH_FAILED: u8 = 0x07;
pub const TAG_SERVER_READY: u8 = 0x08;
pub fn encode_transport_response(
response: &TransportResponse,
buf: &mut [u8],
) -> Result<usize, ProtocolError> {
let needed = match response {
TransportResponse::Challenge { .. } => 4 + TAG_LEN + 32,
_ => 4 + TAG_LEN,
};
if buf.len() < needed {
return Err(ProtocolError::Truncated);
}
let mut pos = 4;
match response {
TransportResponse::Heartbeat => {
buf[pos] = TAG_RESPONSE_HEARTBEAT;
pos += 1;
}
TransportResponse::BatchEnd => {
buf[pos] = TAG_BATCH_END;
pos += 1;
}
TransportResponse::EngineError => {
buf[pos] = TAG_ENGINE_ERROR;
pos += 1;
}
TransportResponse::ServerBusy => {
buf[pos] = TAG_SERVER_BUSY;
pos += 1;
}
TransportResponse::Challenge { nonce } => {
buf[pos] = TAG_CHALLENGE;
pos += 1;
buf[pos..pos + 32].copy_from_slice(nonce);
pos += 32;
}
TransportResponse::AuthFailed => {
buf[pos] = TAG_AUTH_FAILED;
pos += 1;
}
TransportResponse::ServerReady => {
buf[pos] = TAG_SERVER_READY;
pos += 1;
}
}
let payload_len = (pos - 4) as u32;
buf[..4].copy_from_slice(&payload_len.to_le_bytes());
Ok(pos)
}
pub const CHALLENGE_RESPONSE_LEN: usize = TAG_LEN + 64 + 32;
pub fn encode_challenge_response(
response: &ChallengeResponse,
buf: &mut [u8],
) -> Result<usize, ProtocolError> {
if buf.len() < CHALLENGE_RESPONSE_LEN {
return Err(ProtocolError::Truncated);
}
buf[0] = TAG_CHALLENGE_RESPONSE;
buf[1..65].copy_from_slice(&response.signature);
buf[65..97].copy_from_slice(&response.public_key);
Ok(CHALLENGE_RESPONSE_LEN)
}
pub fn decode_challenge_response(buf: &[u8]) -> Result<ChallengeResponse, ProtocolError> {
if buf.len() < CHALLENGE_RESPONSE_LEN {
return Err(ProtocolError::Truncated);
}
let tag = buf[0];
if tag != TAG_CHALLENGE_RESPONSE {
return Err(ProtocolError::UnknownTag(tag));
}
let mut signature = [0u8; 64];
signature.copy_from_slice(&buf[1..65]);
let mut public_key = [0u8; 32];
public_key.copy_from_slice(&buf[65..97]);
Ok(ChallengeResponse {
signature,
public_key,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_tag_only_variants() {
let variants = [
TransportResponse::Heartbeat,
TransportResponse::BatchEnd,
TransportResponse::EngineError,
TransportResponse::ServerBusy,
TransportResponse::AuthFailed,
TransportResponse::ServerReady,
];
for variant in &variants {
let mut buf = [0u8; 8];
let written = encode_transport_response(variant, &mut buf).unwrap();
assert_eq!(written, 5, "variant {variant:?}");
assert_eq!(u32::from_le_bytes(buf[..4].try_into().unwrap()), 1);
}
}
#[test]
fn the_app_tag_is_no_control_frame() {
let variants = [
TransportResponse::Heartbeat,
TransportResponse::BatchEnd,
TransportResponse::EngineError,
TransportResponse::ServerBusy,
TransportResponse::Challenge { nonce: [0; 32] },
TransportResponse::AuthFailed,
TransportResponse::ServerReady,
];
for variant in &variants {
let mut buf = [0u8; 64];
encode_transport_response(variant, &mut buf).unwrap();
assert_ne!(buf[4], TAG_APP, "variant {variant:?}");
assert!(CONTROL_TAGS.contains(&buf[4]), "variant {variant:?}");
}
}
#[test]
fn encode_truncated_tag_only() {
let mut buf = [0u8; 4]; assert!(matches!(
encode_transport_response(&TransportResponse::Heartbeat, &mut buf),
Err(ProtocolError::Truncated)
));
}
#[test]
fn encode_truncated_challenge() {
let mut buf = [0u8; 36]; assert!(matches!(
encode_transport_response(&TransportResponse::Challenge { nonce: [0; 32] }, &mut buf),
Err(ProtocolError::Truncated)
));
}
#[test]
fn round_trip_challenge() {
let nonce = [0xAB; 32];
let mut buf = [0u8; 64];
let written =
encode_transport_response(&TransportResponse::Challenge { nonce }, &mut buf).unwrap();
assert_eq!(written, 37);
assert_eq!(
u32::from_le_bytes(buf[..4].try_into().unwrap()),
33 );
assert_eq!(buf[4], TAG_CHALLENGE);
assert_eq!(&buf[5..37], &nonce);
}
#[test]
fn decode_valid_challenge_response() {
let sig = [0x11; 64];
let pubkey = [0x22; 32];
let mut buf = [0u8; 97];
buf[0] = TAG_CHALLENGE_RESPONSE;
buf[1..65].copy_from_slice(&sig);
buf[65..97].copy_from_slice(&pubkey);
let cr = decode_challenge_response(&buf).unwrap();
assert_eq!(cr.signature, sig);
assert_eq!(cr.public_key, pubkey);
}
#[test]
fn challenge_response_round_trips() {
let response = ChallengeResponse {
signature: [0x33; 64],
public_key: [0x44; 32],
};
let mut buf = [0u8; CHALLENGE_RESPONSE_LEN];
let written = encode_challenge_response(&response, &mut buf).unwrap();
assert_eq!(written, CHALLENGE_RESPONSE_LEN);
assert_eq!(decode_challenge_response(&buf).unwrap(), response);
let mut short = [0u8; CHALLENGE_RESPONSE_LEN - 1];
assert!(matches!(
encode_challenge_response(&response, &mut short),
Err(ProtocolError::Truncated)
));
}
#[test]
fn decode_truncated() {
let buf = [0u8; 50]; assert!(matches!(
decode_challenge_response(&buf),
Err(ProtocolError::Truncated)
));
}
#[test]
fn decode_wrong_tag() {
let mut buf = [0u8; CHALLENGE_RESPONSE_LEN];
buf[0] = 99; assert!(matches!(
decode_challenge_response(&buf),
Err(ProtocolError::UnknownTag(99))
));
}
}