Skip to main content

sendcipher_core/crypto/
blob_header.rs

1/* Created on 2025.09.29 */
2/* Copyright (c) 2025-2026 Youcef Lemsafer */
3/* SPDX-License-Identifier: MIT */
4
5use byteorder::{LittleEndian, ReadBytesExt};
6use serde::{Deserialize, Serialize};
7use std::io::{Cursor, Read, Write};
8
9mod constants {
10    pub const CURRENT_BLOB_HEADER_VERSION: u32 = 1;
11}
12
13#[repr(C)]
14#[derive(Debug, Clone, Serialize, Deserialize)]
15/// FILE FORMAT PREFIX LAYOUT.
16/// THIS MUST NEVER CHANGE
17/// The prefix is part of the on-disk binary format and is used for
18/// streaming / incremental parsing. If you modify the fields, their
19/// types, ordering, or size, all previously written files will
20/// become unreadable.
21///
22/// If format updates are needed, bump `version` instead and evolve
23/// the *rest* of the header format conditionally.
24pub struct HeaderPrefix {
25    pub magic: [u8; 4],
26    pub version: u32,
27    pub remaining_header_bytes: u32,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct BlobHeader {
32    pub prefix: HeaderPrefix,
33    pub envelopes: Vec<KeyEnvelope>,
34    pub cipher_algorithm: CipherAlgorithm,
35    pub cipher_param_length: u32,
36    pub cipher_raw_params: Vec<u8>,
37    pub authentication_data_length: u16,
38    pub authentication_data: Vec<u8>,
39    pub cipher_length: u64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct KeyEnvelope {
44    pub version: u32,
45    pub envelope_type: KeyEnvelopeType,
46    pub envelope_data: Vec<u8>,
47}
48
49impl KeyEnvelope {
50    const CURRENT_VERSION: u32 = 1;
51    pub fn new(envelope_type: KeyEnvelopeType, envelope_data: Vec<u8>) -> Self {
52        Self {
53            version: Self::CURRENT_VERSION,
54            envelope_type,
55            envelope_data,
56        }
57    }
58    pub fn envelope_data(&self) -> &Vec<u8> {
59        &self.envelope_data
60    }
61}
62
63#[repr(u8)]
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65pub enum KeyEnvelopeType {
66    Invalid = 0,
67    Kdf = 1,
68    Pgp = 2,
69    Age = 3,
70}
71
72impl KeyEnvelopeType {
73    pub fn from_bytes(data: &[u8]) -> Result<(KeyEnvelopeType, usize), crate::error::Error> {
74        Ok((
75            bincode::deserialize(data)
76                .map_err(|e| crate::error::Error::DeserializationError(e.to_string()))?,
77            1,
78        ))
79    }
80}
81
82#[repr(u32)]
83#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq)]
84pub enum CipherAlgorithm {
85    Invalid = 0,
86    Aes256Gcm = 1,
87}
88
89impl CipherAlgorithm {
90    pub fn from_u32(value: u32) -> Option<Self> {
91        match value {
92            0 => Some(Self::Invalid),
93            1 => Some(Self::Aes256Gcm),
94            _ => None,
95        }
96    }
97
98    pub fn as_u32(self) -> u32 {
99        self as u32
100    }
101}
102
103#[repr(u16)]
104#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq)]
105pub enum KdfAlgorithm {
106    Invalid = 0,
107    Argon2id = 1,
108}
109
110impl KdfAlgorithm {
111    pub fn from_u16(value: u16) -> Option<Self> {
112        match value {
113            0 => Some(Self::Invalid),
114            1 => Some(Self::Argon2id),
115            _ => None,
116        }
117    }
118
119    pub fn as_u16(self) -> u16 {
120        self as u16
121    }
122
123    pub fn to_bytes(&self) -> [u8; 2] {
124        self.as_u16().to_le_bytes()
125    }
126
127    pub fn from_bytes(bytes: &[u8]) -> Result<(KdfAlgorithm, usize), crate::error::Error> {
128        if bytes.len() < 2 {
129            return Err(crate::error::Error::DeserializationError(
130                format!(
131                    "Cannot read an u16 from a byte sequence of length {:?}",
132                    bytes.len()
133                )
134                .to_string(),
135            ));
136        }
137        match Self::from_u16(u16::from_le_bytes(bytes[0..2].try_into().unwrap())) {
138            Some(kdf_algo) => Ok((kdf_algo, 2 as usize)),
139            None => Err(crate::error::Error::DeserializationError(
140                "Unexpected KDF algorithm tag value".to_string(),
141            )),
142        }
143    }
144}
145
146#[derive(Clone, Serialize, Deserialize, Debug)]
147pub struct Argon2idParams {
148    pub m_cost: u32,
149    pub t_cost: u32,
150    pub p_cost: u32,
151    pub salt: Vec<u8>,
152}
153
154#[derive(Clone, Serialize, Deserialize)]
155pub struct Aes256GcmParams {
156    pub nonce: Vec<u8>,
157}
158
159
160impl HeaderPrefix {
161    const HEADER_PREFIX_LENGTH: usize = size_of::<HeaderPrefix>();
162    const MAGIC: [u8; 4] = *b"SDCR";
163
164    pub fn new() -> Self {
165        HeaderPrefix {
166            magic: Self::MAGIC,
167            version: constants::CURRENT_BLOB_HEADER_VERSION,
168            remaining_header_bytes: 0,
169        }
170    }
171
172    pub fn length() -> usize {
173        HeaderPrefix::HEADER_PREFIX_LENGTH
174    }
175
176    pub fn write_all(&self, cursor: &mut Cursor<&mut [u8]>) -> Result<(), crate::error::Error> {
177        cursor.write_all(&self.magic)?;
178        cursor.write_all(&self.version.to_le_bytes())?;
179        cursor.write_all(&self.remaining_header_bytes.to_le_bytes())?;
180        Ok(())
181    }
182
183    pub fn parse(buffer: &[u8]) -> Result<(Option<HeaderPrefix>, usize), crate::error::Error> {
184        log::debug!("About to parse buffer in HeaderPrefix");
185        if buffer.len() < Self::length() {
186            return Ok((None, 0));
187        }
188        let mut prefix = Self::new();
189        let mut cursor = Cursor::new(buffer);
190        cursor.read_exact(&mut prefix.magic)?;
191        if prefix.magic != Self::MAGIC {
192            return Err(crate::error::Error::DeserializationError(
193                format!(
194                    "Unexpected magic '{:?}' while parsing blob header",
195                    prefix.magic
196                )
197                .to_string(),
198            ));
199        }
200        prefix.version = cursor.read_u32::<LittleEndian>()?;
201        prefix.remaining_header_bytes = cursor.read_u32::<LittleEndian>()?;
202        Ok((Some(prefix), cursor.position() as usize))
203    }
204}
205
206impl BlobHeader {
207    pub fn serialized_size(&self) -> Result<usize, crate::error::Error> {
208        Ok(HeaderPrefix::length()
209            + self.size_of_envelopes()?
210            + size_of_val(&self.cipher_algorithm)
211            + size_of_val(&self.cipher_param_length)
212            + self.cipher_raw_params.len()
213            + size_of_val(&self.authentication_data_length)
214            + self.authentication_data.len()
215            + size_of_val(&self.cipher_length))
216    }
217
218    fn size_of_envelopes(&self) -> Result<usize, crate::error::Error> {
219        Ok(bincode::serialized_size(&self.envelopes)
220            .map_err(|e| crate::error::Error::SerializationError(e.to_string()))?
221            as usize)
222    }
223
224    pub fn get_cipher_length_pos(&self) -> Result<usize, crate::error::Error> {
225        Ok(self.serialized_size()? - size_of_val(&self.cipher_length))
226    }
227
228    pub fn get_cipher_length_length(&self) -> usize {
229        size_of_val(&self.cipher_length)
230    }
231
232    pub fn write_to_slice(&self, buffer: &mut [u8]) -> Result<(), crate::error::Error> {
233        let mut copied_prefix = self.prefix.clone();
234        let mut cursor = Cursor::new(&mut buffer[..]);
235        // We write a header prefix with a temporary remaining_bytes value until
236        // the real one is known
237        copied_prefix.write_all(&mut cursor)?;
238        let end_of_prefix_pos = cursor.position() as u32;
239        log::debug!("Writing key envelopes at offset {:?}", end_of_prefix_pos);
240        bincode::serialize_into(&mut cursor, &self.envelopes)
241            .map_err(|e| crate::error::Error::SerializationError(e.to_string()))?;
242        cursor.write_all(&self.cipher_algorithm.as_u32().to_le_bytes())?;
243        cursor.write_all(&self.cipher_param_length.to_le_bytes())?;
244        cursor.write_all(&self.cipher_raw_params)?;
245        cursor.write_all(&self.authentication_data_length.to_le_bytes())?;
246        cursor.write_all(&self.authentication_data)?;
247        cursor.write_all(&self.cipher_length.to_le_bytes())?;
248        let end_of_header_pos = cursor.position() as u32;
249        copied_prefix.remaining_header_bytes = end_of_header_pos - end_of_prefix_pos;
250        // Overwrite header prefix with correct remaining_bytes value
251        let mut cursor2 = Cursor::new(&mut buffer[..]);
252        copied_prefix.write_all(&mut cursor2)?;
253        log::debug!("Blob header written to buffer: {:02x?}", buffer);
254        Ok(())
255    }
256
257    pub fn to_bytes(&self) -> Result<Vec<u8>, crate::error::Error> {
258        let mut buffer = Vec::with_capacity(self.serialized_size()?);
259        self.write_to_slice(&mut buffer)?;
260        Ok(buffer)
261    }
262
263    pub fn new() -> BlobHeader {
264        Self {
265            prefix: HeaderPrefix::new(),
266            envelopes: vec![],
267            cipher_algorithm: CipherAlgorithm::Invalid,
268            cipher_param_length: 0,
269            cipher_raw_params: vec![0u8; 0],
270            authentication_data_length: 0,
271            authentication_data: vec![0u8; 0],
272            cipher_length: 0,
273        }
274    }
275
276    pub fn parse(buffer: &[u8]) -> Result<(Self, u64), crate::error::Error> {
277        log::debug!(
278            "Parsing buffer form {:02x?} [truncated to 256 bytes]",
279            &buffer[..buffer.len().min(256)]
280        );
281        let mut file_header = BlobHeader::new();
282        // Read prefix (magic, version, remaining_bytes) and all the following fields
283        let (prefix, header_prefix_length) = HeaderPrefix::parse(buffer)?;
284        if prefix.is_none()
285            || (header_prefix_length + prefix.as_ref().unwrap().remaining_header_bytes as usize
286                > buffer.len())
287        {
288            // Not enough bytes
289            return Err(crate::error::Error::BlobParsingError(
290                "Cannot read blob header, not enough data".to_string(),
291            ));
292        }
293        file_header.prefix = prefix.unwrap();
294        let mut cursor = Cursor::new(&buffer[header_prefix_length..]);
295        log::debug!("Reading key envelopes at offset {:?}", header_prefix_length);
296        file_header.envelopes = bincode::deserialize_from(&mut cursor)
297            .map_err(|e| crate::error::Error::DeserializationError(e.to_string()))?;
298        log::debug!("Read {:?} key envelope", file_header.envelopes.len());
299        log::debug!("Envelope: {:?}", file_header.envelopes.first());
300
301        file_header.cipher_algorithm =
302            CipherAlgorithm::from_u32(cursor.read_u32::<LittleEndian>()?)
303                .expect("Invalid cipher algorithm id");
304        file_header.cipher_param_length = cursor.read_u32::<LittleEndian>()?;
305        log::debug!(
306            "Cipher parameters length: {}",
307            file_header.cipher_param_length
308        );
309        file_header.cipher_raw_params = vec![0u8; file_header.cipher_param_length as usize];
310        cursor.read_exact(&mut file_header.cipher_raw_params)?;
311        file_header.authentication_data_length = cursor.read_u16::<LittleEndian>()?;
312        file_header.authentication_data =
313            vec![0u8; file_header.authentication_data_length as usize];
314        cursor.read_exact(&mut file_header.authentication_data)?;
315        file_header.cipher_length = cursor.read_u64::<LittleEndian>()?;
316
317        if file_header.prefix.remaining_header_bytes != cursor.position() as u32 {
318            log::debug!(
319                "** EPIC FAIL: {:?} != {:?}",
320                file_header.prefix.remaining_header_bytes,
321                cursor.position()
322            );
323            return Err(crate::error::Error::BlobParsingError(
324                "Corrupt blob encountered, unexpected header length".to_string(),
325            ));
326        }
327
328        Ok((file_header, header_prefix_length as u64 + cursor.position()))
329    }
330}
331
332impl Argon2idParams {
333    pub fn to_bytes(&self) -> Result<Vec<u8>, crate::error::Error> {
334        bincode::serialize(self).map_err(|e| crate::error::Error::BincodeError(e.to_string()))
335    }
336    pub fn from_bytes(bytes: &[u8]) -> Result<(Self, u64), crate::error::Error> {
337        log::debug!(
338            "Reading Argon2idParams from bytes, buffer size: {}",
339            bytes.len()
340        );
341        let mut cursor = Cursor::new(bytes);
342        Ok((
343            bincode::deserialize_from(&mut cursor)
344                .map_err(|e| crate::error::Error::BincodeError(e.to_string()))?,
345            cursor.position(),
346        ))
347    }
348}
349
350impl Aes256GcmParams {
351    pub fn to_bytes(&self) -> Result<Vec<u8>, crate::error::Error> {
352        bincode::serialize(self).map_err(|e| crate::error::Error::BincodeError(e.to_string()))
353    }
354    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::error::Error> {
355        log::debug!(
356            "Reading Aes256GcmParams from buffer of size {}",
357            bytes.len()
358        );
359        bincode::deserialize(bytes).map_err(|e| crate::error::Error::BincodeError(e.to_string()))
360    }
361}
362