use crate::{Error, Result};
pub const MAGIC: [u8; 11] = *b"\0GITXCRYPT\0";
pub const FORMAT_VERSION: u8 = 1;
pub const SUITE_AES_256_SIV: u8 = 1;
pub const FLAG_LF_NORMALIZED: u8 = 0b0000_0001;
const KNOWN_FLAGS: u8 = FLAG_LF_NORMALIZED;
pub const KEY_ID_LEN: usize = 8;
pub const HEADER_LEN: usize = 22;
pub const SIV_LEN: usize = 16;
pub const OVERHEAD: usize = HEADER_LEN + SIV_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
pub version: u8,
pub suite: u8,
pub flags: u8,
pub key_id: [u8; KEY_ID_LEN],
}
impl Header {
#[must_use]
pub fn new(flags: u8, key_id: [u8; KEY_ID_LEN]) -> Self {
Self {
version: FORMAT_VERSION,
suite: SUITE_AES_256_SIV,
flags,
key_id,
}
}
#[must_use]
pub fn to_bytes(self) -> [u8; HEADER_LEN] {
let mut bytes = [0u8; HEADER_LEN];
bytes[..MAGIC.len()].copy_from_slice(&MAGIC);
bytes[11] = self.version;
bytes[12] = self.suite;
bytes[13] = self.flags;
bytes[14..HEADER_LEN].copy_from_slice(&self.key_id);
bytes
}
pub fn parse(blob: &[u8]) -> Result<Self> {
if blob.len() < OVERHEAD {
return Err(Error::Format(
"the file is shorter than an empty encrypted file".into(),
));
}
if !blob.starts_with(&MAGIC) {
return Err(Error::Format("the file does not carry our magic".into()));
}
let version = blob[11];
if version != FORMAT_VERSION {
return Err(Error::Format(format!(
"format version {version} needs a newer git-xcrypt"
)));
}
let suite = blob[12];
if suite != SUITE_AES_256_SIV {
return Err(Error::Format(format!(
"cipher suite {suite:#04x} needs a newer git-xcrypt"
)));
}
let flags = blob[13];
if flags & !KNOWN_FLAGS != 0 {
return Err(Error::Format(format!(
"flags {flags:#010b} set a reserved bit; this file needs a newer git-xcrypt"
)));
}
let mut key_id = [0u8; KEY_ID_LEN];
key_id.copy_from_slice(&blob[14..HEADER_LEN]);
Ok(Self {
version,
suite,
flags,
key_id,
})
}
}
#[must_use]
pub fn looks_encrypted(content: &[u8]) -> bool {
content.starts_with(&MAGIC)
}
#[cfg(test)]
mod tests {
use super::*;
const KEY_ID: [u8; KEY_ID_LEN] = [0x3f, 0xa9, 0x12, 0x0b, 0x7e, 0xc4, 0x55, 0x8a];
fn blob_with(version: u8, suite: u8, flags: u8) -> Vec<u8> {
let mut header = Header::new(flags, KEY_ID).to_bytes();
header[11] = version;
header[12] = suite;
let mut blob = header.to_vec();
blob.extend_from_slice(&[0u8; SIV_LEN]);
blob
}
#[test]
fn magic_starts_with_nul_so_git_sees_binary() {
assert_eq!(MAGIC[0], 0);
assert_eq!(MAGIC[MAGIC.len() - 1], 0);
}
#[test]
fn overhead_is_thirty_eight_bytes() {
assert_eq!(OVERHEAD, 38);
assert_eq!(Header::new(0, KEY_ID).to_bytes().len(), HEADER_LEN);
}
#[test]
fn an_unknown_version_is_refused() {
let blob = blob_with(FORMAT_VERSION + 1, SUITE_AES_256_SIV, 0);
assert!(Header::parse(&blob).is_err());
}
#[test]
fn an_unknown_suite_is_refused() {
let blob = blob_with(FORMAT_VERSION, SUITE_AES_256_SIV + 1, 0);
assert!(Header::parse(&blob).is_err());
}
#[test]
fn a_reserved_flag_bit_is_refused() {
for bit in 1..8 {
let blob = blob_with(FORMAT_VERSION, SUITE_AES_256_SIV, 1 << bit);
assert!(
Header::parse(&blob).is_err(),
"flag bit {bit} must be refused until it means something"
);
}
}
}