pamoja_update/block.rs
1//! Carrying a signed update inside one data block, for a transport that moves blocks.
2//!
3//! A fragmented transport such as LoRaWAN's TS004 hands a device one block of bytes and says
4//! nothing about what is in it: the specification leaves that to the vendor, and gives the
5//! server a four-byte descriptor to name whatever convention the two ends share. This is that
6//! convention for a pamoja update.
7//!
8//! A block is the signed manifest and the image it describes, one after the other, behind a
9//! short header that says which is which:
10//!
11//! ```text
12//! "PJU1" | envelope length, two bytes | envelope | image
13//! ```
14//!
15//! Nothing here is trusted. The header only says where the two halves are; every rule that
16//! decides whether the image runs is the manifest's, checked by the updater as always, so a
17//! block that arrives over a broadcast nobody authenticated is no more dangerous than one
18//! fetched over a wire.
19//!
20//! # Examples
21//!
22//! A server frames an update, and a device stages what came out of its transport:
23//!
24//! ```
25//! use pamoja_update::block::{frame, split, DESCRIPTOR};
26//!
27//! let envelope = b"a signed manifest";
28//! let image = b"the firmware";
29//! let mut block = [0u8; 64];
30//! let len = frame(envelope, image, &mut block)?;
31//!
32//! let (read_envelope, read_image) = split(&block[..len])?;
33//! assert_eq!(read_envelope, envelope);
34//! assert_eq!(read_image, image);
35//! assert_eq!(&DESCRIPTOR, b"PJU1", "what the transport calls this block");
36//! # Ok::<(), pamoja_update::Refusal>(())
37//! ```
38
39use crate::{Refusal, Result};
40
41/// What a transport calls a block in this shape, for a field that names the convention.
42pub const DESCRIPTOR: [u8; 4] = *b"PJU1";
43
44/// The bytes the header takes: the descriptor and the envelope length.
45pub const HEADER_LEN: usize = 6;
46
47/// Writes a signed update into one block.
48///
49/// # Arguments
50///
51/// * `envelope` - the signed manifest.
52/// * `image` - the image it describes.
53/// * `out` - where to write the block.
54///
55/// # Returns
56///
57/// How many bytes the block takes.
58///
59/// # Errors
60///
61/// Returns [`Refusal::Size`] when the envelope is longer than two bytes can say, or when the
62/// buffer is too small for the block.
63pub fn frame(envelope: &[u8], image: &[u8], out: &mut [u8]) -> Result<usize> {
64 let len = HEADER_LEN + envelope.len() + image.len();
65 if envelope.len() > u16::MAX as usize || out.len() < len {
66 return Err(Refusal::Size);
67 }
68 out[..4].copy_from_slice(&DESCRIPTOR);
69 out[4..6].copy_from_slice(&(envelope.len() as u16).to_le_bytes());
70 out[6..6 + envelope.len()].copy_from_slice(envelope);
71 out[6 + envelope.len()..len].copy_from_slice(image);
72 Ok(len)
73}
74
75/// Reads a block back into the signed manifest and the image.
76///
77/// # Arguments
78///
79/// * `block` - the block a transport reassembled, with any padding already removed.
80///
81/// # Returns
82///
83/// The envelope and the image, borrowed from the block.
84///
85/// # Errors
86///
87/// Returns [`Refusal::Malformed`] when the block does not carry this convention's header, and
88/// [`Refusal::Size`] when the header says the envelope is longer than the block.
89pub fn split(block: &[u8]) -> Result<(&[u8], &[u8])> {
90 let header = block.get(..HEADER_LEN).ok_or(Refusal::Malformed)?;
91 if header[..4] != DESCRIPTOR {
92 return Err(Refusal::Malformed);
93 }
94 let len = usize::from(u16::from_le_bytes([header[4], header[5]]));
95 let envelope = block
96 .get(HEADER_LEN..HEADER_LEN + len)
97 .ok_or(Refusal::Size)?;
98 Ok((envelope, &block[HEADER_LEN + len..]))
99}