use purecrypto::hash::HashAlgorithm;
use crate::Result;
use super::af;
use super::crypt::{CipherSpec, SectorCipher};
use super::hash;
pub const LUKS_MAGIC: [u8; 6] = [b'L', b'U', b'K', b'S', 0xba, 0xbe];
pub const PHDR_BYTES: usize = 592;
pub const NUM_KEYS: usize = 8;
pub const DIGEST_BYTES: usize = 20;
pub const SALT_BYTES: usize = 32;
pub const SLOT_ENABLED: u32 = 0x00AC_71F3;
pub const SLOT_DISABLED: u32 = 0x0000_DEAD;
pub const KEYSLOT_ALIGN: u64 = 4096;
#[derive(Debug, Clone, Copy)]
pub struct KeySlot {
pub active: u32,
pub iterations: u32,
pub salt: [u8; SALT_BYTES],
pub key_material_offset: u32,
pub stripes: u32,
}
impl KeySlot {
pub fn is_enabled(&self) -> bool {
self.active == SLOT_ENABLED
}
fn decode(b: &[u8]) -> Self {
let mut salt = [0u8; SALT_BYTES];
salt.copy_from_slice(&b[8..40]);
Self {
active: u32_be(b, 0),
iterations: u32_be(b, 4),
salt,
key_material_offset: u32_be(b, 40),
stripes: u32_be(b, 44),
}
}
fn encode(&self, b: &mut [u8]) {
b[0..4].copy_from_slice(&self.active.to_be_bytes());
b[4..8].copy_from_slice(&self.iterations.to_be_bytes());
b[8..40].copy_from_slice(&self.salt);
b[40..44].copy_from_slice(&self.key_material_offset.to_be_bytes());
b[44..48].copy_from_slice(&self.stripes.to_be_bytes());
}
}
#[derive(Debug, Clone)]
pub struct Header {
pub cipher_name: String,
pub cipher_mode: String,
pub hash_spec: String,
pub payload_offset: u32,
pub key_bytes: u32,
pub mk_digest: [u8; DIGEST_BYTES],
pub mk_digest_salt: [u8; SALT_BYTES],
pub mk_digest_iter: u32,
pub uuid: String,
pub slots: [KeySlot; NUM_KEYS],
}
impl Header {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < PHDR_BYTES {
return Err(crate::Error::InvalidImage(format!(
"luks1: header buffer is {} bytes, need ≥ {PHDR_BYTES}",
buf.len()
)));
}
if buf[0..6] != LUKS_MAGIC {
return Err(crate::Error::InvalidImage(
"luks1: bad magic (not a LUKS volume)".into(),
));
}
let version = u16::from_be_bytes([buf[6], buf[7]]);
if version != 1 {
return Err(crate::Error::InvalidImage(format!(
"luks1: header says version {version}, not 1"
)));
}
let mut mk_digest = [0u8; DIGEST_BYTES];
mk_digest.copy_from_slice(&buf[112..132]);
let mut mk_digest_salt = [0u8; SALT_BYTES];
mk_digest_salt.copy_from_slice(&buf[132..164]);
let mut slots = [KeySlot {
active: SLOT_DISABLED,
iterations: 0,
salt: [0u8; SALT_BYTES],
key_material_offset: 0,
stripes: 0,
}; NUM_KEYS];
for (i, slot) in slots.iter_mut().enumerate() {
*slot = KeySlot::decode(&buf[208 + i * 48..208 + (i + 1) * 48]);
}
let h = Self {
cipher_name: cstr(&buf[8..40]),
cipher_mode: cstr(&buf[40..72]),
hash_spec: cstr(&buf[72..104]),
payload_offset: u32_be(buf, 104),
key_bytes: u32_be(buf, 108),
mk_digest,
mk_digest_salt,
mk_digest_iter: u32_be(buf, 164),
uuid: cstr(&buf[168..208]),
slots,
};
h.validate()?;
Ok(h)
}
fn validate(&self) -> Result<()> {
if self.key_bytes == 0 || self.key_bytes > 4096 {
return Err(crate::Error::InvalidImage(format!(
"luks1: implausible key-bytes {}",
self.key_bytes
)));
}
if self.payload_offset == 0 {
return Err(crate::Error::InvalidImage(
"luks1: payload offset is 0 — it would overlap the header".into(),
));
}
if self.mk_digest_iter == 0 {
return Err(crate::Error::InvalidImage(
"luks1: master-key digest iteration count is 0".into(),
));
}
for (i, s) in self.slots.iter().enumerate() {
if !s.is_enabled() {
continue;
}
if s.iterations == 0 {
return Err(crate::Error::InvalidImage(format!(
"luks1: keyslot {i} is enabled but has 0 PBKDF2 iterations"
)));
}
if s.stripes == 0 {
return Err(crate::Error::InvalidImage(format!(
"luks1: keyslot {i} is enabled but has 0 anti-forensic stripes"
)));
}
let material = s.stripes as u64 * self.key_bytes as u64;
if material > super::v2::MAX_AF_MATERIAL_BYTES {
return Err(crate::Error::InvalidImage(format!(
"luks1: keyslot {i} declares {} stripes of {} bytes = {material}, \
over the {} a keyslot may hold",
s.stripes,
self.key_bytes,
super::v2::MAX_AF_MATERIAL_BYTES
)));
}
}
Ok(())
}
pub fn cipher_spec_string(&self) -> String {
format!("{}-{}", self.cipher_name, self.cipher_mode)
}
pub fn cipher_spec(&self) -> Result<CipherSpec> {
CipherSpec::parse(&self.cipher_spec_string(), self.key_bytes as usize)
}
pub fn hash(&self) -> Result<HashAlgorithm> {
hash::parse(&self.hash_spec)
}
pub fn payload_offset_bytes(&self) -> u64 {
self.payload_offset as u64 * 512
}
pub fn slot_material_len(&self, i: usize) -> u64 {
self.slots[i].stripes as u64 * self.key_bytes as u64
}
pub fn slot_material_extent(&self, i: usize) -> (u64, u64) {
(
self.slots[i].key_material_offset as u64 * 512,
self.slot_material_len(i).div_ceil(512) * 512,
)
}
pub fn verify_master_key(&self, mk: &[u8]) -> Result<bool> {
let alg = self.hash()?;
let mut got = [0u8; DIGEST_BYTES];
hash::pbkdf2(alg, mk, &self.mk_digest_salt, self.mk_digest_iter, &mut got)?;
Ok(constant_time_eq(&got, &self.mk_digest))
}
pub fn slot_key(&self, i: usize, passphrase: &[u8]) -> Result<Vec<u8>> {
let slot = &self.slots[i];
let alg = self.hash()?;
let mut out = vec![0u8; self.key_bytes as usize];
hash::pbkdf2(alg, passphrase, &slot.salt, slot.iterations, &mut out)?;
Ok(out)
}
pub fn unlock_slot(
&self,
i: usize,
passphrase: &[u8],
encrypted_material: &mut [u8],
) -> Result<Option<Vec<u8>>> {
let slot = &self.slots[i];
let exact = self.slot_material_len(i) as usize;
if encrypted_material.len() < exact {
return Err(crate::Error::InvalidImage(format!(
"luks1: keyslot {i} material is {} bytes, need {exact}",
encrypted_material.len()
)));
}
let derived = self.slot_key(i, passphrase)?;
let cipher = SectorCipher::new(self.cipher_spec()?, &derived, 512)?;
cipher.decrypt(0, encrypted_material)?;
let mk = af::merge(
self.hash()?,
&encrypted_material[..exact],
self.key_bytes as usize,
slot.stripes,
)?;
if self.verify_master_key(&mk)? {
Ok(Some(mk))
} else {
Ok(None)
}
}
pub fn encode(&self) -> [u8; PHDR_BYTES] {
let mut b = [0u8; PHDR_BYTES];
b[0..6].copy_from_slice(&LUKS_MAGIC);
b[6..8].copy_from_slice(&1u16.to_be_bytes());
put_cstr(&mut b[8..40], &self.cipher_name);
put_cstr(&mut b[40..72], &self.cipher_mode);
put_cstr(&mut b[72..104], &self.hash_spec);
b[104..108].copy_from_slice(&self.payload_offset.to_be_bytes());
b[108..112].copy_from_slice(&self.key_bytes.to_be_bytes());
b[112..132].copy_from_slice(&self.mk_digest);
b[132..164].copy_from_slice(&self.mk_digest_salt);
b[164..168].copy_from_slice(&self.mk_digest_iter.to_be_bytes());
put_cstr(&mut b[168..208], &self.uuid);
for (i, s) in self.slots.iter().enumerate() {
s.encode(&mut b[208 + i * 48..208 + (i + 1) * 48]);
}
b
}
}
fn cstr(field: &[u8]) -> String {
let end = field.iter().position(|&c| c == 0).unwrap_or(field.len());
String::from_utf8_lossy(&field[..end]).into_owned()
}
fn put_cstr(field: &mut [u8], s: &str) {
field.fill(0);
let n = s.len().min(field.len());
field[..n].copy_from_slice(&s.as_bytes()[..n]);
}
fn u32_be(buf: &[u8], off: usize) -> u32 {
u32::from_be_bytes(buf[off..off + 4].try_into().unwrap())
}
pub(super) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Header {
Header {
cipher_name: "aes".into(),
cipher_mode: "xts-plain64".into(),
hash_spec: "sha256".into(),
payload_offset: 4096,
key_bytes: 64,
mk_digest: [1u8; DIGEST_BYTES],
mk_digest_salt: [2u8; SALT_BYTES],
mk_digest_iter: 5000,
uuid: "c9f1a3e4-0000-4000-8000-000000000001".into(),
slots: [KeySlot {
active: SLOT_DISABLED,
iterations: 0,
salt: [0u8; SALT_BYTES],
key_material_offset: 0,
stripes: 0,
}; NUM_KEYS],
}
}
#[test]
fn encode_decode_round_trip() {
let mut h = sample();
h.slots[0] = KeySlot {
active: SLOT_ENABLED,
iterations: 100_000,
salt: [7u8; SALT_BYTES],
key_material_offset: 8,
stripes: 4000,
};
let bytes = h.encode();
let d = Header::decode(&bytes).unwrap();
assert_eq!(d.cipher_name, "aes");
assert_eq!(d.cipher_mode, "xts-plain64");
assert_eq!(d.hash_spec, "sha256");
assert_eq!(d.key_bytes, 64);
assert_eq!(d.payload_offset, 4096);
assert_eq!(d.uuid, h.uuid);
assert!(d.slots[0].is_enabled());
assert_eq!(d.slots[0].stripes, 4000);
assert!(!d.slots[1].is_enabled());
assert_eq!(d.cipher_spec_string(), "aes-xts-plain64");
assert_eq!(d.cipher_spec().unwrap().key_bytes, 64);
}
#[test]
fn rejects_foreign_or_broken_headers() {
let mut bytes = sample().encode();
bytes[0] = b'X';
assert!(Header::decode(&bytes).is_err());
let mut bytes = sample().encode();
bytes[6..8].copy_from_slice(&2u16.to_be_bytes()); assert!(Header::decode(&bytes).is_err());
let mut bytes = sample().encode();
bytes[108..112].copy_from_slice(&0u32.to_be_bytes()); assert!(Header::decode(&bytes).is_err());
let mut bytes = sample().encode();
bytes[104..108].copy_from_slice(&0u32.to_be_bytes()); assert!(Header::decode(&bytes).is_err());
}
#[test]
fn refuses_an_absurd_stripe_count() {
let mut h = sample();
h.slots[0] = KeySlot {
active: SLOT_ENABLED,
iterations: 1000,
salt: [0u8; SALT_BYTES],
key_material_offset: 8,
stripes: u32::MAX,
};
let bytes = h.encode();
assert!(matches!(
Header::decode(&bytes),
Err(crate::Error::InvalidImage(_))
));
}
#[test]
fn truncated_buffer_is_an_error() {
let bytes = sample().encode();
assert!(Header::decode(&bytes[..200]).is_err());
}
#[test]
fn unlock_slot_round_trip() {
let mut h = sample();
h.key_bytes = 32;
h.cipher_mode = "xts-plain64".into();
let mk = vec![0x33u8; 32];
let alg = h.hash().unwrap();
hash::pbkdf2(
alg,
&mk,
&h.mk_digest_salt,
h.mk_digest_iter,
&mut h.mk_digest,
)
.unwrap();
h.slots[0] = KeySlot {
active: SLOT_ENABLED,
iterations: 1000,
salt: [9u8; SALT_BYTES],
key_material_offset: 8,
stripes: 16,
};
let random: Vec<u8> = (0..32 * 15).map(|i| (i as u8).wrapping_mul(13)).collect();
let split = af::split(alg, &mk, 16, &random).unwrap();
let derived = h.slot_key(0, b"open sesame").unwrap();
let cipher = SectorCipher::new(h.cipher_spec().unwrap(), &derived, 512).unwrap();
let mut material = split.clone();
cipher.encrypt(0, &mut material).unwrap();
let mut probe = material.clone();
let got = h.unlock_slot(0, b"open sesame", &mut probe).unwrap();
assert_eq!(got.as_deref(), Some(&mk[..]));
let mut probe = material.clone();
assert!(h.unlock_slot(0, b"wrong", &mut probe).unwrap().is_none());
}
#[test]
fn constant_time_eq_behaves_like_eq() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
}
}