Skip to main content

revault_vault_api/
agent_protocol.rs

1use revault_lockbox_api::{LockboxId, SecretVec};
2use std::io;
3
4const MAGIC: &[u8; 4] = b"LBX1";
5const HEADER_LEN: usize = 9;
6const MAX_MESSAGE_BYTES: usize = 128 * 1024;
7
8const REQ_GET: u8 = 0x01;
9const REQ_PUT: u8 = 0x02;
10const REQ_FORGET: u8 = 0x03;
11const REQ_FORGET_ALL: u8 = 0x04;
12const REQ_STOP: u8 = 0x05;
13const REQ_LIST: u8 = 0x06;
14const REQ_INFO: u8 = 0x07;
15const REQ_REGISTER_SECRET_ACTIVITY: u8 = 0x10;
16const REQ_UNREGISTER_SECRET_ACTIVITY: u8 = 0x11;
17
18const RESP_REGISTERED: u8 = 0x80;
19const RESP_OK: u8 = 0x81;
20const RESP_MISS: u8 = 0x82;
21const RESP_KEY: u8 = 0x83;
22const RESP_LIST: u8 = 0x84;
23const RESP_INFO: u8 = 0x85;
24const RESP_ERR: u8 = 0xff;
25
26pub(crate) const DEFAULT_TTL_SECONDS: u64 = 15 * 60;
27pub(crate) const AGENT_PROTOCOL_VERSION: u32 = 1;
28pub(crate) const AGENT_IMPLEMENTATION_VERSION: &str = env!("CARGO_PKG_VERSION");
29
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct CachedLockbox {
32    pub id: String,
33    pub path: Option<String>,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum SecretActivityKind {
38    Open,
39    Close,
40    Variables,
41    Form,
42    Recovery,
43    Vault,
44}
45
46impl SecretActivityKind {
47    pub fn as_str(self) -> &'static str {
48        match self {
49            Self::Open => "open",
50            Self::Close => "close",
51            Self::Variables => "variables",
52            Self::Form => "form",
53            Self::Recovery => "recovery",
54            Self::Vault => "vault",
55        }
56    }
57
58    fn to_wire(self) -> u8 {
59        match self {
60            Self::Open => 1,
61            Self::Close => 2,
62            Self::Variables => 3,
63            Self::Form => 4,
64            Self::Recovery => 5,
65            Self::Vault => 6,
66        }
67    }
68}
69
70pub(crate) enum AgentRequest {
71    Get(String),
72    Put(String, SecretVec, Option<String>, Option<u64>),
73    Forget(String),
74    ForgetAll,
75    Stop,
76    List,
77    Info,
78}
79
80pub(crate) enum AgentResponse {
81    Ok,
82    Miss,
83    Key(SecretVec),
84    List(Vec<CachedLockbox>),
85    Info(u32, String),
86    Err(String),
87}
88
89pub(crate) enum ControlRequest {
90    RegisterSecretActivity(u32, SecretActivityKind),
91    UnregisterSecretActivity(u32, u64),
92}
93
94pub(crate) enum ControlResponse {
95    Ok,
96    Registered(u64),
97    Err(String),
98}
99
100#[allow(dead_code)]
101pub(crate) fn encode_get(lockbox_id: LockboxId) -> io::Result<SecretVec> {
102    encode_get_identifier(&lockbox_id.to_string())
103}
104
105pub(crate) fn encode_get_identifier(identifier: &str) -> io::Result<SecretVec> {
106    encode_frame(REQ_GET, identifier.as_bytes())
107}
108
109#[allow(dead_code)]
110pub(crate) fn encode_put(
111    lockbox_id: LockboxId,
112    key: &SecretVec,
113    path: Option<&str>,
114    ttl_seconds: Option<u64>,
115) -> io::Result<SecretVec> {
116    let lockbox_id = lockbox_id.to_string();
117    encode_put_identifier(&lockbox_id, key, path, ttl_seconds)
118}
119
120pub(crate) fn encode_put_identifier(
121    identifier: &str,
122    key: &SecretVec,
123    path: Option<&str>,
124    ttl_seconds: Option<u64>,
125) -> io::Result<SecretVec> {
126    let path = path.unwrap_or("");
127    let ttl_seconds = ttl_seconds.unwrap_or(DEFAULT_TTL_SECONDS);
128    let mut payload = SecretVec::new();
129    push_string(&mut payload, identifier)?;
130    push_u32(&mut payload, key.len() as u32)?;
131    push_u32(&mut payload, path.len() as u32)?;
132    push_u64(&mut payload, ttl_seconds)?;
133    payload
134        .try_extend_from_secure(key)
135        .map_err(io::Error::other)?;
136    payload
137        .try_extend_from_slice(path.as_bytes())
138        .map_err(io::Error::other)?;
139    encode_frame_secure(REQ_PUT, &payload)
140}
141
142#[allow(dead_code)]
143pub(crate) fn encode_forget(lockbox_id: LockboxId) -> io::Result<SecretVec> {
144    encode_forget_identifier(&lockbox_id.to_string())
145}
146
147pub(crate) fn encode_forget_identifier(identifier: &str) -> io::Result<SecretVec> {
148    encode_frame(REQ_FORGET, identifier.as_bytes())
149}
150
151pub(crate) fn encode_forget_all() -> io::Result<SecretVec> {
152    encode_frame(REQ_FORGET_ALL, &[])
153}
154
155pub(crate) fn encode_stop() -> io::Result<SecretVec> {
156    encode_frame(REQ_STOP, &[])
157}
158
159pub(crate) fn encode_list() -> io::Result<SecretVec> {
160    encode_frame(REQ_LIST, &[])
161}
162
163pub(crate) fn encode_info() -> io::Result<SecretVec> {
164    encode_frame(REQ_INFO, &[])
165}
166
167pub(crate) fn encode_register_secret_activity(
168    pid: u32,
169    kind: SecretActivityKind,
170) -> io::Result<Vec<u8>> {
171    let mut payload = Vec::with_capacity(5);
172    payload.extend_from_slice(&pid.to_le_bytes());
173    payload.push(kind.to_wire());
174    encode_plain_frame(REQ_REGISTER_SECRET_ACTIVITY, &payload)
175}
176
177pub(crate) fn encode_unregister_secret_activity(pid: u32, token: u64) -> io::Result<Vec<u8>> {
178    let mut payload = Vec::with_capacity(12);
179    payload.extend_from_slice(&pid.to_le_bytes());
180    payload.extend_from_slice(&token.to_le_bytes());
181    encode_plain_frame(REQ_UNREGISTER_SECRET_ACTIVITY, &payload)
182}
183
184pub(crate) fn encode_key_response(key: &SecretVec) -> io::Result<SecretVec> {
185    encode_frame_secure(RESP_KEY, key)
186}
187
188pub(crate) fn encode_ok_response() -> io::Result<SecretVec> {
189    encode_frame(RESP_OK, &[])
190}
191
192pub(crate) fn encode_miss_response() -> io::Result<SecretVec> {
193    encode_frame(RESP_MISS, &[])
194}
195
196pub(crate) fn encode_err_response(message: &str) -> io::Result<SecretVec> {
197    encode_frame(RESP_ERR, message.as_bytes())
198}
199
200pub(crate) fn encode_control_ok_response() -> io::Result<Vec<u8>> {
201    encode_plain_frame(RESP_OK, &[])
202}
203
204pub(crate) fn encode_control_err_response(message: &str) -> io::Result<Vec<u8>> {
205    encode_plain_frame(RESP_ERR, message.as_bytes())
206}
207
208pub(crate) fn encode_registered_response(token: u64) -> io::Result<Vec<u8>> {
209    encode_plain_frame(RESP_REGISTERED, &token.to_le_bytes())
210}
211
212pub(crate) fn encode_list_response(
213    lockboxes: impl Iterator<Item = CachedLockbox>,
214) -> io::Result<SecretVec> {
215    let lockboxes = lockboxes.collect::<Vec<_>>();
216    let mut payload = SecretVec::new();
217    push_u32(&mut payload, lockboxes.len() as u32)?;
218    for lockbox in lockboxes {
219        push_string(&mut payload, &lockbox.id)?;
220        match lockbox.path {
221            Some(path) => push_bytes_u32(&mut payload, path.as_bytes())?,
222            None => push_u32(&mut payload, 0)?,
223        }
224    }
225    encode_frame_secure(RESP_LIST, &payload)
226}
227
228pub(crate) fn encode_info_response() -> io::Result<SecretVec> {
229    let mut payload = SecretVec::new();
230    push_u32(&mut payload, AGENT_PROTOCOL_VERSION)?;
231    push_string(&mut payload, AGENT_IMPLEMENTATION_VERSION)?;
232    encode_frame_secure(RESP_INFO, &payload)
233}
234
235pub(crate) fn parse_request(request: &SecretVec) -> io::Result<AgentRequest> {
236    let parsed = request
237        .with_bytes(parse_request_bytes)
238        .map_err(io::Error::other)??;
239    match parsed {
240        ParsedRequest::Ready(request) => Ok(request),
241        ParsedRequest::Put {
242            lockbox_id,
243            key_offset,
244            key_len,
245            path,
246            ttl_seconds,
247        } => {
248            let key = request
249                .try_clone_range(key_offset, key_len)
250                .map_err(io::Error::other)?;
251            Ok(AgentRequest::Put(lockbox_id, key, path, Some(ttl_seconds)))
252        }
253    }
254}
255
256enum ParsedRequest {
257    Ready(AgentRequest),
258    Put {
259        lockbox_id: String,
260        key_offset: usize,
261        key_len: usize,
262        path: Option<String>,
263        ttl_seconds: u64,
264    },
265}
266
267fn parse_request_bytes(bytes: &[u8]) -> io::Result<ParsedRequest> {
268    let frame = parse_frame(bytes)?;
269    match frame.message_type {
270        REQ_GET => Ok(ParsedRequest::Ready(AgentRequest::Get(
271            read_utf8(frame.payload)?.to_string(),
272        ))),
273        REQ_PUT => parse_put_request(frame.payload),
274        REQ_FORGET => Ok(ParsedRequest::Ready(AgentRequest::Forget(
275            read_utf8(frame.payload)?.to_string(),
276        ))),
277        REQ_FORGET_ALL if frame.payload.is_empty() => {
278            Ok(ParsedRequest::Ready(AgentRequest::ForgetAll))
279        }
280        REQ_STOP if frame.payload.is_empty() => Ok(ParsedRequest::Ready(AgentRequest::Stop)),
281        REQ_LIST if frame.payload.is_empty() => Ok(ParsedRequest::Ready(AgentRequest::List)),
282        REQ_INFO if frame.payload.is_empty() => Ok(ParsedRequest::Ready(AgentRequest::Info)),
283        _ => invalid_data("invalid binary agent request"),
284    }
285}
286
287pub(crate) fn parse_response(response: SecretVec) -> io::Result<AgentResponse> {
288    let parsed = response
289        .with_bytes(parse_response_bytes)
290        .map_err(io::Error::other)??;
291    match parsed {
292        ParsedResponse::Ready(response) => Ok(response),
293        ParsedResponse::Key { offset, len } => {
294            let key = response
295                .try_clone_range(offset, len)
296                .map_err(io::Error::other)?;
297            Ok(AgentResponse::Key(key))
298        }
299    }
300}
301
302enum ParsedResponse {
303    Ready(AgentResponse),
304    Key { offset: usize, len: usize },
305}
306
307fn parse_response_bytes(bytes: &[u8]) -> io::Result<ParsedResponse> {
308    let frame = parse_frame(bytes)?;
309    match frame.message_type {
310        RESP_OK if frame.payload.is_empty() => Ok(ParsedResponse::Ready(AgentResponse::Ok)),
311        RESP_MISS if frame.payload.is_empty() => Ok(ParsedResponse::Ready(AgentResponse::Miss)),
312        RESP_KEY => Ok(ParsedResponse::Key {
313            offset: HEADER_LEN,
314            len: frame.payload.len(),
315        }),
316        RESP_LIST => parse_list_response(frame.payload),
317        RESP_INFO => parse_info_response(frame.payload),
318        RESP_ERR => Ok(ParsedResponse::Ready(AgentResponse::Err(
319            read_utf8(frame.payload)?.to_string(),
320        ))),
321        _ => invalid_data("invalid binary agent response"),
322    }
323}
324
325pub(crate) fn parse_control_request(request: &[u8]) -> io::Result<ControlRequest> {
326    let frame = parse_frame(request)?;
327    match frame.message_type {
328        REQ_REGISTER_SECRET_ACTIVITY => parse_register_secret_activity(frame.payload),
329        REQ_UNREGISTER_SECRET_ACTIVITY => parse_unregister_secret_activity(frame.payload),
330        _ => invalid_data("invalid binary agent control request"),
331    }
332}
333
334pub(crate) fn parse_control_response(response: &[u8]) -> io::Result<ControlResponse> {
335    let frame = parse_frame(response)?;
336    match frame.message_type {
337        RESP_OK if frame.payload.is_empty() => Ok(ControlResponse::Ok),
338        RESP_REGISTERED if frame.payload.len() == 8 => {
339            Ok(ControlResponse::Registered(read_u64(frame.payload)?))
340        }
341        RESP_ERR => Ok(ControlResponse::Err(read_utf8(frame.payload)?.to_string())),
342        _ => invalid_data("invalid binary agent control response"),
343    }
344}
345
346pub(crate) fn max_message_bytes() -> usize {
347    MAX_MESSAGE_BYTES
348}
349
350pub(crate) fn frame_header_len() -> usize {
351    HEADER_LEN
352}
353
354pub(crate) fn frame_payload_len(header: &[u8]) -> io::Result<usize> {
355    if header.len() != HEADER_LEN || &header[..4] != MAGIC {
356        return invalid_data("invalid binary agent frame header");
357    }
358    let len = read_u32_raw(&header[5..9])? as usize;
359    let total = HEADER_LEN
360        .checked_add(len)
361        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "message too large"))?;
362    if total > MAX_MESSAGE_BYTES {
363        return invalid_data("message too large");
364    }
365    Ok(len)
366}
367
368pub(crate) fn frame_message_type(header: &[u8]) -> io::Result<u8> {
369    if header.len() != HEADER_LEN || &header[..4] != MAGIC {
370        return invalid_data("invalid binary agent frame header");
371    }
372    Ok(header[4])
373}
374
375pub(crate) fn is_control_message_type(message_type: u8) -> bool {
376    matches!(
377        message_type,
378        REQ_REGISTER_SECRET_ACTIVITY | REQ_UNREGISTER_SECRET_ACTIVITY
379    )
380}
381
382fn encode_frame(message_type: u8, payload: &[u8]) -> io::Result<SecretVec> {
383    let payload = SecretVec::try_from_slice(payload).map_err(io::Error::other)?;
384    encode_frame_secure(message_type, &payload)
385}
386
387fn encode_plain_frame(message_type: u8, payload: &[u8]) -> io::Result<Vec<u8>> {
388    let total = HEADER_LEN
389        .checked_add(payload.len())
390        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "message too large"))?;
391    if total > MAX_MESSAGE_BYTES {
392        return invalid_data("message too large");
393    }
394    let mut message = Vec::with_capacity(total);
395    message.extend_from_slice(MAGIC);
396    message.push(message_type);
397    message.extend_from_slice(&(payload.len() as u32).to_le_bytes());
398    message.extend_from_slice(payload);
399    Ok(message)
400}
401
402fn encode_frame_secure(message_type: u8, payload: &SecretVec) -> io::Result<SecretVec> {
403    let total = HEADER_LEN
404        .checked_add(payload.len())
405        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "message too large"))?;
406    if total > MAX_MESSAGE_BYTES {
407        return invalid_data("message too large");
408    }
409    let mut message = SecretVec::new();
410    message
411        .try_extend_from_slice(MAGIC)
412        .map_err(io::Error::other)?;
413    message.try_push(message_type).map_err(io::Error::other)?;
414    push_u32(&mut message, payload.len() as u32)?;
415    if !payload.is_empty() {
416        message
417            .try_extend_from_secure(payload)
418            .map_err(io::Error::other)?;
419    }
420    Ok(message)
421}
422
423struct Frame<'a> {
424    message_type: u8,
425    payload: &'a [u8],
426}
427
428fn parse_frame(bytes: &[u8]) -> io::Result<Frame<'_>> {
429    if bytes.len() > MAX_MESSAGE_BYTES {
430        return invalid_data("agent message too large");
431    }
432    if bytes.len() < HEADER_LEN || &bytes[..4] != MAGIC {
433        return invalid_data("invalid binary agent frame");
434    }
435    let payload_len = read_u32_raw(&bytes[5..9])? as usize;
436    if HEADER_LEN + payload_len != bytes.len() {
437        return invalid_data("binary agent frame length mismatch");
438    }
439    Ok(Frame {
440        message_type: bytes[4],
441        payload: &bytes[HEADER_LEN..],
442    })
443}
444
445fn parse_put_request(payload: &[u8]) -> io::Result<ParsedRequest> {
446    let mut cursor = Cursor::new(payload);
447    let lockbox_id = cursor.read_string()?.to_string();
448    let key_len = cursor.read_u32()? as usize;
449    let path_len = cursor.read_u32()? as usize;
450    let ttl_seconds = cursor.read_u64()?;
451    if ttl_seconds == 0 {
452        return invalid_data("ttl must be positive");
453    }
454    let key_offset = cursor.position();
455    let path_offset = key_offset
456        .checked_add(key_len)
457        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "key length overflow"))?;
458    let end = path_offset
459        .checked_add(path_len)
460        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "path length overflow"))?;
461    if end != payload.len() {
462        return invalid_data("put payload length mismatch");
463    }
464    let path = if path_len == 0 {
465        None
466    } else {
467        Some(read_utf8(&payload[path_offset..end])?.to_string())
468    };
469    Ok(ParsedRequest::Put {
470        lockbox_id,
471        key_offset: HEADER_LEN + key_offset,
472        key_len,
473        path,
474        ttl_seconds,
475    })
476}
477
478fn parse_register_secret_activity(payload: &[u8]) -> io::Result<ControlRequest> {
479    if payload.len() != 5 {
480        return invalid_data("invalid secret activity registration length");
481    }
482    Ok(ControlRequest::RegisterSecretActivity(
483        validate_pid(read_u32_raw(&payload[..4])?)?,
484        kind_from_wire(payload[4])?,
485    ))
486}
487
488fn parse_unregister_secret_activity(payload: &[u8]) -> io::Result<ControlRequest> {
489    if payload.len() != 12 {
490        return invalid_data("invalid secret activity unregister length");
491    }
492    Ok(ControlRequest::UnregisterSecretActivity(
493        validate_pid(read_u32_raw(&payload[..4])?)?,
494        read_u64(&payload[4..12])?,
495    ))
496}
497
498fn parse_list_response(payload: &[u8]) -> io::Result<ParsedResponse> {
499    let mut cursor = Cursor::new(payload);
500    let count = cursor.read_u32()? as usize;
501    let mut lockboxes = Vec::with_capacity(count);
502    for _ in 0..count {
503        let id = cursor.read_string()?.to_string();
504        let path = cursor.read_bytes_u32()?;
505        let path = if path.is_empty() {
506            None
507        } else {
508            Some(read_utf8(path)?.to_string())
509        };
510        lockboxes.push(CachedLockbox { id, path });
511    }
512    if !cursor.is_finished() {
513        return invalid_data("list response has trailing bytes");
514    }
515    Ok(ParsedResponse::Ready(AgentResponse::List(lockboxes)))
516}
517
518fn parse_info_response(payload: &[u8]) -> io::Result<ParsedResponse> {
519    let mut cursor = Cursor::new(payload);
520    let protocol = cursor.read_u32()?;
521    let implementation = cursor.read_string()?.to_string();
522    if !cursor.is_finished() {
523        return invalid_data("agent info response has trailing bytes");
524    }
525    Ok(ParsedResponse::Ready(AgentResponse::Info(
526        protocol,
527        implementation,
528    )))
529}
530
531struct Cursor<'a> {
532    bytes: &'a [u8],
533    position: usize,
534}
535
536impl<'a> Cursor<'a> {
537    fn new(bytes: &'a [u8]) -> Self {
538        Self { bytes, position: 0 }
539    }
540
541    fn position(&self) -> usize {
542        self.position
543    }
544
545    fn is_finished(&self) -> bool {
546        self.position == self.bytes.len()
547    }
548
549    fn read_u16(&mut self) -> io::Result<u16> {
550        let bytes = self.read_exact(2)?;
551        Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
552    }
553
554    fn read_u32(&mut self) -> io::Result<u32> {
555        read_u32_raw(self.read_exact(4)?)
556    }
557
558    fn read_u64(&mut self) -> io::Result<u64> {
559        read_u64(self.read_exact(8)?)
560    }
561
562    fn read_string(&mut self) -> io::Result<&'a str> {
563        let len = self.read_u16()? as usize;
564        read_utf8(self.read_exact(len)?)
565    }
566
567    fn read_bytes_u32(&mut self) -> io::Result<&'a [u8]> {
568        let len = self.read_u32()? as usize;
569        self.read_exact(len)
570    }
571
572    fn read_exact(&mut self, len: usize) -> io::Result<&'a [u8]> {
573        let end = self
574            .position
575            .checked_add(len)
576            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "payload length overflow"))?;
577        let bytes = self
578            .bytes
579            .get(self.position..end)
580            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "payload too short"))?;
581        self.position = end;
582        Ok(bytes)
583    }
584}
585
586fn push_string(out: &mut SecretVec, value: &str) -> io::Result<()> {
587    if value.len() > u16::MAX as usize {
588        return invalid_data("string too long");
589    }
590    out.try_extend_from_slice(&(value.len() as u16).to_le_bytes())
591        .map_err(io::Error::other)?;
592    out.try_extend_from_slice(value.as_bytes())
593        .map_err(io::Error::other)
594}
595
596fn push_bytes_u32(out: &mut SecretVec, value: &[u8]) -> io::Result<()> {
597    push_u32(out, value.len() as u32)?;
598    out.try_extend_from_slice(value).map_err(io::Error::other)
599}
600
601fn push_u32(out: &mut SecretVec, value: u32) -> io::Result<()> {
602    out.try_extend_from_slice(&value.to_le_bytes())
603        .map_err(io::Error::other)
604}
605
606fn push_u64(out: &mut SecretVec, value: u64) -> io::Result<()> {
607    out.try_extend_from_slice(&value.to_le_bytes())
608        .map_err(io::Error::other)
609}
610
611fn read_utf8(bytes: &[u8]) -> io::Result<&str> {
612    std::str::from_utf8(bytes)
613        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "payload is not UTF-8"))
614}
615
616fn read_u32_raw(bytes: &[u8]) -> io::Result<u32> {
617    let bytes: [u8; 4] = bytes
618        .try_into()
619        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid u32 length"))?;
620    Ok(u32::from_le_bytes(bytes))
621}
622
623fn read_u64(bytes: &[u8]) -> io::Result<u64> {
624    let bytes: [u8; 8] = bytes
625        .try_into()
626        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid u64 length"))?;
627    Ok(u64::from_le_bytes(bytes))
628}
629
630fn validate_pid(pid: u32) -> io::Result<u32> {
631    if pid == 0 {
632        return invalid_data("pid must be positive");
633    }
634    Ok(pid)
635}
636
637fn kind_from_wire(value: u8) -> io::Result<SecretActivityKind> {
638    match value {
639        1 => Ok(SecretActivityKind::Open),
640        2 => Ok(SecretActivityKind::Close),
641        3 => Ok(SecretActivityKind::Variables),
642        4 => Ok(SecretActivityKind::Form),
643        5 => Ok(SecretActivityKind::Recovery),
644        6 => Ok(SecretActivityKind::Vault),
645        _ => invalid_data("invalid secret activity kind"),
646    }
647}
648
649fn invalid_data<T>(message: &str) -> io::Result<T> {
650    Err(io::Error::new(io::ErrorKind::InvalidData, message))
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    #[test]
658    fn protocol_rejects_line_based_and_oversized_requests() {
659        let request = SecretVec::try_from_slice(b"GET vault\n").unwrap();
660        assert!(parse_request(&request).is_err());
661        let request = SecretVec::try_from_slice(&vec![b'x'; MAX_MESSAGE_BYTES + 1]).unwrap();
662        assert!(parse_request(&request).is_err());
663    }
664
665    #[test]
666    fn protocol_parses_put_and_cache_commands() {
667        let lockbox_id = LockboxId::from_bytes([1; 16]);
668        let key = SecretVec::try_from_slice(b"abc").unwrap();
669        let request = encode_put(lockbox_id, &key, Some("/tmp/a.lbox"), Some(30)).unwrap();
670        request
671            .with_bytes(|request| {
672                assert!(!request
673                    .windows(b"616263".len())
674                    .any(|window| window == b"616263"));
675            })
676            .unwrap();
677        match parse_request(&request).unwrap() {
678            AgentRequest::Put(id, parsed_key, path, ttl_seconds) => {
679                assert_eq!(id, lockbox_id.to_string());
680                parsed_key
681                    .with_bytes(|key| assert_eq!(key, b"abc"))
682                    .unwrap();
683                assert_eq!(path.as_deref(), Some("/tmp/a.lbox"));
684                assert_eq!(ttl_seconds, Some(30));
685            }
686            _ => panic!("expected PUT"),
687        }
688
689        assert!(matches!(
690            parse_request(&encode_forget_all().unwrap()).unwrap(),
691            AgentRequest::ForgetAll
692        ));
693        assert!(matches!(
694            parse_request(&encode_stop().unwrap()).unwrap(),
695            AgentRequest::Stop
696        ));
697        assert!(matches!(
698            parse_request(&encode_list().unwrap()).unwrap(),
699            AgentRequest::List
700        ));
701        assert!(matches!(
702            parse_request(&encode_info().unwrap()).unwrap(),
703            AgentRequest::Info
704        ));
705    }
706
707    #[test]
708    fn protocol_parses_list_and_activity_responses() {
709        let response = encode_list_response(
710            [
711                CachedLockbox {
712                    id: "a".to_string(),
713                    path: Some("/tmp/a.lbox".to_string()),
714                },
715                CachedLockbox {
716                    id: "b".to_string(),
717                    path: None,
718                },
719            ]
720            .into_iter(),
721        )
722        .unwrap();
723        match parse_response(response).unwrap() {
724            AgentResponse::List(ids) => {
725                assert_eq!(ids[0].id, "a");
726                assert_eq!(ids[0].path.as_deref(), Some("/tmp/a.lbox"));
727                assert_eq!(ids[1].id, "b");
728                assert_eq!(ids[1].path, None);
729            }
730            _ => panic!("expected LIST"),
731        }
732
733        let request = encode_register_secret_activity(42, SecretActivityKind::Open).unwrap();
734        match parse_control_request(&request).unwrap() {
735            ControlRequest::RegisterSecretActivity(42, SecretActivityKind::Open) => {}
736            _ => panic!("expected activity registration"),
737        }
738        let response = encode_registered_response(123).unwrap();
739        assert!(matches!(
740            parse_control_response(&response).unwrap(),
741            ControlResponse::Registered(123)
742        ));
743    }
744
745    #[test]
746    fn protocol_reports_agent_compatibility() {
747        match parse_response(encode_info_response().unwrap()).unwrap() {
748            AgentResponse::Info(protocol, implementation) => {
749                assert_eq!(protocol, AGENT_PROTOCOL_VERSION);
750                assert_eq!(implementation, AGENT_IMPLEMENTATION_VERSION);
751            }
752            _ => panic!("expected agent info response"),
753        }
754    }
755}