Skip to main content

moq_secure/
wire.rs

1use crate::crypto::{aead_decrypt, aead_encrypt, sha256_digest};
2use crate::error::MoqSecureError;
3use crate::key_store::KeyStore;
4use ed25519_dalek::{Signer, Verifier};
5
6pub const MAGIC: [u8; 4] = *b"MOQS";
7pub const VERSION: u8 = 1;
8
9pub const SIG_SLOT_LEN: usize = 64;
10pub const AEAD_TAG_LEN: usize = 16;
11pub const PAD_LEN_FIELD_LEN: usize = 4;
12
13// magic(4) | version(1) | key_id(1) | ctr(8) |
14// n_signed(1) | sig_flag(1) | encrypted(1)
15pub const FIXED_HEADER_LEN: usize = 4 + 1 + 1 + 8 + 1 + 1 + 1;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct WireHeader {
19    pub magic: [u8; 4],
20    pub version: u8,
21    pub key_id: u8,
22    pub ctr: u64,
23    pub n_signed: u8,
24    pub sig_flag: u8,
25    pub encrypted: u8,
26}
27
28impl WireHeader {
29    pub fn encode(&self) -> Vec<u8> {
30        let mut v = Vec::with_capacity(FIXED_HEADER_LEN);
31
32        v.extend_from_slice(&self.magic);
33        v.push(self.version);
34        v.push(self.key_id);
35        v.extend_from_slice(&self.ctr.to_be_bytes());
36        v.push(self.n_signed);
37        v.push(self.sig_flag);
38        v.push(self.encrypted);
39
40        v
41    }
42
43    pub fn aad(&self) -> Vec<u8> {
44        self.encode()
45    }
46
47    pub fn validate(&self) -> Result<(), MoqSecureError> {
48        if self.magic != MAGIC {
49            return Err(MoqSecureError::InvalidMagic);
50        }
51
52        if self.version != VERSION {
53            return Err(MoqSecureError::UnsupportedVersion(self.version));
54        }
55
56        if self.sig_flag != 0 && self.sig_flag != 1 {
57            return Err(MoqSecureError::InvalidSigFlag(self.sig_flag));
58        }
59
60        if self.encrypted != 0 && self.encrypted != 1 {
61            return Err(MoqSecureError::InvalidEncryptedFlag(self.encrypted));
62        }
63
64        if self.n_signed == 0 && self.sig_flag != 0 {
65            return Err(MoqSecureError::SigningMismatch);
66        }
67
68        Ok(())
69    }
70}
71
72#[derive(Debug, Clone)]
73pub struct Frame {
74    pub header: WireHeader,
75
76    // For encrypted frames, payload is ciphertext without the AEAD tag.
77    // For unencrypted frames, payload is:
78    // padLen(4-byte big endian) || padding || plaintext.
79    pub payload: Vec<u8>,
80
81    pub tag: [u8; AEAD_TAG_LEN],
82    pub signature: Option<[u8; SIG_SLOT_LEN]>,
83}
84
85impl Frame {
86    pub fn parse(frame: &[u8]) -> Result<Self, MoqSecureError> {
87        if frame.len() < FIXED_HEADER_LEN {
88            return Err(MoqSecureError::TruncatedFrame);
89        }
90
91        let (h_bytes, rest0) = frame.split_at(FIXED_HEADER_LEN);
92        let mut idx = 0;
93
94        let mut magic = [0u8; 4];
95        magic.copy_from_slice(&h_bytes[idx..idx + 4]);
96        idx += 4;
97
98        let version = h_bytes[idx];
99        idx += 1;
100
101        let key_id = h_bytes[idx];
102        idx += 1;
103
104        let mut ctr_bytes = [0u8; 8];
105        ctr_bytes.copy_from_slice(&h_bytes[idx..idx + 8]);
106        idx += 8;
107
108        let ctr = u64::from_be_bytes(ctr_bytes);
109
110        let n_signed = h_bytes[idx];
111        idx += 1;
112
113        let sig_flag = h_bytes[idx];
114        idx += 1;
115
116        let encrypted = h_bytes[idx];
117
118        let header = WireHeader {
119            magic,
120            version,
121            key_id,
122            ctr,
123            n_signed,
124            sig_flag,
125            encrypted,
126        };
127
128        header.validate()?;
129
130        let sig_len = if header.sig_flag == 1 {
131            SIG_SLOT_LEN
132        } else {
133            0
134        };
135
136        if rest0.len() < sig_len {
137            return Err(MoqSecureError::TruncatedFrame);
138        }
139
140        let (payload_and_tag, signature_bytes) = if sig_len == 0 {
141            (rest0, None)
142        } else {
143            let split_at = rest0.len() - SIG_SLOT_LEN;
144            let (payload, signature) = rest0.split_at(split_at);
145            (payload, Some(signature))
146        };
147
148        let signature = if let Some(sig_bytes) = signature_bytes {
149            if sig_bytes.len() != SIG_SLOT_LEN {
150                return Err(MoqSecureError::TruncatedFrame);
151            }
152
153            let mut sig = [0u8; SIG_SLOT_LEN];
154            sig.copy_from_slice(sig_bytes);
155
156            if sig == [0u8; SIG_SLOT_LEN] {
157                return Err(MoqSecureError::InvalidSignature);
158            }
159
160            Some(sig)
161        } else {
162            None
163        };
164
165        if header.encrypted == 1 {
166            if payload_and_tag.len() < AEAD_TAG_LEN {
167                return Err(MoqSecureError::CiphertextTooShort);
168            }
169
170            let split_at = payload_and_tag.len() - AEAD_TAG_LEN;
171            let ciphertext = &payload_and_tag[..split_at];
172            let tag_bytes = &payload_and_tag[split_at..];
173
174            let tag: [u8; AEAD_TAG_LEN] = tag_bytes
175                .try_into()
176                .map_err(|_| MoqSecureError::CiphertextTooShort)?;
177
178            Ok(Self {
179                header,
180                payload: ciphertext.to_vec(),
181                tag,
182                signature,
183            })
184        } else {
185            Ok(Self {
186                header,
187                payload: payload_and_tag.to_vec(),
188                tag: [0u8; AEAD_TAG_LEN],
189                signature,
190            })
191        }
192    }
193
194    pub fn serialize(&self) -> Vec<u8> {
195        let mut out = Vec::new();
196
197        out.extend_from_slice(&self.header.encode());
198
199        if self.header.encrypted == 1 {
200            out.extend_from_slice(&self.payload);
201            out.extend_from_slice(&self.tag);
202        } else {
203            out.extend_from_slice(&self.payload);
204        }
205
206        if self.header.sig_flag == 1 {
207            if let Some(signature) = self.signature {
208                out.extend_from_slice(&signature);
209            } else {
210                out.extend_from_slice(&[0u8; SIG_SLOT_LEN]);
211            }
212        }
213
214        out
215    }
216
217    pub fn aad_bytes(&self) -> Vec<u8> {
218        self.header.aad()
219    }
220
221    pub fn digest_for_signature(&self) -> [u8; 32] {
222        let header_bytes = self.header.encode();
223
224        let mut data = Vec::with_capacity(
225            header_bytes.len()
226                + self.payload.len()
227                + if self.header.encrypted == 1 {
228                    AEAD_TAG_LEN
229                } else {
230                    0
231                },
232        );
233
234        data.extend_from_slice(&header_bytes);
235        data.extend_from_slice(&self.payload);
236
237        if self.header.encrypted == 1 {
238            data.extend_from_slice(&self.tag);
239        }
240
241        sha256_digest(&data)
242    }
243
244    pub fn decode_plaintext_with_key_store(
245        &self,
246        key_store: &dyn KeyStore,
247        broadcaster_public_key: &ed25519_dalek::VerifyingKey,
248        lease_remaining: &mut u8,
249    ) -> Result<Vec<u8>, MoqSecureError> {
250        let signing_enabled = self.header.n_signed > 0;
251        let signed = self.header.sig_flag == 1;
252
253        /*
254         * Validate the signing state and verify signatures first.
255         *
256         * The lease is intentionally not modified here. It is updated
257         * only after decryption and padding validation succeed.
258         */
259        let next_lease_remaining = if !signing_enabled {
260            if self.header.sig_flag != 0 || self.signature.is_some() {
261                return Err(MoqSecureError::SigningMismatch);
262            }
263
264            // Signing is disabled; the lease mechanism does not apply.
265            None
266        } else if signed {
267            let signature_bytes = self
268                .signature
269                .ok_or(MoqSecureError::InvalidSignature)?;
270
271            let signature =
272                ed25519_dalek::Signature::from_bytes(&signature_bytes);
273
274            let digest = self.digest_for_signature();
275
276            broadcaster_public_key
277                .verify(&digest, &signature)
278                .map_err(|_| MoqSecureError::InvalidSignature)?;
279
280            /*
281             * The current frame is signed, so it does not consume an
282             * unsigned-frame lease slot. Only n_signed - 1 subsequent
283             * unsigned frames may be accepted.
284             */
285            Some(self.header.n_signed.saturating_sub(1))
286        } else {
287            /*
288             * An unsigned frame requires a positive lease. The current
289             * frame consumes one lease slot.
290             */
291            if *lease_remaining == 0 {
292                return Err(MoqSecureError::InvalidSignature);
293            }
294
295            Some(lease_remaining.saturating_sub(1))
296        };
297
298        let padded_plaintext = if self.header.encrypted == 1 {
299            let key = key_store
300                .aead_key(self.header.key_id)
301                .ok_or(MoqSecureError::InvalidKeyId(self.header.key_id))?;
302
303            aead_decrypt(
304                key,
305                self.header.key_id,
306                self.header.ctr,
307                &self.aad_bytes(),
308                &self.payload,
309                &self.tag,
310            )?
311        } else {
312            self.payload.clone()
313        };
314
315        if padded_plaintext.len() < PAD_LEN_FIELD_LEN {
316            return Err(MoqSecureError::InvalidPadLength);
317        }
318
319        let mut pad_len_bytes = [0u8; PAD_LEN_FIELD_LEN];
320        pad_len_bytes.copy_from_slice(&padded_plaintext[..PAD_LEN_FIELD_LEN]);
321
322        let pad_len = u32::from_be_bytes(pad_len_bytes) as usize;
323
324        let content_start = PAD_LEN_FIELD_LEN
325            .checked_add(pad_len)
326            .ok_or(MoqSecureError::InvalidPadLength)?;
327
328        if content_start > padded_plaintext.len() {
329            return Err(MoqSecureError::InvalidPadLength);
330        }
331
332        let plaintext = padded_plaintext[content_start..].to_vec();
333
334        /*
335         * Commit the lease update only after the frame has been fully
336         * authenticated, decrypted, and padding-validated.
337         *
338         * When signing is disabled, the lease mechanism does not apply,
339         * so the existing lease value is left unchanged.
340         */
341        if let Some(next) = next_lease_remaining {
342            *lease_remaining = next;
343        }
344
345        Ok(plaintext)
346    }
347}
348
349pub fn encrypt_frame(
350    key_store: &dyn KeyStore,
351    broadcaster_private_key: &ed25519_dalek::SigningKey,
352    key_id: u8,
353    ctr: u64,
354    n_signed: u8,
355    maybe_sign: bool,
356    encrypted: u8,
357    pad_len: u32,
358    plaintext: &[u8],
359) -> Result<Frame, MoqSecureError> {
360    if encrypted != 0 && encrypted != 1 {
361        return Err(MoqSecureError::InvalidEncryptedFlag(encrypted));
362    }
363
364    let sig_flag = if n_signed != 0 && maybe_sign {
365        1
366    } else {
367        0
368    };
369
370    let header = WireHeader {
371        magic: MAGIC,
372        version: VERSION,
373        key_id,
374        ctr,
375        n_signed,
376        sig_flag,
377        encrypted,
378    };
379
380    header.validate()?;
381
382    let pad_len_usize = pad_len as usize;
383
384    let mut padded_plaintext = Vec::with_capacity(
385        PAD_LEN_FIELD_LEN + pad_len_usize + plaintext.len(),
386    );
387
388    padded_plaintext.extend_from_slice(&pad_len.to_be_bytes());
389    padded_plaintext.resize(PAD_LEN_FIELD_LEN + pad_len_usize, 0);
390    padded_plaintext.extend_from_slice(plaintext);
391
392    let frame_without_signature = if encrypted == 1 {
393        let key = key_store
394            .aead_key(key_id)
395            .ok_or(MoqSecureError::InvalidKeyId(key_id))?;
396
397        let (ciphertext, tag) = aead_encrypt(
398            key,
399            key_id,
400            ctr,
401            &header.aad(),
402            &padded_plaintext,
403        );
404
405        Frame {
406            header,
407            payload: ciphertext,
408            tag,
409            signature: None,
410        }
411    } else {
412        Frame {
413            header,
414            payload: padded_plaintext,
415            tag: [0u8; AEAD_TAG_LEN],
416            signature: None,
417        }
418    };
419
420    if sig_flag == 1 {
421        let digest = frame_without_signature.digest_for_signature();
422        let signature = broadcaster_private_key.sign(&digest);
423
424        Ok(Frame {
425            signature: Some(signature.to_bytes()),
426            ..frame_without_signature
427        })
428    } else {
429        Ok(frame_without_signature)
430    }
431}
432
433pub fn decrypt_frame(
434    key_store: &dyn KeyStore,
435    broadcaster_public_key: &ed25519_dalek::VerifyingKey,
436    lease_remaining: &mut u8,
437    frame_bytes: &[u8],
438) -> Result<Vec<u8>, MoqSecureError> {
439    let frame = Frame::parse(frame_bytes)?;
440
441    frame.decode_plaintext_with_key_store(
442        key_store,
443        broadcaster_public_key,
444        lease_remaining,
445    )
446}