Skip to main content

aven_protocol/tail/
envelope.rs

1//! Keyless envelope/projection parsing. Parsing is not device authorization.
2use crate::codec::bytes;
3use crate::wire::tail::RECORD_LIMIT;
4use anyhow::{Result, ensure};
5const BASE32: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
6pub const MAX_MOVE_TASKS: usize = 256;
7fn valid(ok: bool) -> Result<()> {
8    ensure!(ok, "error encrypted-tail-invalid");
9    Ok(())
10}
11fn encode_task(bytes: &[u8]) -> String {
12    let mut value = 0_u128;
13    for byte in bytes {
14        value = (value << 8) | u128::from(*byte);
15    }
16    (0..16)
17        .rev()
18        .map(|shift| BASE32[((value >> (shift * 5)) & 31) as usize] as char)
19        .collect()
20}
21#[derive(Clone, PartialEq, Eq)]
22pub enum Projection {
23    None,
24    Move {
25        source: String,
26        target: String,
27        tasks: Vec<String>,
28    },
29    Ref {
30        workspace: String,
31        task: String,
32        reference: String,
33        descriptor: Vec<u8>,
34        deleted: bool,
35        version: Option<String>,
36    },
37    Unref {
38        workspace: String,
39        task: String,
40        reference: String,
41    },
42    Parent {
43        action: super::parent::ParentAction,
44        workspace: String,
45        task: String,
46        deleted: bool,
47        version: Option<String>,
48    },
49}
50impl Projection {
51    pub fn encode(&self) -> Vec<u8> {
52        match self {
53            Self::Move {
54                source,
55                target,
56                tasks,
57            } => {
58                let mut out = vec![4];
59                encode_text(&mut out, source);
60                encode_text(&mut out, target);
61                out.extend((tasks.len() as u16).to_be_bytes());
62                for task in tasks {
63                    let mut bits = 0_u128;
64                    for byte in task.bytes() {
65                        bits = (bits << 5)
66                            | BASE32
67                                .iter()
68                                .position(|v| *v == byte)
69                                .expect("validated task ID") as u128;
70                    }
71                    out.extend_from_slice(&bits.to_be_bytes()[6..]);
72                }
73                return out;
74            }
75            Self::Ref {
76                workspace,
77                task,
78                reference,
79                descriptor,
80                deleted,
81                version,
82            } => {
83                let mut out = vec![2];
84                for text in [workspace, task, reference] {
85                    encode_text(&mut out, text);
86                }
87                out.extend((descriptor.len() as u32).to_be_bytes());
88                out.extend(descriptor);
89                out.push(u8::from(*deleted));
90                out.push(u8::from(version.is_some()));
91                if let Some(v) = version {
92                    encode_text(&mut out, v);
93                }
94                return out;
95            }
96            Self::Unref {
97                workspace,
98                task,
99                reference,
100            } => {
101                let mut out = vec![3];
102                for text in [workspace, task, reference] {
103                    encode_text(&mut out, text);
104                }
105                return out;
106            }
107            _ => {}
108        }
109        let Self::Parent {
110            action,
111            workspace,
112            task,
113            deleted,
114            version,
115        } = self
116        else {
117            return vec![0];
118        };
119        let mut out = vec![1, *action as u8];
120        encode_text(&mut out, workspace);
121        encode_text(&mut out, task);
122        out.push(u8::from(*deleted));
123        out.push(u8::from(version.is_some()));
124        if let Some(v) = version {
125            encode_text(&mut out, v);
126        }
127        out
128    }
129    pub fn decode(input: &[u8]) -> Result<Self> {
130        decode_projection(input)
131    }
132}
133
134struct Reader<'a>(&'a [u8]);
135impl<'a> Reader<'a> {
136    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
137        valid(n <= self.0.len())?;
138        let (a, b) = self.0.split_at(n);
139        self.0 = b;
140        Ok(a)
141    }
142    fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
143        Ok(self.take(N)?.try_into()?)
144    }
145    fn blob(&mut self, max: usize) -> Result<&'a [u8]> {
146        let n = u32::from_be_bytes(self.array()?) as usize;
147        valid(n <= max)?;
148        self.take(n)
149    }
150}
151pub struct Envelope<'a> {
152    pub vault: [u8; 32],
153    pub stream: [u8; 32],
154    pub generation: [u8; 32],
155    pub id: String,
156    pub projection: Projection,
157    pub header: &'a [u8],
158    pub body: &'a [u8],
159    pub nonce: [u8; 24],
160}
161pub fn parse(record: &[u8]) -> Result<Envelope<'_>> {
162    valid(record.len() <= RECORD_LIMIT)?;
163    let mut r = Reader(record);
164    let header = r.blob(4544)?;
165    let body = r.blob(131072 + 16)?;
166    valid(r.0.is_empty() && body.len() >= 16)?;
167    let mut r = Reader(header);
168    valid(r.take(8)? == b"AVEN\x01\x00\x01\x01")?;
169    let vault = r.blob(32)?.try_into()?;
170    let stream = r.blob(32)?.try_into()?;
171    let generation = r.blob(32)?.try_into()?;
172    valid(r.blob(32)?.len() == 32)?;
173    let id = std::str::from_utf8(r.blob(256)?)?.to_owned();
174    valid(!id.is_empty())?;
175    valid(r.array::<4>()? == 18u32.to_be_bytes())?;
176    let projection = Projection::decode(r.blob(4096)?)?;
177    let nonce = r.blob(24)?.try_into()?;
178    valid(r.0.is_empty())?;
179    Ok(Envelope {
180        vault,
181        stream,
182        generation,
183        id,
184        projection,
185        header,
186        body,
187        nonce,
188    })
189}
190pub fn encode_text(out: &mut Vec<u8>, s: &str) {
191    bytes(out, s.as_bytes());
192}
193pub fn decode_projection(input: &[u8]) -> Result<Projection> {
194    let mut r = Reader(input);
195    let result = match r.take(1)?[0] {
196        0 => Projection::None,
197        1 => {
198            let action = super::parent::ParentAction::from_byte(r.take(1)?[0])
199                .ok_or_else(|| anyhow::anyhow!("error encrypted-tail-invalid"))?;
200            let workspace = std::str::from_utf8(r.blob(256)?)?.to_owned();
201            let task = std::str::from_utf8(r.blob(256)?)?.to_owned();
202            let deleted = r.take(1)?[0];
203            valid(deleted <= 1)?;
204            let version = match r.take(1)?[0] {
205                0 => None,
206                1 => Some(std::str::from_utf8(r.blob(256)?)?.to_owned()),
207                _ => anyhow::bail!("error encrypted-tail-projection"),
208            };
209            valid(
210                !workspace.is_empty()
211                    && !task.is_empty()
212                    && version.as_ref().is_none_or(|s| !s.is_empty()),
213            )?;
214            valid(
215                action != super::parent::ParentAction::Create
216                    || (deleted == 0 && version.is_some()),
217            )?;
218            Projection::Parent {
219                action,
220                workspace,
221                task,
222                deleted: deleted == 1,
223                version,
224            }
225        }
226        kind @ (2 | 3) => {
227            let mut text = || -> Result<String> {
228                let s = std::str::from_utf8(r.blob(256)?)?.to_owned();
229                valid(!s.is_empty())?;
230                Ok(s)
231            };
232            let workspace = text()?;
233            let task = text()?;
234            let reference = text()?;
235            if kind == 3 {
236                Projection::Unref {
237                    workspace,
238                    task,
239                    reference,
240                }
241            } else {
242                let descriptor = r.blob(1984)?.to_vec();
243                crate::images::Descriptor::decode(&descriptor)?;
244                let deleted = r.take(1)?[0];
245                valid(deleted <= 1)?;
246                let version = match r.take(1)?[0] {
247                    0 => None,
248                    1 => {
249                        let s = std::str::from_utf8(r.blob(256)?)?.to_owned();
250                        valid(!s.is_empty())?;
251                        Some(s)
252                    }
253                    _ => anyhow::bail!("error encrypted-image-hint"),
254                };
255                Projection::Ref {
256                    workspace,
257                    task,
258                    reference,
259                    descriptor,
260                    deleted: deleted == 1,
261                    version,
262                }
263            }
264        }
265        4 => {
266            let source = std::str::from_utf8(r.blob(16)?)?.to_owned();
267            let target = std::str::from_utf8(r.blob(16)?)?.to_owned();
268            for id in [&source, &target] {
269                crate::workspace::validate(id)?;
270            }
271            valid(source != target)?;
272            let count = u16::from_be_bytes(r.array()?) as usize;
273            valid((1..=MAX_MOVE_TASKS).contains(&count))?;
274            let mut tasks: Vec<String> = Vec::with_capacity(count);
275            for _ in 0..count {
276                let mut bytes = [0_u8; 16];
277                bytes[6..].copy_from_slice(r.take(10)?);
278                let task = encode_task(&bytes[6..]);
279                valid(tasks.last().is_none_or(|previous| previous < &task))?;
280                tasks.push(task);
281            }
282            Projection::Move {
283                source,
284                target,
285                tasks,
286            }
287        }
288        _ => anyhow::bail!("error encrypted-tail-projection"),
289    };
290    valid(r.0.is_empty())?;
291    Ok(result)
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn move_workspace_validation_keeps_fixed_bytes_and_refusals() {
300        let projection = Projection::Move {
301            source: "0123456789ABCDEF".into(),
302            target: "GHJKMNPQRSTVWXYZ".into(),
303            tasks: vec!["0000000000000000".into()],
304        };
305        let bytes = projection.encode();
306        assert_eq!(
307            hex::encode(&bytes),
308            "0400000010303132333435363738394142434445460000001047484a4b4d4e5051525354565758595a000100000000000000000000"
309        );
310        assert!(Projection::decode(&bytes).unwrap() == projection);
311        for field in [0, 1] {
312            for invalid in [
313                "0123456789ABCDE",
314                "0123456789abcdef",
315                "0123456789ABCDEI",
316                "0123456789ABCDEL",
317                "0123456789ABCDEO",
318                "0123456789ABCDEU",
319                "00000000000000é",
320            ] {
321                let mut bad = vec![4];
322                encode_text(
323                    &mut bad,
324                    if field == 0 {
325                        invalid
326                    } else {
327                        "0123456789ABCDEF"
328                    },
329                );
330                encode_text(
331                    &mut bad,
332                    if field == 1 {
333                        invalid
334                    } else {
335                        "GHJKMNPQRSTVWXYZ"
336                    },
337                );
338                bad.extend(1_u16.to_be_bytes());
339                bad.extend([0; 10]);
340                assert_eq!(
341                    Projection::decode(&bad).err().unwrap().to_string(),
342                    "workspace ID must be 16 Crockford Base32 characters"
343                );
344            }
345        }
346    }
347}