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
475fn take_u64<E: serde::de::Error>(map: &mut Map<String, Value>, key: &str) -> Result<u64, E> {
485 map.remove(key).and_then(|v| v.as_u64()).ok_or_else(|| {
486 E::custom(format!(
487 "missing or non-numeric '{key}' field in packet body"
488 ))
489 })
490}
491
492fn take_string<E: serde::de::Error>(map: &mut Map<String, Value>, key: &str) -> Result<String, E> {
494 match map.remove(key) {
495 Some(Value::String(s)) => Ok(s),
496 _ => Err(E::custom(format!(
497 "missing or non-string '{key}' field in packet body"
498 ))),
499 }
500}
501
502fn take_object<E: serde::de::Error>(
504 map: &mut Map<String, Value>,
505 key: &str,
506) -> Result<Map<String, Value>, E> {
507 match map.remove(key) {
508 None => Ok(Map::new()),
509 Some(Value::Object(o)) => Ok(o),
510 Some(_) => Err(E::custom(format!("'{key}' field must be a JSON object"))),
511 }
512}
513
514fn take_bool(map: &mut Map<String, Value>, key: &str) -> bool {
516 map.remove(key).and_then(|v| v.as_bool()).unwrap_or(false)
517}
518
519fn take_u16(map: &mut Map<String, Value>, key: &str) -> u16 {
521 map.remove(key).and_then(|v| v.as_u64()).unwrap_or(0) as u16
522}
523
524fn take_attachments<E: serde::de::Error>(
530 map: &mut Map<String, Value>,
531) -> Result<Vec<FileAttachment>, E> {
532 match map.remove("a") {
533 None => Ok(Vec::new()),
534 Some(value) => serde_json::from_value(value).map_err(E::custom),
535 }
536}
537
538fn take_error<E: serde::de::Error>(
540 map: &mut Map<String, Value>,
541) -> Result<Option<ErrorPayload>, E> {
542 match map.remove("er") {
543 None => Ok(None),
544 Some(value) => serde_json::from_value(value).map(Some).map_err(E::custom),
545 }
546}
547
548impl<'de> Deserialize<'de> for PacketBody {
549 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
550 where
551 D: Deserializer<'de>,
552 {
553 let mut map = match Value::deserialize(deserializer)? {
556 Value::Object(map) => map,
557 _ => {
558 return Err(serde::de::Error::custom(
559 "packet body must be a JSON object",
560 ));
561 }
562 };
563
564 let k = map.get("k").and_then(|v| v.as_u64()).ok_or_else(|| {
565 serde::de::Error::custom("missing or non-numeric 'k' field in packet body")
566 })?;
567 let k = u8::try_from(k).map_err(|_| {
568 serde::de::Error::custom(format!("'k' value {k} is outside the valid range"))
569 })?;
570
571 match k {
572 K_API_REQUEST => Ok(Self::ApiRequest {
573 request_id: take_u64(&mut map, "i")?,
574 route: take_string(&mut map, "r")?,
575 params: map.remove("p").unwrap_or(Value::Null),
576 attachments: take_attachments(&mut map)?,
577 metadata: map.remove("m").unwrap_or(Value::Null),
578 }),
579 K_EVENT_EMIT => Ok(Self::EventEmit {
580 event_id: take_u64(&mut map, "i")?,
581 name: take_string(&mut map, "n")?,
582 data: take_object(&mut map, "d")?,
583 attachments: take_attachments(&mut map)?,
584 metadata: map.remove("m").unwrap_or(Value::Null),
585 expect_ack: take_bool(&mut map, "e"),
586 }),
587 K_API_RESPONSE => Ok(Self::ApiResponse {
588 request_id: take_u64(&mut map, "i")?,
589 ok: take_bool(&mut map, "o"),
590 status: take_u16(&mut map, "s"),
591 data: map.remove("d").unwrap_or(Value::Null),
592 error: take_error(&mut map)?,
593 metadata: map.remove("m").unwrap_or(Value::Null),
594 }),
595 K_EVENT_ACK => Ok(Self::EventAck {
596 event_id: take_u64(&mut map, "i")?,
597 ok: take_bool(&mut map, "o"),
598 receipt: map.remove("rc").unwrap_or(Value::Null),
599 error: take_error(&mut map)?,
600 }),
601 _ => Err(serde::de::Error::custom(format!("unknown 'k' value: {k}"))),
602 }
603 }
604}
605
606impl PacketBody {
607 pub fn message_type(&self) -> MessageType {
608 match self {
609 Self::ApiRequest { .. } | Self::ApiResponse { .. } => MessageType::Api,
610 Self::EventEmit { .. } | Self::EventAck { .. } => MessageType::Event,
611 }
612 }
613
614 pub fn attachments(&self) -> &[FileAttachment] {
616 match self {
617 Self::ApiRequest { attachments, .. } => attachments,
618 Self::EventEmit { attachments, .. } => attachments,
619 _ => &[],
620 }
621 }
622
623 pub fn set_attachments(&mut self, attachments: Vec<FileAttachment>) {
625 match self {
626 Self::ApiRequest { attachments: a, .. } => *a = attachments,
627 Self::EventEmit { attachments: a, .. } => *a = attachments,
628 _ => {}
629 }
630 }
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct PacketEnvelope {
636 pub message_type: MessageType,
638 pub encryption: EncryptionKind,
640 pub body: PacketBody,
642}
643
644impl PacketEnvelope {
645 pub fn new(body: PacketBody) -> Self {
647 Self {
648 message_type: body.message_type(),
649 encryption: EncryptionKind::None,
650 body,
651 }
652 }
653
654 pub fn with_encryption(body: PacketBody, encryption: EncryptionKind) -> Self {
656 Self {
657 message_type: body.message_type(),
658 encryption,
659 body,
660 }
661 }
662}
663
664#[derive(Clone)]
673pub struct FrameCodec {
674 aes256_cipher: Option<Arc<Aes256Gcm>>,
675 chacha20_cipher: Option<Arc<ChaCha20Poly1305>>,
676 max_frame_bytes: usize,
678}
679
680impl Default for FrameCodec {
681 fn default() -> Self {
682 Self {
683 aes256_cipher: None,
684 chacha20_cipher: None,
685 max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
686 }
687 }
688}
689
690impl std::fmt::Debug for FrameCodec {
691 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692 f.debug_struct("FrameCodec")
693 .field("aes256", &self.aes256_cipher.is_some())
694 .field("chacha20", &self.chacha20_cipher.is_some())
695 .field("max_frame_bytes", &self.max_frame_bytes)
696 .finish()
697 }
698}
699
700impl FrameCodec {
701 pub fn plaintext() -> Self {
703 Self::default()
704 }
705
706 pub fn with_chacha20_key(self, key: [u8; 32]) -> Self {
711 let cipher = ChaCha20Poly1305::new_from_slice(&key)
712 .expect("a 32-byte key is always valid for ChaCha20-Poly1305");
713 Self {
714 chacha20_cipher: Some(Arc::new(cipher)),
715 ..self
716 }
717 }
718
719 pub fn with_aes256_key(self, key: [u8; 32]) -> Self {
724 let cipher =
725 Aes256Gcm::new_from_slice(&key).expect("a 32-byte key is always valid for AES-256-GCM");
726 Self {
727 aes256_cipher: Some(Arc::new(cipher)),
728 ..self
729 }
730 }
731
732 pub fn with_max_frame_bytes(mut self, max: usize) -> Self {
736 self.max_frame_bytes = max;
737 self
738 }
739
740 pub fn max_frame_bytes(&self) -> usize {
742 self.max_frame_bytes
743 }
744
745 pub fn encode(&self, packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
752 let max_payload = self.max_frame_bytes.saturating_sub(6);
753 let attachments = packet.body.attachments();
754 let att_size: usize = attachments.iter().map(|a| a.wire_size()).sum();
755 let message_type = packet.message_type as u8;
756 let encryption = packet.encryption as u8;
757
758 if packet.encryption == EncryptionKind::None {
765 let mut frame = Vec::with_capacity(4 + 2 + 4 + 1 + att_size + 64);
766 frame.extend_from_slice(&[0u8; 4]); frame.push(message_type);
768 frame.push(encryption);
769 frame.extend_from_slice(&[0u8; 4]); serde_json::to_writer(&mut frame, &packet.body)?;
771 let json_len = frame.len() - 10;
772 frame.push(attachments.len() as u8);
773 for att in attachments {
774 att.write_wire(&mut frame);
775 }
776 let payload_len = frame.len() - 6;
777 if payload_len > max_payload {
778 return Err(ProtocolError::PayloadTooLarge {
779 actual: payload_len,
780 max: max_payload,
781 });
782 }
783 let frame_len = (frame.len() - 4) as u32;
784 frame[0..4].copy_from_slice(&frame_len.to_be_bytes());
785 frame[6..10].copy_from_slice(&(json_len as u32).to_be_bytes());
786 return Ok(frame);
787 }
788
789 let mut composite = Vec::with_capacity(4 + 1 + att_size + 64);
793 composite.extend_from_slice(&[0u8; 4]); serde_json::to_writer(&mut composite, &packet.body)?;
795 let json_len = composite.len() - 4;
796 composite[0..4].copy_from_slice(&(json_len as u32).to_be_bytes());
797 composite.push(attachments.len() as u8);
798 for att in attachments {
799 att.write_wire(&mut composite);
800 }
801
802 if composite.len() > max_payload {
804 return Err(ProtocolError::PayloadTooLarge {
805 actual: composite.len(),
806 max: max_payload,
807 });
808 }
809
810 let payload = match packet.encryption {
811 EncryptionKind::ChaCha20 => self.encrypt_chacha20(&composite)?,
812 EncryptionKind::Aes256 => self.encrypt_aes256(&composite)?,
813 EncryptionKind::None => unreachable!("plaintext is handled by the fast path above"),
814 };
815
816 if payload.len() > max_payload {
818 return Err(ProtocolError::PayloadTooLarge {
819 actual: payload.len(),
820 max: max_payload,
821 });
822 }
823
824 let frame_len = 2 + payload.len();
825 let mut frame = Vec::with_capacity(4 + frame_len);
826 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
827 frame.push(message_type);
828 frame.push(encryption);
829 frame.extend_from_slice(&payload);
830 Ok(frame)
831 }
832
833 pub fn decode(&self, frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
835 if frame.len() < 6 {
836 return Err(ProtocolError::FrameTooShort);
837 }
838
839 let declared = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
840 let actual = frame.len() - 4;
841 if declared != actual {
842 return Err(ProtocolError::FrameLengthMismatch { declared, actual });
843 }
844
845 if frame.len() > self.max_frame_bytes {
846 return Err(ProtocolError::FrameTooLarge {
847 actual: frame.len(),
848 max: self.max_frame_bytes,
849 });
850 }
851
852 let message_type = MessageType::try_from(frame[4])?;
853 let encryption = EncryptionKind::try_from(frame[5])?;
854
855 let composite: Cow<'_, [u8]> = match encryption {
857 EncryptionKind::None => Cow::Borrowed(&frame[6..]),
858 EncryptionKind::ChaCha20 => Cow::Owned(self.decrypt_chacha20(&frame[6..])?),
859 EncryptionKind::Aes256 => Cow::Owned(self.decrypt_aes256(&frame[6..])?),
860 };
861
862 if composite.len() < 5 {
864 return Err(ProtocolError::FrameTooShort);
865 }
866 let meta_len =
867 u32::from_be_bytes([composite[0], composite[1], composite[2], composite[3]]) as usize;
868 if composite.len() < 4 + meta_len + 1 {
869 return Err(ProtocolError::FrameTooShort);
870 }
871 let json_slice = &composite[4..4 + meta_len];
872 let att_count = composite[4 + meta_len] as usize;
873 let mut att_data = &composite[4 + meta_len + 1..];
874
875 let mut attachments = Vec::with_capacity(att_count);
877 for _ in 0..att_count {
878 let (att, rest) = FileAttachment::read_wire(att_data)?;
879 attachments.push(att);
880 att_data = rest;
881 }
882
883 let mut body: PacketBody = serde_json::from_slice(json_slice)?;
885 body.set_attachments(attachments);
886
887 if body.message_type() != message_type {
888 return Err(ProtocolError::MessageTypeMismatch);
889 }
890
891 Ok(PacketEnvelope {
892 message_type,
893 encryption,
894 body,
895 })
896 }
897
898 fn encrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
899 let cipher = self
900 .chacha20_cipher
901 .as_ref()
902 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
903 let mut nonce_bytes = [0_u8; CHACHA20_NONCE_LEN];
904 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
905 let ciphertext = cipher
906 .encrypt(Nonce::from_slice(&nonce_bytes), payload)
907 .map_err(|_| ProtocolError::EncryptionFailed("chacha20"))?;
908
909 let mut encoded = Vec::with_capacity(CHACHA20_NONCE_LEN + ciphertext.len());
910 encoded.extend_from_slice(&nonce_bytes);
911 encoded.extend_from_slice(&ciphertext);
912 Ok(encoded)
913 }
914
915 fn decrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
916 if payload.len() < CHACHA20_NONCE_LEN {
917 return Err(ProtocolError::EncryptedPayloadTooShort {
918 algorithm: "chacha20",
919 expected_min: CHACHA20_NONCE_LEN,
920 actual: payload.len(),
921 });
922 }
923
924 let cipher = self
925 .chacha20_cipher
926 .as_ref()
927 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
928 let (nonce_bytes, ciphertext) = payload.split_at(CHACHA20_NONCE_LEN);
929 cipher
930 .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
931 .map_err(|_| ProtocolError::DecryptionFailed("chacha20"))
932 }
933
934 fn encrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
935 let cipher = self
936 .aes256_cipher
937 .as_ref()
938 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
939 let mut nonce_bytes = [0_u8; AES256_NONCE_LEN];
940 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
941 let ciphertext = cipher
942 .encrypt(AesNonce::from_slice(&nonce_bytes), payload)
943 .map_err(|_| ProtocolError::EncryptionFailed("aes256"))?;
944
945 let mut encoded = Vec::with_capacity(AES256_NONCE_LEN + ciphertext.len());
946 encoded.extend_from_slice(&nonce_bytes);
947 encoded.extend_from_slice(&ciphertext);
948 Ok(encoded)
949 }
950
951 fn decrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
952 if payload.len() < AES256_NONCE_LEN {
953 return Err(ProtocolError::EncryptedPayloadTooShort {
954 algorithm: "aes256",
955 expected_min: AES256_NONCE_LEN,
956 actual: payload.len(),
957 });
958 }
959
960 let cipher = self
961 .aes256_cipher
962 .as_ref()
963 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
964 let (nonce_bytes, ciphertext) = payload.split_at(AES256_NONCE_LEN);
965 cipher
966 .decrypt(AesNonce::from_slice(nonce_bytes), ciphertext)
967 .map_err(|_| ProtocolError::DecryptionFailed("aes256"))
968 }
969}
970
971pub fn encode_frame(packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
973 FrameCodec::plaintext().encode(packet)
974}
975
976pub fn decode_frame(frame: &[u8]) -> Result<PacketEnvelope, ProtocolError> {
978 FrameCodec::plaintext().decode(frame)
979}
980
981#[derive(Debug, Error)]
983pub enum ProtocolError {
984 #[error("frame too short")]
985 FrameTooShort,
986 #[error("frame length mismatch: declared={declared}, actual={actual}")]
987 FrameLengthMismatch { declared: usize, actual: usize },
988 #[error("payload too large: actual={actual}, max={max}")]
989 PayloadTooLarge { actual: usize, max: usize },
990 #[error("frame too large: actual={actual}, max={max}")]
991 FrameTooLarge { actual: usize, max: usize },
992 #[error("unknown message type: {0:#x}")]
993 UnknownMessageType(u8),
994 #[error("unknown encryption kind: {0:#x}")]
995 UnknownEncryption(u8),
996 #[error("unsupported encryption kind: {0:#x}")]
997 UnsupportedEncryption(u8),
998 #[error("missing encryption key for {0}")]
999 MissingEncryptionKey(&'static str),
1000 #[error("invalid encryption key for {0}")]
1001 InvalidEncryptionKey(&'static str),
1002 #[error("secure random generation failed: {0}")]
1003 Random(String),
1004 #[error(
1005 "encrypted payload too short for {algorithm}: expected at least {expected_min}, actual={actual}"
1006 )]
1007 EncryptedPayloadTooShort {
1008 algorithm: &'static str,
1009 expected_min: usize,
1010 actual: usize,
1011 },
1012 #[error("encryption failed for {0}")]
1013 EncryptionFailed(&'static str),
1014 #[error("decryption failed for {0}")]
1015 DecryptionFailed(&'static str),
1016 #[error("invalid ECDH public key: expected {expected} bytes, got {actual}")]
1017 InvalidEcdhPublicKey { expected: usize, actual: usize },
1018 #[error("ECDH handshake failed: {0}")]
1019 EcdhHandshake(String),
1020 #[error("message type does not match packet body")]
1021 MessageTypeMismatch,
1022 #[error("invalid attachment encoding: {0}")]
1023 InvalidAttachmentEncoding(String),
1024 #[error("json error: {0}")]
1025 Json(#[from] serde_json::Error),
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::{
1031 EncryptionKind, FileAttachment, FrameCodec, PacketBody, PacketEnvelope, ProtocolError,
1032 decode_frame, encode_frame,
1033 };
1034 use serde_json::json;
1035
1036 const TEST_KEY: [u8; 32] = [0x11; 32];
1037
1038 #[test]
1039 fn plaintext_helpers_still_work() {
1040 let packet = PacketEnvelope::new(PacketBody::EventAck {
1041 event_id: 1,
1042 ok: true,
1043 receipt: json!({ "ok": true }),
1044 error: None,
1045 });
1046
1047 let encoded = encode_frame(&packet).expect("encode plaintext");
1048 let decoded = decode_frame(&encoded).expect("decode plaintext");
1049 assert!(matches!(decoded.encryption, EncryptionKind::None));
1050 }
1051
1052 #[test]
1053 fn aes256_roundtrip_works() {
1054 let codec = FrameCodec::plaintext().with_aes256_key(TEST_KEY);
1055 let packet = PacketEnvelope::with_encryption(
1056 PacketBody::ApiResponse {
1057 request_id: 1,
1058 ok: true,
1059 status: 200,
1060 data: json!({ "message": "encrypted" }),
1061 error: None,
1062 metadata: json!({}),
1063 },
1064 EncryptionKind::Aes256,
1065 );
1066
1067 let encoded = codec.encode(&packet).expect("encode aes256");
1068 let decoded = codec.decode(&encoded).expect("decode aes256");
1069 assert!(matches!(decoded.encryption, EncryptionKind::Aes256));
1070 }
1071
1072 #[test]
1073 fn attachment_roundtrip_plaintext() {
1074 let att = FileAttachment::inline_bytes(
1075 "f1",
1076 "test.bin",
1077 "application/octet-stream",
1078 vec![1, 2, 3, 4, 5],
1079 );
1080 let packet = PacketEnvelope::new(PacketBody::ApiRequest {
1081 request_id: 42,
1082 route: "files.upload".to_string(),
1083 params: json!({ "file": { "$file": "f1" } }),
1084 attachments: vec![att],
1085 metadata: json!({}),
1086 });
1087
1088 let encoded = encode_frame(&packet).expect("encode with attachment");
1089 let decoded = decode_frame(&encoded).expect("decode with attachment");
1090
1091 let atts = decoded.body.attachments();
1092 assert_eq!(atts.len(), 1);
1093 assert_eq!(atts[0].id, "f1");
1094 assert_eq!(atts[0].name, "test.bin");
1095 assert_eq!(atts[0].content_type, "application/octet-stream");
1096 assert_eq!(atts[0].data, vec![1, 2, 3, 4, 5]);
1097 }
1098
1099 #[test]
1100 fn attachment_roundtrip_encrypted() {
1101 let codec = FrameCodec::plaintext().with_chacha20_key(TEST_KEY);
1102 let att = FileAttachment::inline_text("f2", "hello.txt", "text/plain", "hello world");
1103 let packet = PacketEnvelope::with_encryption(
1104 PacketBody::EventEmit {
1105 event_id: 7,
1106 name: "chat.message".to_string(),
1107 data: json!({ "text": "see attached" })
1108 .as_object()
1109 .unwrap()
1110 .clone(),
1111 attachments: vec![att],
1112 metadata: json!({}),
1113 expect_ack: true,
1114 },
1115 EncryptionKind::ChaCha20,
1116 );
1117
1118 let encoded = codec
1119 .encode(&packet)
1120 .expect("encode encrypted with attachment");
1121 let decoded = codec
1122 .decode(&encoded)
1123 .expect("decode encrypted with attachment");
1124
1125 let atts = decoded.body.attachments();
1126 assert_eq!(atts.len(), 1);
1127 assert_eq!(atts[0].id, "f2");
1128 assert_eq!(atts[0].data, b"hello world");
1129 }
1130
1131 #[test]
1132 fn encode_rejects_payloads_over_limit() {
1133 let codec = FrameCodec::plaintext().with_max_frame_bytes(1024);
1135 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
1136 request_id: 999,
1137 ok: true,
1138 status: 200,
1139 data: json!({ "blob": "a".repeat(2048) }),
1140 error: None,
1141 metadata: json!({}),
1142 });
1143
1144 let error = codec
1145 .encode(&packet)
1146 .expect_err("oversized payload should fail");
1147 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
1148 }
1149
1150 #[test]
1151 fn decode_rejects_frames_over_limit() {
1152 let codec = FrameCodec::plaintext().with_max_frame_bytes(64);
1153 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
1155 request_id: 1,
1156 ok: true,
1157 status: 200,
1158 data: json!({ "msg": "a]".repeat(50) }),
1159 error: None,
1160 metadata: json!({}),
1161 });
1162 let encoded = FrameCodec::plaintext()
1163 .encode(&packet)
1164 .expect("encode with default limit");
1165 assert!(encoded.len() > 64);
1166
1167 let error = codec
1168 .decode(&encoded)
1169 .expect_err("oversized frame should fail");
1170 assert!(matches!(error, ProtocolError::FrameTooLarge { .. }));
1171 }
1172}