Skip to main content

luks/
header.rs

1//! LUKS1 partition header (`phdr`) and keyslot parsing.
2//!
3//! The LUKS1 on-disk header is 592 bytes, all integers big-endian, per the
4//! *LUKS On-Disk Format Specification* (version 1.2.3). Layout:
5//!
6//! ```text
7//!   0  magic[6] = "LUKS\xba\xbe"      104  payload-offset  u32 (sectors)
8//!   6  version  u16                    108  key-bytes       u32
9//!   8  cipher-name[32]                 112  mk-digest[20]
10//!  40  cipher-mode[32]                 132  mk-digest-salt[32]
11//!  72  hash-spec[32]                   164  mk-digest-iter  u32
12//!                                       168  uuid[40]
13//!  208  8 x keyslot (48 bytes each)
14//! ```
15
16use crate::bytes::{be_u16, be_u32, bytes_n, cstr};
17use crate::error::{LuksError, Result};
18
19/// The LUKS1 magic signature.
20pub const LUKS_MAGIC: [u8; 6] = [b'L', b'U', b'K', b'S', 0xba, 0xbe];
21/// Total size of the LUKS1 header in bytes.
22pub const LUKS1_PHDR_LEN: usize = 592;
23/// Number of keyslots in a LUKS1 header.
24pub const LUKS_NUM_KEYS: usize = 8;
25/// Size of each keyslot record in bytes.
26pub const KEYSLOT_LEN: usize = 48;
27/// Size of the master-key digest (always 20 bytes in LUKS1).
28pub const MK_DIGEST_LEN: usize = 20;
29/// Size of a salt field.
30pub const SALT_LEN: usize = 32;
31/// Keyslot `active` marker: enabled.
32pub const KEY_ENABLED: u32 = 0x00AC_71F3;
33/// Keyslot `active` marker: disabled.
34pub const KEY_DISABLED: u32 = 0x0000_DEAD;
35/// LUKS sector size (bytes).
36pub const SECTOR: u64 = 512;
37
38/// One LUKS1 keyslot descriptor.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct Keyslot {
41    /// `active` marker (`KEY_ENABLED` / `KEY_DISABLED`).
42    pub active: u32,
43    /// PBKDF2 iteration count for this slot.
44    pub iterations: u32,
45    /// PBKDF2 salt (32 bytes).
46    pub salt: [u8; SALT_LEN],
47    /// Byte offset of the anti-forensic key material = `key_material_offset * 512`.
48    pub key_material_offset: u32,
49    /// Number of anti-forensic stripes (typically 4000).
50    pub stripes: u32,
51}
52
53impl Keyslot {
54    /// Whether this keyslot is active (holds usable key material).
55    #[must_use]
56    pub fn is_active(&self) -> bool {
57        self.active == KEY_ENABLED
58    }
59
60    /// Whether this keyslot is explicitly disabled (marker `KEY_DISABLED`). A slot
61    /// that is neither enabled nor disabled carries a corrupt/unknown marker.
62    #[must_use]
63    pub fn is_disabled(&self) -> bool {
64        self.active == KEY_DISABLED
65    }
66}
67
68/// A parsed LUKS1 partition header.
69#[derive(Debug, Clone)]
70pub struct Luks1Header {
71    /// Header version (always 1 here).
72    pub version: u16,
73    /// Cipher name, e.g. `aes`.
74    pub cipher_name: String,
75    /// Cipher mode, e.g. `xts-plain64`.
76    pub cipher_mode: String,
77    /// Hash spec for PBKDF2 and AF, e.g. `sha256`.
78    pub hash_spec: String,
79    /// Payload start, in 512-byte sectors.
80    pub payload_offset: u32,
81    /// Master-key length in bytes (e.g. 64 for AES-256-XTS).
82    pub key_bytes: u32,
83    /// Master-key digest (PBKDF2 of the master key).
84    pub mk_digest: [u8; MK_DIGEST_LEN],
85    /// Salt for the master-key digest.
86    pub mk_digest_salt: [u8; SALT_LEN],
87    /// Iteration count for the master-key digest.
88    pub mk_digest_iter: u32,
89    /// Volume UUID.
90    pub uuid: String,
91    /// The eight keyslots.
92    pub keyslots: [Keyslot; LUKS_NUM_KEYS],
93}
94
95impl Luks1Header {
96    /// Parse a LUKS1 header from the first `LUKS1_PHDR_LEN` bytes of `data`.
97    ///
98    /// # Errors
99    /// [`LuksError::NotLuks`] if the magic is absent, [`LuksError::UnsupportedVersion`]
100    /// if the version is not 1, [`LuksError::MalformedHeader`] if the buffer is short.
101    pub fn parse(data: &[u8]) -> Result<Self> {
102        let magic = bytes_n::<6>(data, 0);
103        if magic != LUKS_MAGIC {
104            return Err(LuksError::NotLuks { found: magic });
105        }
106        if data.len() < LUKS1_PHDR_LEN {
107            return Err(LuksError::MalformedHeader {
108                what: "phdr",
109                need: LUKS1_PHDR_LEN,
110                got: data.len(),
111            });
112        }
113        let version = be_u16(data, 6);
114        if version != 1 {
115            return Err(LuksError::UnsupportedVersion { version });
116        }
117
118        let keyslots = std::array::from_fn(|i| {
119            let base = 208 + i * KEYSLOT_LEN;
120            Keyslot {
121                active: be_u32(data, base),
122                iterations: be_u32(data, base + 4),
123                salt: bytes_n::<SALT_LEN>(data, base + 8),
124                key_material_offset: be_u32(data, base + 40),
125                stripes: be_u32(data, base + 44),
126            }
127        });
128
129        Ok(Luks1Header {
130            version,
131            cipher_name: cstr(data, 8, 32),
132            cipher_mode: cstr(data, 40, 32),
133            hash_spec: cstr(data, 72, 32),
134            payload_offset: be_u32(data, 104),
135            key_bytes: be_u32(data, 108),
136            mk_digest: bytes_n::<MK_DIGEST_LEN>(data, 112),
137            mk_digest_salt: bytes_n::<SALT_LEN>(data, 132),
138            mk_digest_iter: be_u32(data, 164),
139            uuid: cstr(data, 168, 40),
140            keyslots,
141        })
142    }
143
144    /// The active keyslots, in slot order.
145    pub fn active_keyslots(&self) -> impl Iterator<Item = &Keyslot> {
146        self.keyslots.iter().filter(|k| k.is_active())
147    }
148
149    /// Payload byte offset (`payload_offset * 512`).
150    #[must_use]
151    pub fn payload_byte_offset(&self) -> u64 {
152        u64::from(self.payload_offset) * SECTOR
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    /// Build a synthetic LUKS1 header with the given fields (one active keyslot 0).
161    fn build_header() -> Vec<u8> {
162        let mut h = vec![0u8; LUKS1_PHDR_LEN];
163        h[0..6].copy_from_slice(&LUKS_MAGIC);
164        h[6..8].copy_from_slice(&1u16.to_be_bytes());
165        h[8..11].copy_from_slice(b"aes");
166        h[40..51].copy_from_slice(b"xts-plain64");
167        h[72..78].copy_from_slice(b"sha256");
168        h[104..108].copy_from_slice(&4096u32.to_be_bytes()); // payload offset
169        h[108..112].copy_from_slice(&64u32.to_be_bytes()); // key bytes (AES-256-XTS)
170        h[112..132].copy_from_slice(&[0xAB; 20]); // mk-digest
171        h[132..164].copy_from_slice(&[0xCD; 32]); // mk-digest salt
172        h[164..168].copy_from_slice(&1000u32.to_be_bytes()); // mk-digest iter
173        h[168..204].copy_from_slice(b"b22690e1-a392-4ecc-83b1-c1cf21200116");
174        // keyslot 0: active, iterations, salt, key-material offset (sector 8), stripes 4000
175        let b = 208;
176        h[b..b + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
177        h[b + 4..b + 8].copy_from_slice(&50000u32.to_be_bytes());
178        h[b + 8..b + 40].copy_from_slice(&[0x11; 32]);
179        h[b + 40..b + 44].copy_from_slice(&8u32.to_be_bytes());
180        h[b + 44..b + 48].copy_from_slice(&4000u32.to_be_bytes());
181        // keyslot 1: disabled
182        let b1 = 208 + KEYSLOT_LEN;
183        h[b1..b1 + 4].copy_from_slice(&KEY_DISABLED.to_be_bytes());
184        h
185    }
186
187    #[test]
188    fn parses_luks1_header_fields() {
189        let h = Luks1Header::parse(&build_header()).unwrap();
190        assert_eq!(h.version, 1);
191        assert_eq!(h.cipher_name, "aes");
192        assert_eq!(h.cipher_mode, "xts-plain64");
193        assert_eq!(h.hash_spec, "sha256");
194        assert_eq!(h.payload_offset, 4096);
195        assert_eq!(h.payload_byte_offset(), 4096 * 512);
196        assert_eq!(h.key_bytes, 64);
197        assert_eq!(h.mk_digest, [0xAB; 20]);
198        assert_eq!(h.mk_digest_salt, [0xCD; 32]);
199        assert_eq!(h.mk_digest_iter, 1000);
200        assert_eq!(h.uuid, "b22690e1-a392-4ecc-83b1-c1cf21200116");
201    }
202
203    #[test]
204    fn parses_keyslots_and_active_filter() {
205        let h = Luks1Header::parse(&build_header()).unwrap();
206        assert!(h.keyslots[0].is_active());
207        assert_eq!(h.keyslots[0].iterations, 50000);
208        assert_eq!(h.keyslots[0].salt, [0x11; 32]);
209        assert_eq!(h.keyslots[0].key_material_offset, 8);
210        assert_eq!(h.keyslots[0].stripes, 4000);
211        assert!(!h.keyslots[1].is_active());
212        assert!(h.keyslots[1].is_disabled());
213        assert_eq!(h.active_keyslots().count(), 1);
214    }
215
216    #[test]
217    fn rejects_non_luks() {
218        let err = Luks1Header::parse(&[0u8; LUKS1_PHDR_LEN]).unwrap_err();
219        assert!(matches!(err, LuksError::NotLuks { .. }));
220    }
221
222    #[test]
223    fn rejects_bad_version() {
224        let mut h = build_header();
225        h[6..8].copy_from_slice(&9u16.to_be_bytes());
226        assert!(matches!(
227            Luks1Header::parse(&h).unwrap_err(),
228            LuksError::UnsupportedVersion { version: 9 }
229        ));
230    }
231
232    #[test]
233    fn rejects_truncated() {
234        let mut h = build_header();
235        h.truncate(100);
236        assert!(matches!(
237            Luks1Header::parse(&h).unwrap_err(),
238            LuksError::MalformedHeader { .. }
239        ));
240    }
241}