Skip to main content

git_simple_encrypt/crypt/
header.rs

1// GITSE Binary Header Layout (64 Bytes)
2//  00          04  05  06  07           17                  27              3F
3//  +-----------+---+---+---+-----------+-------------------+---------------+
4//  |   MAGIC   | V | F | A |   SALT    |     `FILE_ID`     |   RESERVED    |
5//  |  "GITSE"  |   |   |   | (16 bytes)|    (16 bytes)     |  (24 bytes)   |
6//  +-----------+---+---+---+-----------+-------------------+---------------+
7//    5 bytes     1   1   1    16 bytes       16 bytes          24 bytes
8//                |   |   |
9//     Version ---+   |   +--- Encryption Algo (1 = XChaCha20-Poly1305 Stream)
10//                    |
11//      Flags --------+ (Bit 0: Compression)
12
13use rand::Rng;
14
15pub const MAGIC: &[u8; 5] = b"GITSE";
16pub const VERSION: u8 = 3;
17pub(super) const FLAG_COMPRESSED: u8 = 1 << 0;
18pub(super) const ENC_ALGO: u8 = 1;
19
20pub const SALT_LEN: usize = 16;
21pub const FILE_ID_LEN: usize = 16;
22pub const NONCE_LEN: usize = 24;
23pub const HEADER_LEN: usize = 64;
24pub(super) const RESERVED_LEN: usize =
25    HEADER_LEN - (MAGIC.len() + 1 + 1 + 1 + SALT_LEN + FILE_ID_LEN);
26
27pub const CHUNK_SIZE: usize = 65536;
28
29#[inline]
30#[must_use]
31pub const fn is_encrypted_version(v: u8) -> bool {
32    v == VERSION
33}
34
35#[repr(C)]
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub struct FileHeader {
38    pub magic: [u8; 5],
39    pub version: u8,
40    pub flags: u8,
41    pub enc_algo: u8,
42    pub salt: [u8; SALT_LEN],
43    pub file_id: [u8; FILE_ID_LEN],
44    pub reserved: [u8; RESERVED_LEN],
45}
46
47const _: () = assert!(std::mem::size_of::<FileHeader>() == HEADER_LEN);
48const _: () = assert!(std::mem::align_of::<FileHeader>() == 1);
49
50impl FileHeader {
51    #[must_use]
52    pub const fn new(compressed: bool, salt: [u8; SALT_LEN], file_id: [u8; FILE_ID_LEN]) -> Self {
53        let mut flags = 0u8;
54        if compressed {
55            flags |= FLAG_COMPRESSED;
56        }
57        Self {
58            magic: *MAGIC,
59            version: VERSION,
60            flags,
61            enc_algo: ENC_ALGO,
62            salt,
63            file_id,
64            reserved: [0u8; RESERVED_LEN],
65        }
66    }
67
68    #[must_use]
69    pub fn generate_file_id() -> [u8; FILE_ID_LEN] {
70        let mut rng = rand::rng();
71        let mut id = [0u8; FILE_ID_LEN];
72        rng.fill_bytes(&mut id);
73        id
74    }
75
76    pub fn from_bytes(bytes: &[u8; HEADER_LEN]) -> crate::error::Result<&Self> {
77        use crate::error::Error;
78
79        let header: &Self = unsafe { &*(bytes.as_ptr().cast()) };
80
81        if &header.magic != MAGIC {
82            return Err(Error::InvalidMagic);
83        }
84        if !is_encrypted_version(header.version) {
85            return Err(Error::UnsupportedVersion(header.version));
86        }
87        if header.enc_algo != ENC_ALGO {
88            return Err(Error::UnsupportedAlgo(header.enc_algo));
89        }
90
91        Ok(header)
92    }
93
94    pub fn read_from<R: std::io::Read>(reader: &mut R) -> crate::error::Result<Self> {
95        let mut buf = [0u8; HEADER_LEN];
96        reader.read_exact(&mut buf)?;
97        Ok(*Self::from_bytes(&buf)?)
98    }
99
100    pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> crate::error::Result<()> {
101        writer.write_all(self.as_bytes())?;
102        Ok(())
103    }
104
105    #[must_use]
106    pub const fn as_bytes(&self) -> &[u8; HEADER_LEN] {
107        unsafe { &*std::ptr::from_ref::<Self>(self).cast() }
108    }
109
110    #[must_use]
111    pub const fn is_compressed(&self) -> bool {
112        (self.flags & FLAG_COMPRESSED) != 0
113    }
114}