1use std::sync::Arc;
7
8use aes_gcm::{Aes256Gcm, KeyInit as AesKeyInit, Nonce as AesNonce, aead::Aead as AesAead};
9use bytes::Bytes;
10use chacha20poly1305::{ChaCha20Poly1305, Nonce};
11use getrandom::getrandom;
12use serde::de::{Deserializer, MapAccess, SeqAccess, Visitor};
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)]
165pub struct FileAttachment {
166 pub id: String,
168 pub name: String,
170 pub content_type: String,
172 pub data: Bytes,
174}
175
176impl FileAttachment {
177 pub fn inline_text(
179 id: impl Into<String>,
180 name: impl Into<String>,
181 content_type: impl Into<String>,
182 text: impl AsRef<str>,
183 ) -> Self {
184 Self::inline_bytes(
185 id,
186 name,
187 content_type,
188 Bytes::from(text.as_ref().to_owned()),
189 )
190 }
191
192 pub fn inline_bytes(
197 id: impl Into<String>,
198 name: impl Into<String>,
199 content_type: impl Into<String>,
200 bytes: impl Into<Bytes>,
201 ) -> Self {
202 Self {
203 id: id.into(),
204 name: name.into(),
205 content_type: content_type.into(),
206 data: bytes.into(),
207 }
208 }
209
210 pub fn size(&self) -> usize {
212 self.data.len()
213 }
214
215 pub fn param_ref(id: impl Into<String>) -> Value {
217 json!({ "$file": id.into() })
218 }
219
220 pub(crate) fn wire_size(&self) -> usize {
224 1 + self.id.len() + 1 + self.name.len() + 1 + self.content_type.len() + 4 + self.data.len()
225 }
226
227 pub(crate) fn write_wire(&self, buf: &mut Vec<u8>) {
229 buf.push(self.id.len() as u8);
230 buf.extend_from_slice(self.id.as_bytes());
231 buf.push(self.name.len() as u8);
232 buf.extend_from_slice(self.name.as_bytes());
233 buf.push(self.content_type.len() as u8);
234 buf.extend_from_slice(self.content_type.as_bytes());
235 buf.extend_from_slice(&(self.data.len() as u32).to_be_bytes());
236 buf.extend_from_slice(&self.data);
237 }
238
239 pub(crate) fn read_wire(data: &Bytes) -> Result<(Self, Bytes), ProtocolError> {
245 let mut pos = 0;
246
247 if pos >= data.len() {
249 return Err(ProtocolError::InvalidAttachmentEncoding(
250 "truncated id_len".into(),
251 ));
252 }
253 let id_len = data[pos] as usize;
254 pos += 1;
255 if pos + id_len > data.len() {
256 return Err(ProtocolError::InvalidAttachmentEncoding(
257 "truncated id".into(),
258 ));
259 }
260 let id = String::from_utf8_lossy(&data[pos..pos + id_len]).into_owned();
261 pos += id_len;
262
263 if pos >= data.len() {
265 return Err(ProtocolError::InvalidAttachmentEncoding(
266 "truncated name_len".into(),
267 ));
268 }
269 let name_len = data[pos] as usize;
270 pos += 1;
271 if pos + name_len > data.len() {
272 return Err(ProtocolError::InvalidAttachmentEncoding(
273 "truncated name".into(),
274 ));
275 }
276 let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
277 pos += name_len;
278
279 if pos >= data.len() {
281 return Err(ProtocolError::InvalidAttachmentEncoding(
282 "truncated ct_len".into(),
283 ));
284 }
285 let ct_len = data[pos] as usize;
286 pos += 1;
287 if pos + ct_len > data.len() {
288 return Err(ProtocolError::InvalidAttachmentEncoding(
289 "truncated content_type".into(),
290 ));
291 }
292 let content_type = String::from_utf8_lossy(&data[pos..pos + ct_len]).into_owned();
293 pos += ct_len;
294
295 if pos + 4 > data.len() {
297 return Err(ProtocolError::InvalidAttachmentEncoding(
298 "truncated data_len".into(),
299 ));
300 }
301 let data_len =
302 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
303 pos += 4;
304 if pos + data_len > data.len() {
305 return Err(ProtocolError::InvalidAttachmentEncoding(
306 "truncated data".into(),
307 ));
308 }
309 let payload = data.slice(pos..pos + data_len);
311 pos += data_len;
312
313 Ok((
314 Self {
315 id,
316 name,
317 content_type,
318 data: payload,
319 },
320 data.slice(pos..),
321 ))
322 }
323}
324
325impl Serialize for FileAttachment {
332 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
333 let mut map = serializer.serialize_map(Some(4))?;
334 map.serialize_entry("id", &self.id)?;
335 map.serialize_entry("name", &self.name)?;
336 map.serialize_entry("content_type", &self.content_type)?;
337 map.serialize_entry("data", &self.data.to_vec())?;
338 map.end()
339 }
340}
341
342impl<'de> Deserialize<'de> for FileAttachment {
343 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
344 #[derive(Deserialize)]
345 #[serde(field_identifier, rename_all = "snake_case")]
346 enum Field {
347 Id,
348 Name,
349 ContentType,
350 Data,
351 }
352
353 struct FileAttachmentVisitor;
354
355 impl<'de> Visitor<'de> for FileAttachmentVisitor {
356 type Value = FileAttachment;
357
358 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
359 f.write_str("struct FileAttachment")
360 }
361
362 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
363 let mut id = None;
364 let mut name = None;
365 let mut content_type = None;
366 let mut data: Option<Bytes> = None;
367 while let Some(key) = map.next_key()? {
368 match key {
369 Field::Id => id = Some(map.next_value()?),
370 Field::Name => name = Some(map.next_value()?),
371 Field::ContentType => content_type = Some(map.next_value()?),
372 Field::Data => {
373 let bytes: Vec<u8> = map.next_value()?;
374 data = Some(Bytes::from(bytes));
375 }
376 }
377 }
378 Ok(FileAttachment {
379 id: id.ok_or_else(|| serde::de::Error::missing_field("id"))?,
380 name: name.ok_or_else(|| serde::de::Error::missing_field("name"))?,
381 content_type: content_type
382 .ok_or_else(|| serde::de::Error::missing_field("content_type"))?,
383 data: data.ok_or_else(|| serde::de::Error::missing_field("data"))?,
384 })
385 }
386
387 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
388 let id = seq
389 .next_element()?
390 .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
391 let name = seq
392 .next_element()?
393 .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
394 let content_type = seq
395 .next_element()?
396 .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
397 let bytes: Vec<u8> = seq
398 .next_element()?
399 .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
400 Ok(FileAttachment {
401 id,
402 name,
403 content_type,
404 data: Bytes::from(bytes),
405 })
406 }
407 }
408
409 const FIELDS: &[&str] = &["id", "name", "content_type", "data"];
410 deserializer.deserialize_struct("FileAttachment", FIELDS, FileAttachmentVisitor)
411 }
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ErrorPayload {
417 pub code: String,
418 pub message: String,
419 pub status: u16,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub details: Option<Value>,
422}
423
424pub const K_API_REQUEST: u8 = 0;
428pub const K_EVENT_EMIT: u8 = 1;
429pub const K_API_RESPONSE: u8 = 2;
430pub const K_EVENT_ACK: u8 = 3;
431
432#[derive(Debug, Clone)]
444pub enum PacketBody {
445 ApiRequest {
447 request_id: u64,
448 route: String,
449 params: Value,
450 attachments: Vec<FileAttachment>,
451 metadata: Value,
452 },
453 ApiResponse {
455 request_id: u64,
456 ok: bool,
457 status: u16,
458 data: Value,
459 error: Option<ErrorPayload>,
460 metadata: Value,
461 },
462 EventEmit {
466 event_id: u64,
467 name: String,
468 data: Map<String, Value>,
469 attachments: Vec<FileAttachment>,
470 metadata: Value,
471 expect_ack: bool,
472 },
473 EventAck {
475 event_id: u64,
476 ok: bool,
477 receipt: Value,
478 error: Option<ErrorPayload>,
479 },
480}
481
482fn metadata_is_empty(v: &Value) -> bool {
486 matches!(v, Value::Null) || v.as_object().is_some_and(|m| m.is_empty())
487}
488
489impl Serialize for PacketBody {
490 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
491 where
492 S: serde::Serializer,
493 {
494 match self {
495 Self::ApiRequest {
496 request_id,
497 route,
498 params,
499 attachments: _,
500 metadata,
501 } => {
502 let include_meta = !metadata_is_empty(metadata);
503 let field_count = 4 + usize::from(include_meta);
504 let mut s = serializer.serialize_map(Some(field_count))?;
505 s.serialize_entry("k", &K_API_REQUEST)?;
506 s.serialize_entry("i", request_id)?;
507 s.serialize_entry("r", route)?;
508 s.serialize_entry("p", params)?;
509 if include_meta {
510 s.serialize_entry("m", metadata)?;
511 }
512 s.end()
513 }
514 Self::ApiResponse {
515 request_id,
516 ok,
517 status,
518 data,
519 error,
520 metadata,
521 } => {
522 let include_meta = !metadata_is_empty(metadata);
523 let field_count = 5 + usize::from(error.is_some()) + usize::from(include_meta);
524 let mut s = serializer.serialize_map(Some(field_count))?;
525 s.serialize_entry("k", &K_API_RESPONSE)?;
526 s.serialize_entry("i", request_id)?;
527 s.serialize_entry("o", ok)?;
528 s.serialize_entry("s", status)?;
529 s.serialize_entry("d", data)?;
530 if let Some(err) = error {
531 s.serialize_entry("er", err)?;
532 }
533 if include_meta {
534 s.serialize_entry("m", metadata)?;
535 }
536 s.end()
537 }
538 Self::EventEmit {
539 event_id,
540 name,
541 data,
542 attachments: _,
543 metadata,
544 expect_ack,
545 } => {
546 let include_meta = !metadata_is_empty(metadata);
547 let field_count = 5 + usize::from(include_meta);
548 let mut s = serializer.serialize_map(Some(field_count))?;
549 s.serialize_entry("k", &K_EVENT_EMIT)?;
550 s.serialize_entry("i", event_id)?;
551 s.serialize_entry("n", name)?;
552 s.serialize_entry("d", data)?;
553 if include_meta {
554 s.serialize_entry("m", metadata)?;
555 }
556 s.serialize_entry("e", expect_ack)?;
557 s.end()
558 }
559 Self::EventAck {
560 event_id,
561 ok,
562 receipt,
563 error,
564 } => {
565 let field_count = 3 + usize::from(error.is_some()) + 1;
566 let mut s = serializer.serialize_map(Some(field_count))?;
567 s.serialize_entry("k", &K_EVENT_ACK)?;
568 s.serialize_entry("i", event_id)?;
569 s.serialize_entry("o", ok)?;
570 s.serialize_entry("rc", receipt)?;
571 if let Some(err) = error {
572 s.serialize_entry("er", err)?;
573 }
574 s.end()
575 }
576 }
577 }
578}
579
580fn take_u64<E: serde::de::Error>(map: &mut Map<String, Value>, key: &str) -> Result<u64, E> {
590 map.remove(key).and_then(|v| v.as_u64()).ok_or_else(|| {
591 E::custom(format!(
592 "missing or non-numeric '{key}' field in packet body"
593 ))
594 })
595}
596
597fn take_string<E: serde::de::Error>(map: &mut Map<String, Value>, key: &str) -> Result<String, E> {
599 match map.remove(key) {
600 Some(Value::String(s)) => Ok(s),
601 _ => Err(E::custom(format!(
602 "missing or non-string '{key}' field in packet body"
603 ))),
604 }
605}
606
607fn take_object<E: serde::de::Error>(
609 map: &mut Map<String, Value>,
610 key: &str,
611) -> Result<Map<String, Value>, E> {
612 match map.remove(key) {
613 None => Ok(Map::new()),
614 Some(Value::Object(o)) => Ok(o),
615 Some(_) => Err(E::custom(format!("'{key}' field must be a JSON object"))),
616 }
617}
618
619fn take_bool(map: &mut Map<String, Value>, key: &str) -> bool {
621 map.remove(key).and_then(|v| v.as_bool()).unwrap_or(false)
622}
623
624fn take_u16(map: &mut Map<String, Value>, key: &str) -> u16 {
626 map.remove(key).and_then(|v| v.as_u64()).unwrap_or(0) as u16
627}
628
629fn take_attachments<E: serde::de::Error>(
635 map: &mut Map<String, Value>,
636) -> Result<Vec<FileAttachment>, E> {
637 match map.remove("a") {
638 None => Ok(Vec::new()),
639 Some(value) => serde_json::from_value(value).map_err(E::custom),
640 }
641}
642
643fn take_error<E: serde::de::Error>(
645 map: &mut Map<String, Value>,
646) -> Result<Option<ErrorPayload>, E> {
647 match map.remove("er") {
648 None => Ok(None),
649 Some(value) => serde_json::from_value(value).map(Some).map_err(E::custom),
650 }
651}
652
653impl<'de> Deserialize<'de> for PacketBody {
654 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
655 where
656 D: Deserializer<'de>,
657 {
658 let mut map = match Value::deserialize(deserializer)? {
661 Value::Object(map) => map,
662 _ => {
663 return Err(serde::de::Error::custom(
664 "packet body must be a JSON object",
665 ));
666 }
667 };
668
669 let k = map.get("k").and_then(|v| v.as_u64()).ok_or_else(|| {
670 serde::de::Error::custom("missing or non-numeric 'k' field in packet body")
671 })?;
672 let k = u8::try_from(k).map_err(|_| {
673 serde::de::Error::custom(format!("'k' value {k} is outside the valid range"))
674 })?;
675
676 match k {
677 K_API_REQUEST => Ok(Self::ApiRequest {
678 request_id: take_u64(&mut map, "i")?,
679 route: take_string(&mut map, "r")?,
680 params: map.remove("p").unwrap_or(Value::Null),
681 attachments: take_attachments(&mut map)?,
682 metadata: map.remove("m").unwrap_or(Value::Null),
683 }),
684 K_EVENT_EMIT => Ok(Self::EventEmit {
685 event_id: take_u64(&mut map, "i")?,
686 name: take_string(&mut map, "n")?,
687 data: take_object(&mut map, "d")?,
688 attachments: take_attachments(&mut map)?,
689 metadata: map.remove("m").unwrap_or(Value::Null),
690 expect_ack: take_bool(&mut map, "e"),
691 }),
692 K_API_RESPONSE => Ok(Self::ApiResponse {
693 request_id: take_u64(&mut map, "i")?,
694 ok: take_bool(&mut map, "o"),
695 status: take_u16(&mut map, "s"),
696 data: map.remove("d").unwrap_or(Value::Null),
697 error: take_error(&mut map)?,
698 metadata: map.remove("m").unwrap_or(Value::Null),
699 }),
700 K_EVENT_ACK => Ok(Self::EventAck {
701 event_id: take_u64(&mut map, "i")?,
702 ok: take_bool(&mut map, "o"),
703 receipt: map.remove("rc").unwrap_or(Value::Null),
704 error: take_error(&mut map)?,
705 }),
706 _ => Err(serde::de::Error::custom(format!("unknown 'k' value: {k}"))),
707 }
708 }
709}
710
711impl PacketBody {
712 pub fn message_type(&self) -> MessageType {
713 match self {
714 Self::ApiRequest { .. } | Self::ApiResponse { .. } => MessageType::Api,
715 Self::EventEmit { .. } | Self::EventAck { .. } => MessageType::Event,
716 }
717 }
718
719 pub fn attachments(&self) -> &[FileAttachment] {
721 match self {
722 Self::ApiRequest { attachments, .. } => attachments,
723 Self::EventEmit { attachments, .. } => attachments,
724 _ => &[],
725 }
726 }
727
728 pub fn set_attachments(&mut self, attachments: Vec<FileAttachment>) {
730 match self {
731 Self::ApiRequest { attachments: a, .. } => *a = attachments,
732 Self::EventEmit { attachments: a, .. } => *a = attachments,
733 _ => {}
734 }
735 }
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct PacketEnvelope {
741 pub message_type: MessageType,
743 pub encryption: EncryptionKind,
745 pub body: PacketBody,
747}
748
749impl PacketEnvelope {
750 pub fn new(body: PacketBody) -> Self {
752 Self {
753 message_type: body.message_type(),
754 encryption: EncryptionKind::None,
755 body,
756 }
757 }
758
759 pub fn with_encryption(body: PacketBody, encryption: EncryptionKind) -> Self {
761 Self {
762 message_type: body.message_type(),
763 encryption,
764 body,
765 }
766 }
767}
768
769#[derive(Clone)]
786pub struct FrameCodec {
787 aes256_cipher: Option<Arc<Aes256Gcm>>,
788 chacha20_cipher: Option<Arc<ChaCha20Poly1305>>,
789 wire_encryption: EncryptionKind,
791 max_frame_bytes: usize,
793}
794
795impl Default for FrameCodec {
796 fn default() -> Self {
797 Self {
798 aes256_cipher: None,
799 chacha20_cipher: None,
800 wire_encryption: EncryptionKind::None,
801 max_frame_bytes: DEFAULT_MAX_FRAME_BYTES,
802 }
803 }
804}
805
806impl std::fmt::Debug for FrameCodec {
807 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
808 f.debug_struct("FrameCodec")
809 .field("aes256", &self.aes256_cipher.is_some())
810 .field("chacha20", &self.chacha20_cipher.is_some())
811 .field("wire_encryption", &self.wire_encryption)
812 .field("max_frame_bytes", &self.max_frame_bytes)
813 .finish()
814 }
815}
816
817impl FrameCodec {
818 pub fn plaintext() -> Self {
820 Self::default()
821 }
822
823 pub fn with_chacha20_key(self, key: [u8; 32]) -> Self {
829 let cipher = ChaCha20Poly1305::new_from_slice(&key)
830 .expect("a 32-byte key is always valid for ChaCha20-Poly1305");
831 Self {
832 chacha20_cipher: Some(Arc::new(cipher)),
833 wire_encryption: EncryptionKind::ChaCha20,
834 ..self
835 }
836 }
837
838 pub fn with_aes256_key(self, key: [u8; 32]) -> Self {
844 let cipher =
845 Aes256Gcm::new_from_slice(&key).expect("a 32-byte key is always valid for AES-256-GCM");
846 Self {
847 aes256_cipher: Some(Arc::new(cipher)),
848 wire_encryption: EncryptionKind::Aes256,
849 ..self
850 }
851 }
852
853 pub fn wire_encryption(&self) -> EncryptionKind {
855 self.wire_encryption
856 }
857
858 pub fn with_max_frame_bytes(mut self, max: usize) -> Self {
862 self.max_frame_bytes = max;
863 self
864 }
865
866 pub fn max_frame_bytes(&self) -> usize {
868 self.max_frame_bytes
869 }
870
871 pub fn encode(&self, packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
883 let max_payload = self.max_frame_bytes.saturating_sub(5);
884 let attachments = packet.body.attachments();
885 let att_size: usize = attachments.iter().map(|a| a.wire_size()).sum();
886 let message_type = packet.message_type as u8;
887
888 if self.wire_encryption == EncryptionKind::None {
895 let mut frame = Vec::with_capacity(4 + 1 + 4 + 1 + att_size + 64);
896 frame.extend_from_slice(&[0u8; 4]); frame.push(message_type);
898 frame.extend_from_slice(&[0u8; 4]); serde_json::to_writer(&mut frame, &packet.body)?;
900 let json_len = frame.len() - 9;
901 frame.push(attachments.len() as u8);
902 for att in attachments {
903 att.write_wire(&mut frame);
904 }
905 let payload_len = frame.len() - 5;
906 if payload_len > max_payload {
907 return Err(ProtocolError::PayloadTooLarge {
908 actual: payload_len,
909 max: max_payload,
910 });
911 }
912 let frame_len = (frame.len() - 4) as u32;
913 frame[0..4].copy_from_slice(&frame_len.to_be_bytes());
914 frame[5..9].copy_from_slice(&(json_len as u32).to_be_bytes());
915 return Ok(frame);
916 }
917
918 let mut composite = Vec::with_capacity(4 + 1 + att_size + 64);
922 composite.extend_from_slice(&[0u8; 4]); serde_json::to_writer(&mut composite, &packet.body)?;
924 let json_len = composite.len() - 4;
925 composite[0..4].copy_from_slice(&(json_len as u32).to_be_bytes());
926 composite.push(attachments.len() as u8);
927 for att in attachments {
928 att.write_wire(&mut composite);
929 }
930
931 if composite.len() > max_payload {
933 return Err(ProtocolError::PayloadTooLarge {
934 actual: composite.len(),
935 max: max_payload,
936 });
937 }
938
939 let payload = match self.wire_encryption {
940 EncryptionKind::ChaCha20 => self.encrypt_chacha20(&composite)?,
941 EncryptionKind::Aes256 => self.encrypt_aes256(&composite)?,
942 EncryptionKind::None => unreachable!("plaintext is handled by the fast path above"),
943 };
944
945 if payload.len() > max_payload {
947 return Err(ProtocolError::PayloadTooLarge {
948 actual: payload.len(),
949 max: max_payload,
950 });
951 }
952
953 let frame_len = 1 + payload.len();
954 let mut frame = Vec::with_capacity(4 + frame_len);
955 frame.extend_from_slice(&(frame_len as u32).to_be_bytes());
956 frame.push(message_type);
957 frame.extend_from_slice(&payload);
958 Ok(frame)
959 }
960
961 pub fn decode(&self, frame: Bytes) -> Result<PacketEnvelope, ProtocolError> {
970 if frame.len() < 5 {
971 return Err(ProtocolError::FrameTooShort);
972 }
973
974 let declared = u32::from_be_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
975 let actual = frame.len() - 4;
976 if declared != actual {
977 return Err(ProtocolError::FrameLengthMismatch { declared, actual });
978 }
979
980 if frame.len() > self.max_frame_bytes {
981 return Err(ProtocolError::FrameTooLarge {
982 actual: frame.len(),
983 max: self.max_frame_bytes,
984 });
985 }
986
987 let message_type = MessageType::try_from(frame[4])?;
988
989 let composite: Bytes = match self.wire_encryption {
993 EncryptionKind::None => frame.slice(5..),
994 EncryptionKind::ChaCha20 => Bytes::from(self.decrypt_chacha20(&frame[5..])?),
995 EncryptionKind::Aes256 => Bytes::from(self.decrypt_aes256(&frame[5..])?),
996 };
997
998 if composite.len() < 5 {
1000 return Err(ProtocolError::FrameTooShort);
1001 }
1002 let meta_len =
1003 u32::from_be_bytes([composite[0], composite[1], composite[2], composite[3]]) as usize;
1004 if composite.len() < 4 + meta_len + 1 {
1005 return Err(ProtocolError::FrameTooShort);
1006 }
1007 let json_slice = &composite[4..4 + meta_len];
1008 let att_count = composite[4 + meta_len] as usize;
1009 let mut att_data = composite.slice(4 + meta_len + 1..);
1010
1011 let mut attachments = Vec::with_capacity(att_count);
1013 for _ in 0..att_count {
1014 let (att, rest) = FileAttachment::read_wire(&att_data)?;
1015 attachments.push(att);
1016 att_data = rest;
1017 }
1018
1019 let mut body: PacketBody = serde_json::from_slice(json_slice)?;
1021 body.set_attachments(attachments);
1022
1023 if body.message_type() != message_type {
1024 return Err(ProtocolError::MessageTypeMismatch);
1025 }
1026
1027 Ok(PacketEnvelope {
1028 message_type,
1029 encryption: self.wire_encryption,
1030 body,
1031 })
1032 }
1033
1034 fn encrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
1035 let cipher = self
1036 .chacha20_cipher
1037 .as_ref()
1038 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
1039 let mut nonce_bytes = [0_u8; CHACHA20_NONCE_LEN];
1040 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
1041 let ciphertext = cipher
1042 .encrypt(Nonce::from_slice(&nonce_bytes), payload)
1043 .map_err(|_| ProtocolError::EncryptionFailed("chacha20"))?;
1044
1045 let mut encoded = Vec::with_capacity(CHACHA20_NONCE_LEN + ciphertext.len());
1046 encoded.extend_from_slice(&nonce_bytes);
1047 encoded.extend_from_slice(&ciphertext);
1048 Ok(encoded)
1049 }
1050
1051 fn decrypt_chacha20(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
1052 if payload.len() < CHACHA20_NONCE_LEN {
1053 return Err(ProtocolError::EncryptedPayloadTooShort {
1054 algorithm: "chacha20",
1055 expected_min: CHACHA20_NONCE_LEN,
1056 actual: payload.len(),
1057 });
1058 }
1059
1060 let cipher = self
1061 .chacha20_cipher
1062 .as_ref()
1063 .ok_or(ProtocolError::MissingEncryptionKey("chacha20"))?;
1064 let (nonce_bytes, ciphertext) = payload.split_at(CHACHA20_NONCE_LEN);
1065 cipher
1066 .decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
1067 .map_err(|_| ProtocolError::DecryptionFailed("chacha20"))
1068 }
1069
1070 fn encrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
1071 let cipher = self
1072 .aes256_cipher
1073 .as_ref()
1074 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
1075 let mut nonce_bytes = [0_u8; AES256_NONCE_LEN];
1076 getrandom(&mut nonce_bytes).map_err(|source| ProtocolError::Random(source.to_string()))?;
1077 let ciphertext = cipher
1078 .encrypt(AesNonce::from_slice(&nonce_bytes), payload)
1079 .map_err(|_| ProtocolError::EncryptionFailed("aes256"))?;
1080
1081 let mut encoded = Vec::with_capacity(AES256_NONCE_LEN + ciphertext.len());
1082 encoded.extend_from_slice(&nonce_bytes);
1083 encoded.extend_from_slice(&ciphertext);
1084 Ok(encoded)
1085 }
1086
1087 fn decrypt_aes256(&self, payload: &[u8]) -> Result<Vec<u8>, ProtocolError> {
1088 if payload.len() < AES256_NONCE_LEN {
1089 return Err(ProtocolError::EncryptedPayloadTooShort {
1090 algorithm: "aes256",
1091 expected_min: AES256_NONCE_LEN,
1092 actual: payload.len(),
1093 });
1094 }
1095
1096 let cipher = self
1097 .aes256_cipher
1098 .as_ref()
1099 .ok_or(ProtocolError::MissingEncryptionKey("aes256"))?;
1100 let (nonce_bytes, ciphertext) = payload.split_at(AES256_NONCE_LEN);
1101 cipher
1102 .decrypt(AesNonce::from_slice(nonce_bytes), ciphertext)
1103 .map_err(|_| ProtocolError::DecryptionFailed("aes256"))
1104 }
1105}
1106
1107pub fn encode_frame(packet: &PacketEnvelope) -> Result<Vec<u8>, ProtocolError> {
1109 FrameCodec::plaintext().encode(packet)
1110}
1111
1112pub fn decode_frame(frame: Bytes) -> Result<PacketEnvelope, ProtocolError> {
1114 FrameCodec::plaintext().decode(frame)
1115}
1116
1117#[derive(Debug, Error)]
1119pub enum ProtocolError {
1120 #[error("frame too short")]
1121 FrameTooShort,
1122 #[error("frame length mismatch: declared={declared}, actual={actual}")]
1123 FrameLengthMismatch { declared: usize, actual: usize },
1124 #[error("payload too large: actual={actual}, max={max}")]
1125 PayloadTooLarge { actual: usize, max: usize },
1126 #[error("frame too large: actual={actual}, max={max}")]
1127 FrameTooLarge { actual: usize, max: usize },
1128 #[error("unknown message type: {0:#x}")]
1129 UnknownMessageType(u8),
1130 #[error("unknown encryption kind: {0:#x}")]
1131 UnknownEncryption(u8),
1132 #[error("unsupported encryption kind: {0:#x}")]
1133 UnsupportedEncryption(u8),
1134 #[error("missing encryption key for {0}")]
1135 MissingEncryptionKey(&'static str),
1136 #[error("invalid encryption key for {0}")]
1137 InvalidEncryptionKey(&'static str),
1138 #[error("secure random generation failed: {0}")]
1139 Random(String),
1140 #[error(
1141 "encrypted payload too short for {algorithm}: expected at least {expected_min}, actual={actual}"
1142 )]
1143 EncryptedPayloadTooShort {
1144 algorithm: &'static str,
1145 expected_min: usize,
1146 actual: usize,
1147 },
1148 #[error("encryption failed for {0}")]
1149 EncryptionFailed(&'static str),
1150 #[error("decryption failed for {0}")]
1151 DecryptionFailed(&'static str),
1152 #[error("invalid ECDH public key: expected {expected} bytes, got {actual}")]
1153 InvalidEcdhPublicKey { expected: usize, actual: usize },
1154 #[error("ECDH handshake failed: {0}")]
1155 EcdhHandshake(String),
1156 #[error("message type does not match packet body")]
1157 MessageTypeMismatch,
1158 #[error("invalid attachment encoding: {0}")]
1159 InvalidAttachmentEncoding(String),
1160 #[error("json error: {0}")]
1161 Json(#[from] serde_json::Error),
1162}
1163
1164#[cfg(test)]
1165mod tests {
1166 use super::{
1167 Bytes, EncryptionKind, FileAttachment, FrameCodec, PacketBody, PacketEnvelope,
1168 ProtocolError, decode_frame, encode_frame,
1169 };
1170 use serde_json::json;
1171
1172 const TEST_KEY: [u8; 32] = [0x11; 32];
1173
1174 #[test]
1175 fn plaintext_helpers_still_work() {
1176 let packet = PacketEnvelope::new(PacketBody::EventAck {
1177 event_id: 1,
1178 ok: true,
1179 receipt: json!({ "ok": true }),
1180 error: None,
1181 });
1182
1183 let encoded = encode_frame(&packet).expect("encode plaintext");
1184 let decoded = decode_frame(Bytes::from(encoded)).expect("decode plaintext");
1185 assert!(matches!(decoded.encryption, EncryptionKind::None));
1186 }
1187
1188 #[test]
1189 fn aes256_roundtrip_works() {
1190 let codec = FrameCodec::plaintext().with_aes256_key(TEST_KEY);
1191 let packet = PacketEnvelope::with_encryption(
1192 PacketBody::ApiResponse {
1193 request_id: 1,
1194 ok: true,
1195 status: 200,
1196 data: json!({ "message": "encrypted" }),
1197 error: None,
1198 metadata: json!({}),
1199 },
1200 EncryptionKind::Aes256,
1201 );
1202
1203 let encoded = codec.encode(&packet).expect("encode aes256");
1204 let decoded = codec.decode(Bytes::from(encoded)).expect("decode aes256");
1205 assert!(matches!(decoded.encryption, EncryptionKind::Aes256));
1206 }
1207
1208 #[test]
1209 fn attachment_roundtrip_plaintext() {
1210 let att = FileAttachment::inline_bytes(
1211 "f1",
1212 "test.bin",
1213 "application/octet-stream",
1214 vec![1, 2, 3, 4, 5],
1215 );
1216 let packet = PacketEnvelope::new(PacketBody::ApiRequest {
1217 request_id: 42,
1218 route: "files.upload".to_string(),
1219 params: json!({ "file": { "$file": "f1" } }),
1220 attachments: vec![att],
1221 metadata: json!({}),
1222 });
1223
1224 let encoded = encode_frame(&packet).expect("encode with attachment");
1225 let decoded = decode_frame(Bytes::from(encoded)).expect("decode with attachment");
1226
1227 let atts = decoded.body.attachments();
1228 assert_eq!(atts.len(), 1);
1229 assert_eq!(atts[0].id, "f1");
1230 assert_eq!(atts[0].name, "test.bin");
1231 assert_eq!(atts[0].content_type, "application/octet-stream");
1232 assert_eq!(&atts[0].data[..], &[1, 2, 3, 4, 5]);
1233 }
1234
1235 #[test]
1236 fn attachment_roundtrip_encrypted() {
1237 let codec = FrameCodec::plaintext().with_chacha20_key(TEST_KEY);
1238 let att = FileAttachment::inline_text("f2", "hello.txt", "text/plain", "hello world");
1239 let packet = PacketEnvelope::with_encryption(
1240 PacketBody::EventEmit {
1241 event_id: 7,
1242 name: "chat.message".to_string(),
1243 data: json!({ "text": "see attached" })
1244 .as_object()
1245 .unwrap()
1246 .clone(),
1247 attachments: vec![att],
1248 metadata: json!({}),
1249 expect_ack: true,
1250 },
1251 EncryptionKind::ChaCha20,
1252 );
1253
1254 let encoded = codec
1255 .encode(&packet)
1256 .expect("encode encrypted with attachment");
1257 let decoded = codec
1258 .decode(Bytes::from(encoded))
1259 .expect("decode encrypted with attachment");
1260
1261 let atts = decoded.body.attachments();
1262 assert_eq!(atts.len(), 1);
1263 assert_eq!(atts[0].id, "f2");
1264 assert_eq!(&atts[0].data[..], b"hello world");
1265 }
1266
1267 #[test]
1268 fn encode_rejects_payloads_over_limit() {
1269 let codec = FrameCodec::plaintext().with_max_frame_bytes(1024);
1271 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
1272 request_id: 999,
1273 ok: true,
1274 status: 200,
1275 data: json!({ "blob": "a".repeat(2048) }),
1276 error: None,
1277 metadata: json!({}),
1278 });
1279
1280 let error = codec
1281 .encode(&packet)
1282 .expect_err("oversized payload should fail");
1283 assert!(matches!(error, ProtocolError::PayloadTooLarge { .. }));
1284 }
1285
1286 #[test]
1287 fn decode_rejects_frames_over_limit() {
1288 let codec = FrameCodec::plaintext().with_max_frame_bytes(64);
1289 let packet = PacketEnvelope::new(PacketBody::ApiResponse {
1291 request_id: 1,
1292 ok: true,
1293 status: 200,
1294 data: json!({ "msg": "a]".repeat(50) }),
1295 error: None,
1296 metadata: json!({}),
1297 });
1298 let encoded = FrameCodec::plaintext()
1299 .encode(&packet)
1300 .expect("encode with default limit");
1301 assert!(encoded.len() > 64);
1302
1303 let error = codec
1304 .decode(Bytes::from(encoded))
1305 .expect_err("oversized frame should fail");
1306 assert!(matches!(error, ProtocolError::FrameTooLarge { .. }));
1307 }
1308}