1use std::borrow::Cow;
7use std::sync::Arc;
8
9use aes_gcm::{Aes256Gcm, KeyInit as AesKeyInit, Nonce as AesNonce, aead::Aead as AesAead};
10use base64::Engine;
11use base64::engine::general_purpose::STANDARD as BASE64;
12use chacha20poly1305::{ChaCha20Poly1305, Nonce};
13use getrandom::getrandom;
14use serde::de::Deserializer;
15use serde::ser::SerializeMap;
16use serde::{Deserialize, Serialize};
17use serde_json::{Value, json};
18use sha2::{Digest, Sha256};
19use thiserror::Error;
20use x25519_dalek::{PublicKey, StaticSecret};
21
22pub const ECDH_DOMAIN_TAG: &[u8] = b"wscall-ecdh-v1";
33
34pub const ECDH_KEY_LEN: usize = 32;
36
37pub struct EcdhKeypair {
42 secret: StaticSecret,
43 public: PublicKey,
44}
45
46impl EcdhKeypair {
47 pub fn generate() -> Result<Self, ProtocolError> {
49 let mut secret_bytes = [0u8; ECDH_KEY_LEN];
50 getrandom(&mut secret_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
51 let secret = StaticSecret::from(secret_bytes);
52 let public = PublicKey::from(&secret);
53 Ok(Self { secret, public })
54 }
55
56 pub fn public_bytes(&self) -> [u8; ECDH_KEY_LEN] {
58 self.public.to_bytes()
59 }
60
61 pub fn derive_session_key(&self, peer_public: &[u8; ECDH_KEY_LEN]) -> [u8; 32] {
66 let peer = PublicKey::from(*peer_public);
67 let shared = self.secret.diffie_hellman(&peer);
68 derive_session_key(&shared.to_bytes())
69 }
70}
71
72pub fn derive_session_key(shared_secret: &[u8; 32]) -> [u8; 32] {
77 let mut hasher = Sha256::new();
78 hasher.update(ECDH_DOMAIN_TAG);
79 hasher.update(shared_secret);
80 let result = hasher.finalize();
81 let mut key = [0u8; 32];
82 key.copy_from_slice(&result);
83 key
84}
85
86pub fn parse_peer_public(bytes: &[u8]) -> Result<[u8; ECDH_KEY_LEN], ProtocolError> {
90 if bytes.len() != ECDH_KEY_LEN {
91 return Err(ProtocolError::InvalidEcdhPublicKey {
92 expected: ECDH_KEY_LEN,
93 actual: bytes.len(),
94 });
95 }
96 let mut key = [0u8; ECDH_KEY_LEN];
97 key.copy_from_slice(bytes);
98 Ok(key)
99}
100
101const AES256_NONCE_LEN: usize = 12;
102const CHACHA20_NONCE_LEN: usize = 12;
103const MAX_FRAME_BYTES: usize = 10 * 1024 * 1024;
104const MAX_PAYLOAD_BYTES: usize = MAX_FRAME_BYTES - 6;
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[repr(u8)]
109pub enum MessageType {
110 Api = 0x00,
112 Event = 0x01,
114}
115
116impl TryFrom<u8> for MessageType {
117 type Error = ProtocolError;
118
119 fn try_from(value: u8) -> Result<Self, Self::Error> {
120 match value {
121 0x00 => Ok(Self::Api),
122 0x01 => Ok(Self::Event),
123 _ => Err(ProtocolError::UnknownMessageType(value)),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130#[repr(u8)]
131pub enum EncryptionKind {
132 None = 0x00,
134 ChaCha20 = 0x01,
136 Aes256 = 0x02,
138}
139
140impl TryFrom<u8> for EncryptionKind {
141 type Error = ProtocolError;
142
143 fn try_from(value: u8) -> Result<Self, Self::Error> {
144 match value {
145 0x00 => Ok(Self::None),
146 0x01 => Ok(Self::ChaCha20),
147 0x02 => Ok(Self::Aes256),
148 _ => Err(ProtocolError::UnknownEncryption(value)),
149 }
150 }
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct FileAttachment {
156 pub id: String,
158 pub name: String,
160 pub content_type: String,
162 pub encoding: String,
164 pub data: String,
166 pub size: usize,
168}
169
170impl FileAttachment {
171 pub fn inline_text(
173 id: impl Into<String>,
174 name: impl Into<String>,
175 content_type: impl Into<String>,
176 text: impl AsRef<str>,
177 ) -> Self {
178 Self::inline_bytes(id, name, content_type, text.as_ref().as_bytes().to_vec())
179 }
180
181 pub fn inline_bytes(
183 id: impl Into<String>,
184 name: impl Into<String>,
185 content_type: impl Into<String>,
186 bytes: Vec<u8>,
187 ) -> Self {
188 let size = bytes.len();
189 Self {
190 id: id.into(),
191 name: name.into(),
192 content_type: content_type.into(),
193 encoding: "base64".to_string(),
194 data: BASE64.encode(bytes),
195 size,
196 }
197 }
198
199 pub fn decode_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
201 BASE64
202 .decode(self.data.as_bytes())
203 .map_err(|source| ProtocolError::InvalidAttachmentEncoding(source.to_string()))
204 }
205
206 pub fn param_ref(id: impl Into<String>) -> Value {
208 json!({ "$file": id.into() })
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ErrorPayload {
215 pub code: String,
216 pub message: String,
217 pub status: u16,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub details: Option<Value>,
220}
221
222pub const K_API_REQUEST: u8 = 0;
226pub const K_EVENT_EMIT: u8 = 1;
227pub const K_API_RESPONSE: u8 = 2;
228pub const K_EVENT_ACK: u8 = 3;
229
230#[derive(Debug, Clone)]
242pub enum PacketBody {
243 ApiRequest {
245 request_id: u64,
246 route: String,
247 params: Value,
248 attachments: Vec<FileAttachment>,
249 metadata: Value,
250 },
251 ApiResponse {
253 request_id: u64,
254 ok: bool,
255 status: u16,
256 data: Value,
257 error: Option<ErrorPayload>,
258 metadata: Value,
259 },
260 EventEmit {
262 event_id: u64,
263 name: String,
264 data: Value,
265 attachments: Vec<FileAttachment>,
266 metadata: Value,
267 expect_ack: bool,
268 storage_id: Option<u64>,
271 },
272 EventAck {
274 event_id: u64,
275 ok: bool,
276 receipt: Value,
277 error: Option<ErrorPayload>,
278 },
279}
280
281impl Serialize for PacketBody {
284 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
285 where
286 S: serde::Serializer,
287 {
288 match self {
289 Self::ApiRequest {
290 request_id,
291 route,
292 params,
293 attachments,
294 metadata,
295 } => {
296 let mut s = serializer.serialize_map(Some(6))?;
297 s.serialize_entry("k", &K_API_REQUEST)?;
298 s.serialize_entry("i", request_id)?;
299 s.serialize_entry("r", route)?;
300 s.serialize_entry("p", params)?;
301 s.serialize_entry("a", attachments)?;
302 s.serialize_entry("m", metadata)?;
303 s.end()
304 }
305 Self::ApiResponse {
306 request_id,
307 ok,
308 status,
309 data,
310 error,
311 metadata,
312 } => {
313 let field_count = 5 + usize::from(error.is_some()) + 1;
314 let mut s = serializer.serialize_map(Some(field_count))?;
315 s.serialize_entry("k", &K_API_RESPONSE)?;
316 s.serialize_entry("i", request_id)?;
317 s.serialize_entry("o", ok)?;
318 s.serialize_entry("s", status)?;
319 s.serialize_entry("d", data)?;
320 if let Some(err) = error {
321 s.serialize_entry("er", err)?;
322 }
323 s.serialize_entry("m", metadata)?;
324 s.end()
325 }
326 Self::EventEmit {
327 event_id,
328 name,
329 data,
330 attachments,
331 metadata,
332 expect_ack,
333 storage_id,
334 } => {
335 let field_count = 7 + usize::from(storage_id.is_some());
336 let mut s = serializer.serialize_map(Some(field_count))?;
337 s.serialize_entry("k", &K_EVENT_EMIT)?;
338 s.serialize_entry("i", event_id)?;
339 s.serialize_entry("n", name)?;
340 s.serialize_entry("d", data)?;
341 s.serialize_entry("a", attachments)?;
342 s.serialize_entry("m", metadata)?;
343 s.serialize_entry("e", expect_ack)?;
344 if let Some(si) = storage_id {
345 s.serialize_entry("si", si)?;
346 }
347 s.end()
348 }
349 Self::EventAck {
350 event_id,
351 ok,
352 receipt,
353 error,
354 } => {
355 let field_count = 3 + usize::from(error.is_some()) + 1;
356 let mut s = serializer.serialize_map(Some(field_count))?;
357 s.serialize_entry("k", &K_EVENT_ACK)?;
358 s.serialize_entry("i", event_id)?;
359 s.serialize_entry("o", ok)?;
360 s.serialize_entry("rc", receipt)?;
361 if let Some(err) = error {
362 s.serialize_entry("er", err)?;
363 }
364 s.end()
365 }
366 }
367 }
368}
369
370#[derive(Deserialize)]
374struct ApiRequestFields {
375 #[serde(rename = "i")]
376 request_id: u64,
377 #[serde(rename = "r")]
378 route: String,
379 #[serde(rename = "p", default)]
380 params: Value,
381 #[serde(rename = "a", default)]
382 attachments: Vec<FileAttachment>,
383 #[serde(rename = "m", default)]
384 metadata: Value,
385}
386
387#[derive(Deserialize)]
389struct ApiResponseFields {
390 #[serde(rename = "i")]
391 request_id: u64,
392 #[serde(rename = "o", default)]
393 ok: bool,
394 #[serde(rename = "s", default)]
395 status: u16,
396 #[serde(rename = "d", default)]
397 data: Value,
398 #[serde(rename = "er", default)]
399 error: Option<ErrorPayload>,
400 #[serde(rename = "m", default)]
401 metadata: Value,
402}
403
404#[derive(Deserialize)]
406struct EventEmitFields {
407 #[serde(rename = "i")]
408 event_id: u64,
409 #[serde(rename = "n")]
410 name: String,
411 #[serde(rename = "d", default)]
412 data: Value,
413 #[serde(rename = "a", default)]
414 attachments: Vec<FileAttachment>,
415 #[serde(rename = "m", default)]
416 metadata: Value,
417 #[serde(rename = "e", default)]
418 expect_ack: bool,
419 #[serde(rename = "si", default)]
420 storage_id: Option<u64>,
421}
422
423#[derive(Deserialize)]
425struct EventAckFields {
426 #[serde(rename = "i")]
427 event_id: u64,
428 #[serde(rename = "o", default)]
429 ok: bool,
430 #[serde(rename = "rc", default)]
431 receipt: Value,
432 #[serde(rename = "er", default)]
433 error: Option<ErrorPayload>,
434}
435
436impl<'de> Deserialize<'de> for PacketBody {
437 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
438 where
439 D: Deserializer<'de>,
440 {
441 let value = Value::deserialize(deserializer)?;
444 let k = value.get("k").and_then(|v| v.as_u64()).ok_or_else(|| {
445 serde::de::Error::custom("missing or non-numeric 'k' field in packet body")
446 })?;
447 let k = u8::try_from(k).map_err(|_| {
448 serde::de::Error::custom(format!("'k' value {k} is outside the valid range"))
449 })?;
450
451 match k {
452 K_API_REQUEST => {
453 let f = serde_json::from_value::<ApiRequestFields>(value)
454 .map_err(serde::de::Error::custom)?;
455 Ok(Self::ApiRequest {
456 request_id: f.request_id,
457 route: f.route,
458 params: f.params,
459 attachments: f.attachments,
460 metadata: f.metadata,
461 })
462 }
463 K_EVENT_EMIT => {
464 let f = serde_json::from_value::<EventEmitFields>(value)
465 .map_err(serde::de::Error::custom)?;
466 Ok(Self::EventEmit {
467 event_id: f.event_id,
468 name: f.name,
469 data: f.data,
470 attachments: f.attachments,
471 metadata: f.metadata,
472 expect_ack: f.expect_ack,
473 storage_id: f.storage_id,
474 })
475 }
476 K_API_RESPONSE => {
477 let f = serde_json::from_value::<ApiResponseFields>(value)
478 .map_err(serde::de::Error::custom)?;
479 Ok(Self::ApiResponse {
480 request_id: f.request_id,
481 ok: f.ok,
482 status: f.status,
483 data: f.data,
484 error: f.error,
485 metadata: f.metadata,
486 })
487 }
488 K_EVENT_ACK => {
489 let f = serde_json::from_value::<EventAckFields>(value)
490 .map_err(serde::de::Error::custom)?;
491 Ok(Self::EventAck {
492 event_id: f.event_id,
493 ok: f.ok,
494 receipt: f.receipt,
495 error: f.error,
496 })
497 }
498 _ => Err(serde::de::Error::custom(format!("unknown 'k' value: {k}"))),
499 }
500 }
501}
502
503impl PacketBody {
504 pub fn message_type(&self) -> MessageType {
505 match self {
506 Self::ApiRequest { .. } | Self::ApiResponse { .. } => MessageType::Api,
507 Self::EventEmit { .. } | Self::EventAck { .. } => MessageType::Event,
508 }
509 }
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct PacketEnvelope {
515 pub message_type: MessageType,
517 pub encryption: EncryptionKind,
519 pub body: PacketBody,
521}
522
523impl PacketEnvelope {
524 pub fn new(body: PacketBody) -> Self {
526 Self {
527 message_type: body.message_type(),
528 encryption: EncryptionKind::None,
529 body,
530 }
531 }
532
533 pub fn with_encryption(body: PacketBody, encryption: EncryptionKind) -> Self {
535 Self {
536 message_type: body.message_type(),
537 encryption,
538 body,
539 }
540 }
541}
542
543#[derive(Clone, Default)]
549pub struct FrameCodec {
550 aes256_cipher: Option<Arc<Aes256Gcm>>,
551 chacha20_cipher: Option<Arc<ChaCha20Poly1305>>,
552}
553
554impl std::fmt::Debug for FrameCodec {
555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556 f.debug_struct("FrameCodec")
557 .field("aes256", &self.aes256_cipher.is_some())
558 .field("chacha20", &self.chacha20_cipher.is_some())
559 .finish()
560 }
561}
562
563impl FrameCodec {
564 pub fn plaintext() -> Self {
566 Self::default()
567 }
568
569 pub fn with_chacha20_key(self, key: [u8; 32]) -> Self {
574 let cipher = ChaCha20Poly1305::new_from_slice(&key)
575 .expect("a 32-byte key is always valid for ChaCha20-Poly1305");
576 Self {
577 chacha20_cipher: Some(Arc::new(cipher)),
578 ..self
579 }
580 }
581
582 pub fn with_aes256_key(self, key: [u8; 32]) -> Self {
587 let cipher =
588 Aes256Gcm::new_from_slice(&key).expect("a 32-byte key is always valid for AES-256-GCM");
589 Self {
590 aes256_cipher: Some(Arc::new(cipher)),
591 ..self
592 }
593 }
594
595 pub fn encode(&self, packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
597 let payload = serde_json::to_vec(&packet.body)?;
598
599 if payload.len() > MAX_PAYLOAD_BYTES {
602 return Err(ProtocolError::PayloadTooLarge {
603 actual: payload.len(),
604 max: MAX_PAYLOAD_BYTES,
605 });
606 }
607
608 let payload = match packet.encryption {
609 EncryptionKind::None => payload,
610 EncryptionKind::ChaCha20 => self.encrypt_chacha20(&payload)?,
611 EncryptionKind::Aes256 => self.encrypt_aes256(&payload)?,
612 };
613
614 if payload.len() > MAX_PAYLOAD_BYTES {
617 return Err(ProtocolError::PayloadTooLarge {
618 actual: payload.len(),
619 max: MAX_PAYLOAD_BYTES,
620 });
621 }
622
623 let frame_len = 2 + payload.len();
624 let mut frame = Vec::with_capacity(4 + frame_len);
625 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
626 frame.push(packet.message_type as u8);
627 frame.push(packet.encryption as u8);
628 frame.extend_from_slice(&payload);
629 Ok(frame)
630 }
631
632 pub fn decode(&self, frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
634 if frame.len() < 6 {
635 return Err(ProtocolError::FrameTooShort);
636 }
637
638 let declared = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
639 let actual = frame.len() - 4;
640 if declared != actual {
641 return Err(ProtocolError::FrameLengthMismatch { declared, actual });
642 }
643
644 let payload_len = actual - 2;
645 if payload_len > MAX_PAYLOAD_BYTES {
646 return Err(ProtocolError::PayloadTooLarge {
647 actual: payload_len,
648 max: MAX_PAYLOAD_BYTES,
649 });
650 }
651
652 let message_type = MessageType::try_from(frame[4])?;
653 let encryption = EncryptionKind::try_from(frame[5])?;
654 let payload: Cow<'_, [u8]> = match encryption {
656 EncryptionKind::None => Cow::Borrowed(&frame[6..]),
657 EncryptionKind::ChaCha20 => Cow::Owned(self.decrypt_chacha20(&frame[6..])?),
658 EncryptionKind::Aes256 => Cow::Owned(self.decrypt_aes256(&frame[6..])?),
659 };
660
661 let body: PacketBody = serde_json::from_slice(&payload)?;
662 if body.message_type() != message_type {
663 return Err(ProtocolError::MessageTypeMismatch);
664 }
665
666 Ok(PacketEnvelope {
667 message_type,
668 encryption,
669 body,
670 })
671 }
672
673 fn encrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
674 let cipher = self
675 .chacha20_cipher
676 .as_ref()
677 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
678 let mut nonce_bytes = [0_u8; CHACHA20_NONCE_LEN];
679 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
680 let ciphertext = cipher
681 .encrypt(Nonce::from_slice(&nonce_bytes), payload)
682 .map_err(|_| ProtocolError::EncryptionFailed("chacha20"))?;
683
684 let mut encoded = Vec::with_capacity(CHACHA20_NONCE_LEN + ciphertext.len());
685 encoded.extend_from_slice(&nonce_bytes);
686 encoded.extend_from_slice(&ciphertext);
687 Ok(encoded)
688 }
689
690 fn decrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
691 if payload.len() < CHACHA20_NONCE_LEN {
692 return Err(ProtocolError::EncryptedPayloadTooShort {
693 algorithm: "chacha20",
694 expected_min: CHACHA20_NONCE_LEN,
695 actual: payload.len(),
696 });
697 }
698
699 let cipher = self
700 .chacha20_cipher
701 .as_ref()
702 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
703 let (nonce_bytes, ciphertext) = payload.split_at(CHACHA20_NONCE_LEN);
704 cipher
705 .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
706 .map_err(|_| ProtocolError::DecryptionFailed("chacha20"))
707 }
708
709 fn encrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
710 let cipher = self
711 .aes256_cipher
712 .as_ref()
713 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
714 let mut nonce_bytes = [0_u8; AES256_NONCE_LEN];
715 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
716 let ciphertext = cipher
717 .encrypt(AesNonce::from_slice(&nonce_bytes), payload)
718 .map_err(|_| ProtocolError::EncryptionFailed("aes256"))?;
719
720 let mut encoded = Vec::with_capacity(AES256_NONCE_LEN + ciphertext.len());
721 encoded.extend_from_slice(&nonce_bytes);
722 encoded.extend_from_slice(&ciphertext);
723 Ok(encoded)
724 }
725
726 fn decrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
727 if payload.len() < AES256_NONCE_LEN {
728 return Err(ProtocolError::EncryptedPayloadTooShort {
729 algorithm: "aes256",
730 expected_min: AES256_NONCE_LEN,
731 actual: payload.len(),
732 });
733 }
734
735 let cipher = self
736 .aes256_cipher
737 .as_ref()
738 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
739 let (nonce_bytes, ciphertext) = payload.split_at(AES256_NONCE_LEN);
740 cipher
741 .decrypt(AesNonce::from_slice(nonce_bytes), ciphertext)
742 .map_err(|_| ProtocolError::DecryptionFailed("aes256"))
743 }
744}
745
746pub fn encode_frame(packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
748 FrameCodec::plaintext().encode(packet)
749}
750
751pub fn decode_frame(frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
753 FrameCodec::plaintext().decode(frame)
754}
755
756#[derive(Debug, Error)]
758pub enum ProtocolError {
759 #[error("frame too short")]
760 FrameTooShort,
761 #[error("frame length mismatch: declared={declared}, actual={actual}")]
762 FrameLengthMismatch { declared: usize, actual: usize },
763 #[error("payload too large: actual={actual}, max={max}")]
764 PayloadTooLarge { actual: usize, max: usize },
765 #[error("unknown message type: {0:#x}")]
766 UnknownMessageType(u8),
767 #[error("unknown encryption kind: {0:#x}")]
768 UnknownEncryption(u8),
769 #[error("unsupported encryption kind: {0:#x}")]
770 UnsupportedEncryption(u8),
771 #[error("missing encryption key for {0}")]
772 MissingEncryptionKey(&'static str),
773 #[error("invalid encryption key for {0}")]
774 InvalidEncryptionKey(&'static str),
775 #[error("secure random generation failed: {0}")]
776 Random(String),
777 #[error(
778 "encrypted payload too short for {algorithm}: expected at least {expected_min}, actual={actual}"
779 )]
780 EncryptedPayloadTooShort {
781 algorithm: &'static str,
782 expected_min: usize,
783 actual: usize,
784 },
785 #[error("encryption failed for {0}")]
786 EncryptionFailed(&'static str),
787 #[error("decryption failed for {0}")]
788 DecryptionFailed(&'static str),
789 #[error("invalid ECDH public key: expected {expected} bytes, got {actual}")]
790 InvalidEcdhPublicKey { expected: usize, actual: usize },
791 #[error("ECDH handshake failed: {0}")]
792 EcdhHandshake(String),
793 #[error("message type does not match packet body")]
794 MessageTypeMismatch,
795 #[error("invalid attachment encoding: {0}")]
796 InvalidAttachmentEncoding(String),
797 #[error("json error: {0}")]
798 Json(#[from] serde_json::Error),
799}
800
801#[cfg(test)]
802mod tests {
803 use super::{
804 EncryptionKind, FrameCodec, MAX_PAYLOAD_BYTES, MessageType, PacketBody, PacketEnvelope,
805 ProtocolError, decode_frame, encode_frame,
806 };
807 use serde_json::json;
808
809 const TEST_KEY: [u8; 32] = [0x11; 32];
810
811 #[test]
812 fn plaintext_helpers_still_work() {
813 let packet = PacketEnvelope::new(PacketBody::EventAck {
814 event_id: 1,
815 ok: true,
816 receipt: json!({ "ok": true }),
817 error: None,
818 });
819
820 let encoded = encode_frame(&packet).expect("encode plaintext");
821 let decoded = decode_frame(&encoded).expect("decode plaintext");
822 assert!(matches!(decoded.encryption, EncryptionKind::None));
823 }
824
825 #[test]
826 fn aes256_roundtrip_works() {
827 let codec = FrameCodec::plaintext().with_aes256_key(TEST_KEY);
828 let packet = PacketEnvelope::with_encryption(
829 PacketBody::ApiResponse {
830 request_id: 1,
831 ok: true,
832 status: 200,
833 data: json!({ "message": "encrypted" }),
834 error: None,
835 metadata: json!({}),
836 },
837 EncryptionKind::Aes256,
838 );
839
840 let encoded = codec.encode(&packet).expect("encode aes256");
841 let decoded = codec.decode(&encoded).expect("decode aes256");
842 assert!(matches!(decoded.encryption, EncryptionKind::Aes256));
843 }
844
845 #[test]
846 fn encode_rejects_payloads_over_limit() {
847 let codec = FrameCodec::plaintext();
848 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
849 request_id: 999,
850 ok: true,
851 status: 200,
852 data: json!({ "blob": "a".repeat(10 * 1024 * 1024) }),
853 error: None,
854 metadata: json!({}),
855 });
856
857 let error = codec
858 .encode(&packet)
859 .expect_err("oversized payload should fail");
860 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
861 }
862
863 #[test]
864 fn decode_rejects_payloads_over_limit() {
865 let payload = vec![0_u8; MAX_PAYLOAD_BYTES + 1];
866 let frame_len = 2 + payload.len();
867 let mut frame = Vec::with_capacity(4 + frame_len);
868 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
869 frame.push(MessageType::Api as u8);
870 frame.push(EncryptionKind::None as u8);
871 frame.extend_from_slice(&payload);
872
873 let error = FrameCodec::plaintext()
874 .decode(&frame)
875 .expect_err("oversized payload should fail");
876 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
877 }
878}