1use std::borrow::Cow;
7use std::sync::Arc;
8
9use aes_gcm::{Aes256Gcm, KeyInit as AesKeyInit, Nonce as AesNonce, aead::Aead as AesAead};
10use chacha20poly1305::{ChaCha20Poly1305, Nonce};
11use getrandom::getrandom;
12use serde::de::Deserializer;
13use serde::ser::SerializeMap;
14use serde::{Deserialize, Serialize};
15use serde_json::{Map, Value, json};
16use sha2::{Digest, Sha256};
17use thiserror::Error;
18use x25519_dalek::{PublicKey, StaticSecret};
19
20pub const ECDH_DOMAIN_TAG: &[u8] = b"wscall-ecdh-v1";
31
32pub const ECDH_KEY_LEN: usize = 32;
34
35pub struct EcdhKeypair {
40 secret: StaticSecret,
41 public: PublicKey,
42}
43
44impl EcdhKeypair {
45 pub fn generate() -> Result<Self, ProtocolError> {
47 let mut secret_bytes = [0u8; ECDH_KEY_LEN];
48 getrandom(&mut secret_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
49 let secret = StaticSecret::from(secret_bytes);
50 let public = PublicKey::from(&secret);
51 Ok(Self { secret, public })
52 }
53
54 pub fn public_bytes(&self) -> [u8; ECDH_KEY_LEN] {
56 self.public.to_bytes()
57 }
58
59 pub fn derive_session_key(&self, peer_public: &[u8; ECDH_KEY_LEN]) -> [u8; 32] {
64 let peer = PublicKey::from(*peer_public);
65 let shared = self.secret.diffie_hellman(&peer);
66 derive_session_key(&shared.to_bytes())
67 }
68}
69
70pub fn derive_session_key(shared_secret: &[u8; 32]) -> [u8; 32] {
75 let mut hasher = Sha256::new();
76 hasher.update(ECDH_DOMAIN_TAG);
77 hasher.update(shared_secret);
78 let result = hasher.finalize();
79 let mut key = [0u8; 32];
80 key.copy_from_slice(&result);
81 key
82}
83
84pub fn parse_peer_public(bytes: &[u8]) -> Result<[u8; ECDH_KEY_LEN], ProtocolError> {
88 if bytes.len() != ECDH_KEY_LEN {
89 return Err(ProtocolError::InvalidEcdhPublicKey {
90 expected: ECDH_KEY_LEN,
91 actual: bytes.len(),
92 });
93 }
94 let mut key = [0u8; ECDH_KEY_LEN];
95 key.copy_from_slice(bytes);
96 Ok(key)
97}
98
99const AES256_NONCE_LEN: usize = 12;
100const CHACHA20_NONCE_LEN: usize = 12;
101
102pub const DEFAULT_MAX_FRAME_BYTES: usize = 100 * 1024 * 1024;
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[repr(u8)]
111pub enum MessageType {
112 Api = 0x00,
114 Event = 0x01,
116}
117
118impl TryFrom<u8> for MessageType {
119 type Error = ProtocolError;
120
121 fn try_from(value: u8) -> Result<Self, Self::Error> {
122 match value {
123 0x00 => Ok(Self::Api),
124 0x01 => Ok(Self::Event),
125 _ => Err(ProtocolError::UnknownMessageType(value)),
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132#[repr(u8)]
133pub enum EncryptionKind {
134 None = 0x00,
136 ChaCha20 = 0x01,
138 Aes256 = 0x02,
140}
141
142impl TryFrom<u8> for EncryptionKind {
143 type Error = ProtocolError;
144
145 fn try_from(value: u8) -> Result<Self, Self::Error> {
146 match value {
147 0x00 => Ok(Self::None),
148 0x01 => Ok(Self::ChaCha20),
149 0x02 => Ok(Self::Aes256),
150 _ => Err(ProtocolError::UnknownEncryption(value)),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct FileAttachment {
162 pub id: String,
164 pub name: String,
166 pub content_type: String,
168 pub data: Vec<u8>,
170}
171
172impl FileAttachment {
173 pub fn inline_text(
175 id: impl Into<String>,
176 name: impl Into<String>,
177 content_type: impl Into<String>,
178 text: impl AsRef<str>,
179 ) -> Self {
180 Self::inline_bytes(id, name, content_type, text.as_ref().as_bytes().to_vec())
181 }
182
183 pub fn inline_bytes(
185 id: impl Into<String>,
186 name: impl Into<String>,
187 content_type: impl Into<String>,
188 bytes: Vec<u8>,
189 ) -> Self {
190 Self {
191 id: id.into(),
192 name: name.into(),
193 content_type: content_type.into(),
194 data: bytes,
195 }
196 }
197
198 pub fn size(&self) -> usize {
200 self.data.len()
201 }
202
203 pub fn param_ref(id: impl Into<String>) -> Value {
205 json!({ "$file": id.into() })
206 }
207
208 pub(crate) fn wire_size(&self) -> usize {
212 1 + self.id.len() + 1 + self.name.len() + 1 + self.content_type.len() + 4 + self.data.len()
213 }
214
215 pub(crate) fn write_wire(&self, buf: &mut Vec<u8>) {
217 buf.push(self.id.len() as u8);
218 buf.extend_from_slice(self.id.as_bytes());
219 buf.push(self.name.len() as u8);
220 buf.extend_from_slice(self.name.as_bytes());
221 buf.push(self.content_type.len() as u8);
222 buf.extend_from_slice(self.content_type.as_bytes());
223 buf.extend_from_slice(&(self.data.len() as u32).to_be_bytes());
224 buf.extend_from_slice(&self.data);
225 }
226
227 pub(crate) fn read_wire(data: &[u8]) -> Result<(Self, &[u8]), ProtocolError> {
230 let mut pos = 0;
231
232 if pos >= data.len() {
234 return Err(ProtocolError::InvalidAttachmentEncoding(
235 "truncated id_len".into(),
236 ));
237 }
238 let id_len = data[pos] as usize;
239 pos += 1;
240 if pos + id_len > data.len() {
241 return Err(ProtocolError::InvalidAttachmentEncoding(
242 "truncated id".into(),
243 ));
244 }
245 let id = String::from_utf8_lossy(&data[pos..pos + id_len]).into_owned();
246 pos += id_len;
247
248 if pos >= data.len() {
250 return Err(ProtocolError::InvalidAttachmentEncoding(
251 "truncated name_len".into(),
252 ));
253 }
254 let name_len = data[pos] as usize;
255 pos += 1;
256 if pos + name_len > data.len() {
257 return Err(ProtocolError::InvalidAttachmentEncoding(
258 "truncated name".into(),
259 ));
260 }
261 let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
262 pos += name_len;
263
264 if pos >= data.len() {
266 return Err(ProtocolError::InvalidAttachmentEncoding(
267 "truncated ct_len".into(),
268 ));
269 }
270 let ct_len = data[pos] as usize;
271 pos += 1;
272 if pos + ct_len > data.len() {
273 return Err(ProtocolError::InvalidAttachmentEncoding(
274 "truncated content_type".into(),
275 ));
276 }
277 let content_type = String::from_utf8_lossy(&data[pos..pos + ct_len]).into_owned();
278 pos += ct_len;
279
280 if pos + 4 > data.len() {
282 return Err(ProtocolError::InvalidAttachmentEncoding(
283 "truncated data_len".into(),
284 ));
285 }
286 let data_len =
287 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
288 pos += 4;
289 if pos + data_len > data.len() {
290 return Err(ProtocolError::InvalidAttachmentEncoding(
291 "truncated data".into(),
292 ));
293 }
294 let payload = data[pos..pos + data_len].to_vec();
295 pos += data_len;
296
297 Ok((
298 Self {
299 id,
300 name,
301 content_type,
302 data: payload,
303 },
304 &data[pos..],
305 ))
306 }
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct ErrorPayload {
312 pub code: String,
313 pub message: String,
314 pub status: u16,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub details: Option<Value>,
317}
318
319pub const K_API_REQUEST: u8 = 0;
323pub const K_EVENT_EMIT: u8 = 1;
324pub const K_API_RESPONSE: u8 = 2;
325pub const K_EVENT_ACK: u8 = 3;
326
327#[derive(Debug, Clone)]
339pub enum PacketBody {
340 ApiRequest {
342 request_id: u64,
343 route: String,
344 params: Value,
345 attachments: Vec<FileAttachment>,
346 metadata: Value,
347 },
348 ApiResponse {
350 request_id: u64,
351 ok: bool,
352 status: u16,
353 data: Value,
354 error: Option<ErrorPayload>,
355 metadata: Value,
356 },
357 EventEmit {
361 event_id: u64,
362 name: String,
363 data: Map<String, Value>,
364 attachments: Vec<FileAttachment>,
365 metadata: Value,
366 expect_ack: bool,
367 },
368 EventAck {
370 event_id: u64,
371 ok: bool,
372 receipt: Value,
373 error: Option<ErrorPayload>,
374 },
375}
376
377fn metadata_is_empty(v: &Value) -> bool {
381 matches!(v, Value::Null) || v.as_object().is_some_and(|m| m.is_empty())
382}
383
384impl Serialize for PacketBody {
385 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
386 where
387 S: serde::Serializer,
388 {
389 match self {
390 Self::ApiRequest {
391 request_id,
392 route,
393 params,
394 attachments: _,
395 metadata,
396 } => {
397 let include_meta = !metadata_is_empty(metadata);
398 let field_count = 4 + usize::from(include_meta);
399 let mut s = serializer.serialize_map(Some(field_count))?;
400 s.serialize_entry("k", &K_API_REQUEST)?;
401 s.serialize_entry("i", request_id)?;
402 s.serialize_entry("r", route)?;
403 s.serialize_entry("p", params)?;
404 if include_meta {
405 s.serialize_entry("m", metadata)?;
406 }
407 s.end()
408 }
409 Self::ApiResponse {
410 request_id,
411 ok,
412 status,
413 data,
414 error,
415 metadata,
416 } => {
417 let include_meta = !metadata_is_empty(metadata);
418 let field_count = 5 + usize::from(error.is_some()) + usize::from(include_meta);
419 let mut s = serializer.serialize_map(Some(field_count))?;
420 s.serialize_entry("k", &K_API_RESPONSE)?;
421 s.serialize_entry("i", request_id)?;
422 s.serialize_entry("o", ok)?;
423 s.serialize_entry("s", status)?;
424 s.serialize_entry("d", data)?;
425 if let Some(err) = error {
426 s.serialize_entry("er", err)?;
427 }
428 if include_meta {
429 s.serialize_entry("m", metadata)?;
430 }
431 s.end()
432 }
433 Self::EventEmit {
434 event_id,
435 name,
436 data,
437 attachments: _,
438 metadata,
439 expect_ack,
440 } => {
441 let include_meta = !metadata_is_empty(metadata);
442 let field_count = 5 + usize::from(include_meta);
443 let mut s = serializer.serialize_map(Some(field_count))?;
444 s.serialize_entry("k", &K_EVENT_EMIT)?;
445 s.serialize_entry("i", event_id)?;
446 s.serialize_entry("n", name)?;
447 s.serialize_entry("d", data)?;
448 if include_meta {
449 s.serialize_entry("m", metadata)?;
450 }
451 s.serialize_entry("e", expect_ack)?;
452 s.end()
453 }
454 Self::EventAck {
455 event_id,
456 ok,
457 receipt,
458 error,
459 } => {
460 let field_count = 3 + usize::from(error.is_some()) + 1;
461 let mut s = serializer.serialize_map(Some(field_count))?;
462 s.serialize_entry("k", &K_EVENT_ACK)?;
463 s.serialize_entry("i", event_id)?;
464 s.serialize_entry("o", ok)?;
465 s.serialize_entry("rc", receipt)?;
466 if let Some(err) = error {
467 s.serialize_entry("er", err)?;
468 }
469 s.end()
470 }
471 }
472 }
473}
474
475#[derive(Deserialize)]
479struct ApiRequestFields {
480 #[serde(rename = "i")]
481 request_id: u64,
482 #[serde(rename = "r")]
483 route: String,
484 #[serde(rename = "p", default)]
485 params: Value,
486 #[serde(rename = "a", default)]
487 attachments: Vec<FileAttachment>,
488 #[serde(rename = "m", default)]
489 metadata: Value,
490}
491
492#[derive(Deserialize)]
494struct ApiResponseFields {
495 #[serde(rename = "i")]
496 request_id: u64,
497 #[serde(rename = "o", default)]
498 ok: bool,
499 #[serde(rename = "s", default)]
500 status: u16,
501 #[serde(rename = "d", default)]
502 data: Value,
503 #[serde(rename = "er", default)]
504 error: Option<ErrorPayload>,
505 #[serde(rename = "m", default)]
506 metadata: Value,
507}
508
509#[derive(Deserialize)]
511struct EventEmitFields {
512 #[serde(rename = "i")]
513 event_id: u64,
514 #[serde(rename = "n")]
515 name: String,
516 #[serde(rename = "d", default)]
517 data: Map<String, Value>,
518 #[serde(rename = "a", default)]
519 attachments: Vec<FileAttachment>,
520 #[serde(rename = "m", default)]
521 metadata: Value,
522 #[serde(rename = "e", default)]
523 expect_ack: bool,
524}
525
526#[derive(Deserialize)]
528struct EventAckFields {
529 #[serde(rename = "i")]
530 event_id: u64,
531 #[serde(rename = "o", default)]
532 ok: bool,
533 #[serde(rename = "rc", default)]
534 receipt: Value,
535 #[serde(rename = "er", default)]
536 error: Option<ErrorPayload>,
537}
538
539impl<'de> Deserialize<'de> for PacketBody {
540 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
541 where
542 D: Deserializer<'de>,
543 {
544 let value = Value::deserialize(deserializer)?;
547 let k = value.get("k").and_then(|v| v.as_u64()).ok_or_else(|| {
548 serde::de::Error::custom("missing or non-numeric 'k' field in packet body")
549 })?;
550 let k = u8::try_from(k).map_err(|_| {
551 serde::de::Error::custom(format!("'k' value {k} is outside the valid range"))
552 })?;
553
554 match k {
555 K_API_REQUEST => {
556 let f = serde_json::from_value::<ApiRequestFields>(value)
557 .map_err(serde::de::Error::custom)?;
558 Ok(Self::ApiRequest {
559 request_id: f.request_id,
560 route: f.route,
561 params: f.params,
562 attachments: f.attachments,
563 metadata: f.metadata,
564 })
565 }
566 K_EVENT_EMIT => {
567 let f = serde_json::from_value::<EventEmitFields>(value)
568 .map_err(serde::de::Error::custom)?;
569 Ok(Self::EventEmit {
570 event_id: f.event_id,
571 name: f.name,
572 data: f.data,
573 attachments: f.attachments,
574 metadata: f.metadata,
575 expect_ack: f.expect_ack,
576 })
577 }
578 K_API_RESPONSE => {
579 let f = serde_json::from_value::<ApiResponseFields>(value)
580 .map_err(serde::de::Error::custom)?;
581 Ok(Self::ApiResponse {
582 request_id: f.request_id,
583 ok: f.ok,
584 status: f.status,
585 data: f.data,
586 error: f.error,
587 metadata: f.metadata,
588 })
589 }
590 K_EVENT_ACK => {
591 let f = serde_json::from_value::<EventAckFields>(value)
592 .map_err(serde::de::Error::custom)?;
593 Ok(Self::EventAck {
594 event_id: f.event_id,
595 ok: f.ok,
596 receipt: f.receipt,
597 error: f.error,
598 })
599 }
600 _ => Err(serde::de::Error::custom(format!("unknown 'k' value: {k}"))),
601 }
602 }
603}
604
605impl PacketBody {
606 pub fn message_type(&self) -> MessageType {
607 match self {
608 Self::ApiRequest { .. } | Self::ApiResponse { .. } => MessageType::Api,
609 Self::EventEmit { .. } | Self::EventAck { .. } => MessageType::Event,
610 }
611 }
612
613 pub fn attachments(&self) -> &[FileAttachment] {
615 match self {
616 Self::ApiRequest { attachments, .. } => attachments,
617 Self::EventEmit { attachments, .. } => attachments,
618 _ => &[],
619 }
620 }
621
622 pub fn set_attachments(&mut self, attachments: Vec<FileAttachment>) {
624 match self {
625 Self::ApiRequest { attachments: a, .. } => *a = attachments,
626 Self::EventEmit { attachments: a, .. } => *a = attachments,
627 _ => {}
628 }
629 }
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct PacketEnvelope {
635 pub message_type: MessageType,
637 pub encryption: EncryptionKind,
639 pub body: PacketBody,
641}
642
643impl PacketEnvelope {
644 pub fn new(body: PacketBody) -> Self {
646 Self {
647 message_type: body.message_type(),
648 encryption: EncryptionKind::None,
649 body,
650 }
651 }
652
653 pub fn with_encryption(body: PacketBody, encryption: EncryptionKind) -> Self {
655 Self {
656 message_type: body.message_type(),
657 encryption,
658 body,
659 }
660 }
661}
662
663#[derive(Clone)]
672pub struct FrameCodec {
673 aes256_cipher: Option<Arc<Aes256Gcm>>,
674 chacha20_cipher: Option<Arc<ChaCha20Poly1305>>,
675 max_frame_bytes: usize,
677}
678
679impl Default for FrameCodec {
680 fn default() -> Self {
681 Self {
682 aes256_cipher: None,
683 chacha20_cipher: None,
684 max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
685 }
686 }
687}
688
689impl std::fmt::Debug for FrameCodec {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 f.debug_struct("FrameCodec")
692 .field("aes256", &self.aes256_cipher.is_some())
693 .field("chacha20", &self.chacha20_cipher.is_some())
694 .field("max_frame_bytes", &self.max_frame_bytes)
695 .finish()
696 }
697}
698
699impl FrameCodec {
700 pub fn plaintext() -> Self {
702 Self::default()
703 }
704
705 pub fn with_chacha20_key(self, key: [u8; 32]) -> Self {
710 let cipher = ChaCha20Poly1305::new_from_slice(&key)
711 .expect("a 32-byte key is always valid for ChaCha20-Poly1305");
712 Self {
713 chacha20_cipher: Some(Arc::new(cipher)),
714 ..self
715 }
716 }
717
718 pub fn with_aes256_key(self, key: [u8; 32]) -> Self {
723 let cipher =
724 Aes256Gcm::new_from_slice(&key).expect("a 32-byte key is always valid for AES-256-GCM");
725 Self {
726 aes256_cipher: Some(Arc::new(cipher)),
727 ..self
728 }
729 }
730
731 pub fn with_max_frame_bytes(mut self, max: usize) -> Self {
735 self.max_frame_bytes = max;
736 self
737 }
738
739 pub fn max_frame_bytes(&self) -> usize {
741 self.max_frame_bytes
742 }
743
744 pub fn encode(&self, packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
751 let max_payload = self.max_frame_bytes.saturating_sub(6);
752
753 let json_bytes = serde_json::to_vec(&packet.body)?;
755
756 let attachments = packet.body.attachments();
758
759 let mut composite = Vec::with_capacity(
761 4 + json_bytes.len() + 1 + attachments.iter().map(|a| a.wire_size()).sum::<usize>(),
762 );
763 composite.extend_from_slice(&(json_bytes.len() as u32).to_be_bytes());
764 composite.extend_from_slice(&json_bytes);
765 composite.push(attachments.len() as u8);
766 for att in attachments {
767 att.write_wire(&mut composite);
768 }
769
770 if composite.len() > max_payload {
772 return Err(ProtocolError::PayloadTooLarge {
773 actual: composite.len(),
774 max: max_payload,
775 });
776 }
777
778 let payload = match packet.encryption {
779 EncryptionKind::None => composite,
780 EncryptionKind::ChaCha20 => self.encrypt_chacha20(&composite)?,
781 EncryptionKind::Aes256 => self.encrypt_aes256(&composite)?,
782 };
783
784 if payload.len() > max_payload {
786 return Err(ProtocolError::PayloadTooLarge {
787 actual: payload.len(),
788 max: max_payload,
789 });
790 }
791
792 let frame_len = 2 + payload.len();
793 let mut frame = Vec::with_capacity(4 + frame_len);
794 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
795 frame.push(packet.message_type as u8);
796 frame.push(packet.encryption as u8);
797 frame.extend_from_slice(&payload);
798 Ok(frame)
799 }
800
801 pub fn decode(&self, frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
803 if frame.len() < 6 {
804 return Err(ProtocolError::FrameTooShort);
805 }
806
807 let declared = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
808 let actual = frame.len() - 4;
809 if declared != actual {
810 return Err(ProtocolError::FrameLengthMismatch { declared, actual });
811 }
812
813 if frame.len() > self.max_frame_bytes {
814 return Err(ProtocolError::FrameTooLarge {
815 actual: frame.len(),
816 max: self.max_frame_bytes,
817 });
818 }
819
820 let message_type = MessageType::try_from(frame[4])?;
821 let encryption = EncryptionKind::try_from(frame[5])?;
822
823 let composite: Cow<'_, [u8]> = match encryption {
825 EncryptionKind::None => Cow::Borrowed(&frame[6..]),
826 EncryptionKind::ChaCha20 => Cow::Owned(self.decrypt_chacha20(&frame[6..])?),
827 EncryptionKind::Aes256 => Cow::Owned(self.decrypt_aes256(&frame[6..])?),
828 };
829
830 if composite.len() < 5 {
832 return Err(ProtocolError::FrameTooShort);
833 }
834 let meta_len =
835 u32::from_be_bytes([composite[0], composite[1], composite[2], composite[3]]) as usize;
836 if composite.len() < 4 + meta_len + 1 {
837 return Err(ProtocolError::FrameTooShort);
838 }
839 let json_slice = &composite[4..4 + meta_len];
840 let att_count = composite[4 + meta_len] as usize;
841 let mut att_data = &composite[4 + meta_len + 1..];
842
843 let mut attachments = Vec::with_capacity(att_count);
845 for _ in 0..att_count {
846 let (att, rest) = FileAttachment::read_wire(att_data)?;
847 attachments.push(att);
848 att_data = rest;
849 }
850
851 let mut body: PacketBody = serde_json::from_slice(json_slice)?;
853 body.set_attachments(attachments);
854
855 if body.message_type() != message_type {
856 return Err(ProtocolError::MessageTypeMismatch);
857 }
858
859 Ok(PacketEnvelope {
860 message_type,
861 encryption,
862 body,
863 })
864 }
865
866 fn encrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
867 let cipher = self
868 .chacha20_cipher
869 .as_ref()
870 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
871 let mut nonce_bytes = [0_u8; CHACHA20_NONCE_LEN];
872 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
873 let ciphertext = cipher
874 .encrypt(Nonce::from_slice(&nonce_bytes), payload)
875 .map_err(|_| ProtocolError::EncryptionFailed("chacha20"))?;
876
877 let mut encoded = Vec::with_capacity(CHACHA20_NONCE_LEN + ciphertext.len());
878 encoded.extend_from_slice(&nonce_bytes);
879 encoded.extend_from_slice(&ciphertext);
880 Ok(encoded)
881 }
882
883 fn decrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
884 if payload.len() < CHACHA20_NONCE_LEN {
885 return Err(ProtocolError::EncryptedPayloadTooShort {
886 algorithm: "chacha20",
887 expected_min: CHACHA20_NONCE_LEN,
888 actual: payload.len(),
889 });
890 }
891
892 let cipher = self
893 .chacha20_cipher
894 .as_ref()
895 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
896 let (nonce_bytes, ciphertext) = payload.split_at(CHACHA20_NONCE_LEN);
897 cipher
898 .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
899 .map_err(|_| ProtocolError::DecryptionFailed("chacha20"))
900 }
901
902 fn encrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
903 let cipher = self
904 .aes256_cipher
905 .as_ref()
906 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
907 let mut nonce_bytes = [0_u8; AES256_NONCE_LEN];
908 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
909 let ciphertext = cipher
910 .encrypt(AesNonce::from_slice(&nonce_bytes), payload)
911 .map_err(|_| ProtocolError::EncryptionFailed("aes256"))?;
912
913 let mut encoded = Vec::with_capacity(AES256_NONCE_LEN + ciphertext.len());
914 encoded.extend_from_slice(&nonce_bytes);
915 encoded.extend_from_slice(&ciphertext);
916 Ok(encoded)
917 }
918
919 fn decrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
920 if payload.len() < AES256_NONCE_LEN {
921 return Err(ProtocolError::EncryptedPayloadTooShort {
922 algorithm: "aes256",
923 expected_min: AES256_NONCE_LEN,
924 actual: payload.len(),
925 });
926 }
927
928 let cipher = self
929 .aes256_cipher
930 .as_ref()
931 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
932 let (nonce_bytes, ciphertext) = payload.split_at(AES256_NONCE_LEN);
933 cipher
934 .decrypt(AesNonce::from_slice(nonce_bytes), ciphertext)
935 .map_err(|_| ProtocolError::DecryptionFailed("aes256"))
936 }
937}
938
939pub fn encode_frame(packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
941 FrameCodec::plaintext().encode(packet)
942}
943
944pub fn decode_frame(frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
946 FrameCodec::plaintext().decode(frame)
947}
948
949#[derive(Debug, Error)]
951pub enum ProtocolError {
952 #[error("frame too short")]
953 FrameTooShort,
954 #[error("frame length mismatch: declared={declared}, actual={actual}")]
955 FrameLengthMismatch { declared: usize, actual: usize },
956 #[error("payload too large: actual={actual}, max={max}")]
957 PayloadTooLarge { actual: usize, max: usize },
958 #[error("frame too large: actual={actual}, max={max}")]
959 FrameTooLarge { actual: usize, max: usize },
960 #[error("unknown message type: {0:#x}")]
961 UnknownMessageType(u8),
962 #[error("unknown encryption kind: {0:#x}")]
963 UnknownEncryption(u8),
964 #[error("unsupported encryption kind: {0:#x}")]
965 UnsupportedEncryption(u8),
966 #[error("missing encryption key for {0}")]
967 MissingEncryptionKey(&'static str),
968 #[error("invalid encryption key for {0}")]
969 InvalidEncryptionKey(&'static str),
970 #[error("secure random generation failed: {0}")]
971 Random(String),
972 #[error(
973 "encrypted payload too short for {algorithm}: expected at least {expected_min}, actual={actual}"
974 )]
975 EncryptedPayloadTooShort {
976 algorithm: &'static str,
977 expected_min: usize,
978 actual: usize,
979 },
980 #[error("encryption failed for {0}")]
981 EncryptionFailed(&'static str),
982 #[error("decryption failed for {0}")]
983 DecryptionFailed(&'static str),
984 #[error("invalid ECDH public key: expected {expected} bytes, got {actual}")]
985 InvalidEcdhPublicKey { expected: usize, actual: usize },
986 #[error("ECDH handshake failed: {0}")]
987 EcdhHandshake(String),
988 #[error("message type does not match packet body")]
989 MessageTypeMismatch,
990 #[error("invalid attachment encoding: {0}")]
991 InvalidAttachmentEncoding(String),
992 #[error("json error: {0}")]
993 Json(#[from] serde_json::Error),
994}
995
996#[cfg(test)]
997mod tests {
998 use super::{
999 EncryptionKind, FileAttachment, FrameCodec, PacketBody, PacketEnvelope, ProtocolError,
1000 decode_frame, encode_frame,
1001 };
1002 use serde_json::json;
1003
1004 const TEST_KEY: [u8; 32] = [0x11; 32];
1005
1006 #[test]
1007 fn plaintext_helpers_still_work() {
1008 let packet = PacketEnvelope::new(PacketBody::EventAck {
1009 event_id: 1,
1010 ok: true,
1011 receipt: json!({ "ok": true }),
1012 error: None,
1013 });
1014
1015 let encoded = encode_frame(&packet).expect("encode plaintext");
1016 let decoded = decode_frame(&encoded).expect("decode plaintext");
1017 assert!(matches!(decoded.encryption, EncryptionKind::None));
1018 }
1019
1020 #[test]
1021 fn aes256_roundtrip_works() {
1022 let codec = FrameCodec::plaintext().with_aes256_key(TEST_KEY);
1023 let packet = PacketEnvelope::with_encryption(
1024 PacketBody::ApiResponse {
1025 request_id: 1,
1026 ok: true,
1027 status: 200,
1028 data: json!({ "message": "encrypted" }),
1029 error: None,
1030 metadata: json!({}),
1031 },
1032 EncryptionKind::Aes256,
1033 );
1034
1035 let encoded = codec.encode(&packet).expect("encode aes256");
1036 let decoded = codec.decode(&encoded).expect("decode aes256");
1037 assert!(matches!(decoded.encryption, EncryptionKind::Aes256));
1038 }
1039
1040 #[test]
1041 fn attachment_roundtrip_plaintext() {
1042 let att = FileAttachment::inline_bytes(
1043 "f1",
1044 "test.bin",
1045 "application/octet-stream",
1046 vec![1, 2, 3, 4, 5],
1047 );
1048 let packet = PacketEnvelope::new(PacketBody::ApiRequest {
1049 request_id: 42,
1050 route: "files.upload".to_string(),
1051 params: json!({ "file": { "$file": "f1" } }),
1052 attachments: vec![att],
1053 metadata: json!({}),
1054 });
1055
1056 let encoded = encode_frame(&packet).expect("encode with attachment");
1057 let decoded = decode_frame(&encoded).expect("decode with attachment");
1058
1059 let atts = decoded.body.attachments();
1060 assert_eq!(atts.len(), 1);
1061 assert_eq!(atts[0].id, "f1");
1062 assert_eq!(atts[0].name, "test.bin");
1063 assert_eq!(atts[0].content_type, "application/octet-stream");
1064 assert_eq!(atts[0].data, vec![1, 2, 3, 4, 5]);
1065 }
1066
1067 #[test]
1068 fn attachment_roundtrip_encrypted() {
1069 let codec = FrameCodec::plaintext().with_chacha20_key(TEST_KEY);
1070 let att = FileAttachment::inline_text("f2", "hello.txt", "text/plain", "hello world");
1071 let packet = PacketEnvelope::with_encryption(
1072 PacketBody::EventEmit {
1073 event_id: 7,
1074 name: "chat.message".to_string(),
1075 data: json!({ "text": "see attached" })
1076 .as_object()
1077 .unwrap()
1078 .clone(),
1079 attachments: vec![att],
1080 metadata: json!({}),
1081 expect_ack: true,
1082 },
1083 EncryptionKind::ChaCha20,
1084 );
1085
1086 let encoded = codec
1087 .encode(&packet)
1088 .expect("encode encrypted with attachment");
1089 let decoded = codec
1090 .decode(&encoded)
1091 .expect("decode encrypted with attachment");
1092
1093 let atts = decoded.body.attachments();
1094 assert_eq!(atts.len(), 1);
1095 assert_eq!(atts[0].id, "f2");
1096 assert_eq!(atts[0].data, b"hello world");
1097 }
1098
1099 #[test]
1100 fn encode_rejects_payloads_over_limit() {
1101 let codec = FrameCodec::plaintext().with_max_frame_bytes(1024);
1103 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
1104 request_id: 999,
1105 ok: true,
1106 status: 200,
1107 data: json!({ "blob": "a".repeat(2048) }),
1108 error: None,
1109 metadata: json!({}),
1110 });
1111
1112 let error = codec
1113 .encode(&packet)
1114 .expect_err("oversized payload should fail");
1115 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
1116 }
1117
1118 #[test]
1119 fn decode_rejects_frames_over_limit() {
1120 let codec = FrameCodec::plaintext().with_max_frame_bytes(64);
1121 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
1123 request_id: 1,
1124 ok: true,
1125 status: 200,
1126 data: json!({ "msg": "a]".repeat(50) }),
1127 error: None,
1128 metadata: json!({}),
1129 });
1130 let encoded = FrameCodec::plaintext()
1131 .encode(&packet)
1132 .expect("encode with default limit");
1133 assert!(encoded.len() > 64);
1134
1135 let error = codec
1136 .decode(&encoded)
1137 .expect_err("oversized frame should fail");
1138 assert!(matches!(error, ProtocolError::FrameTooLarge { .. }));
1139 }
1140}