Skip to main content

luks/
header2.rs

1//! LUKS2 header parsing: the 4096-byte binary header plus the JSON metadata area.
2//!
3//! LUKS2 replaces the fixed LUKS1 `phdr` with a small binary header (magic,
4//! version, `hdr_size`) followed by a JSON document describing keyslots (Argon2
5//! or PBKDF2 KDF + anti-forensic area), data segments (cipher, sector size, IV
6//! tweak), and digests (master-key verification). See the *LUKS2 On-Disk Format
7//! Specification*.
8
9use base64::Engine;
10use serde_json::Value;
11
12use crate::bytes::{be_u16, be_u64, bytes_n};
13use crate::error::{LuksError, Result};
14use crate::header::LUKS_MAGIC;
15
16/// Size of the LUKS2 binary header preceding the JSON area.
17pub const LUKS2_BIN_HDR_LEN: usize = 4096;
18
19/// A LUKS2 keyslot key-derivation function.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Luks2Kdf {
22    /// Argon2i / Argon2id (the LUKS2 default).
23    Argon2 {
24        /// `argon2i` or `argon2id`.
25        kind: String,
26        /// Time cost.
27        time: u32,
28        /// Memory cost (KiB).
29        memory: u32,
30        /// Parallelism.
31        cpus: u32,
32        /// Salt bytes.
33        salt: Vec<u8>,
34    },
35    /// PBKDF2 (legacy / converted volumes).
36    Pbkdf2 {
37        /// Hash spec.
38        hash: String,
39        /// Iteration count.
40        iterations: u32,
41        /// Salt bytes.
42        salt: Vec<u8>,
43    },
44}
45
46/// A LUKS2 keyslot.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Luks2Keyslot {
49    /// Keyslot id (JSON object key).
50    pub id: String,
51    /// Master-key size in bytes.
52    pub key_size: usize,
53    /// Anti-forensic stripe count.
54    pub af_stripes: u32,
55    /// Anti-forensic hash.
56    pub af_hash: String,
57    /// Byte offset of the AF key-material area.
58    pub area_offset: u64,
59    /// Byte length of the AF key-material area.
60    pub area_size: u64,
61    /// AF-area cipher (e.g. `aes-xts-plain64`).
62    pub area_encryption: String,
63    /// The KDF.
64    pub kdf: Luks2Kdf,
65}
66
67/// A LUKS2 data segment.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Luks2Segment {
70    /// Segment id.
71    pub id: String,
72    /// Byte offset of the encrypted data.
73    pub offset: u64,
74    /// Cipher (e.g. `aes-xts-plain64`).
75    pub encryption: String,
76    /// Encryption sector size (512 or 4096).
77    pub sector_size: usize,
78    /// IV tweak base added to the plain64 sector number.
79    pub iv_tweak: u128,
80}
81
82/// A LUKS2 digest (master-key verifier).
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Luks2Digest {
85    /// Digest type (`pbkdf2`).
86    pub kind: String,
87    /// Hash spec.
88    pub hash: String,
89    /// PBKDF2 iteration count.
90    pub iterations: u32,
91    /// Salt bytes.
92    pub salt: Vec<u8>,
93    /// Expected digest bytes.
94    pub digest: Vec<u8>,
95    /// Keyslot ids this digest verifies.
96    pub keyslots: Vec<String>,
97}
98
99/// A parsed LUKS2 header.
100#[derive(Debug, Clone)]
101pub struct Luks2Header {
102    /// Version (2).
103    pub version: u16,
104    /// Total header size (binary + JSON) in bytes.
105    pub hdr_size: u64,
106    /// Keyslots.
107    pub keyslots: Vec<Luks2Keyslot>,
108    /// Data segments.
109    pub segments: Vec<Luks2Segment>,
110    /// Digests.
111    pub digests: Vec<Luks2Digest>,
112}
113
114/// Read a JSON value that LUKS2 may encode as a string ("32768") or a number.
115fn as_u64(v: &Value) -> u64 {
116    match v {
117        Value::String(s) => s.parse().unwrap_or(0),
118        Value::Number(n) => n.as_u64().unwrap_or(0),
119        _ => 0,
120    }
121}
122
123fn b64(v: &Value) -> Vec<u8> {
124    v.as_str()
125        .and_then(|s| base64::engine::general_purpose::STANDARD.decode(s).ok())
126        .unwrap_or_default()
127}
128
129impl Luks2Header {
130    /// Parse a LUKS2 header from a buffer covering the binary header and JSON area.
131    ///
132    /// # Errors
133    /// [`LuksError::NotLuks`] / [`LuksError::UnsupportedVersion`] on a bad
134    /// signature/version, [`LuksError::MalformedHeader`] if the JSON is absent or
135    /// invalid.
136    pub fn parse(data: &[u8]) -> Result<Self> {
137        let magic = bytes_n::<6>(data, 0);
138        if magic != LUKS_MAGIC {
139            return Err(LuksError::NotLuks { found: magic });
140        }
141        let version = be_u16(data, 6);
142        if version != 2 {
143            return Err(LuksError::UnsupportedVersion { version });
144        }
145        let hdr_size = be_u64(data, 8);
146
147        let end = (hdr_size as usize).min(data.len());
148        let json_area = data
149            .get(LUKS2_BIN_HDR_LEN..end)
150            .ok_or(LuksError::MalformedHeader {
151                what: "json area",
152                need: LUKS2_BIN_HDR_LEN,
153                got: data.len(),
154            })?;
155        let json_end = json_area
156            .iter()
157            .position(|&b| b == 0)
158            .unwrap_or(json_area.len());
159        let root: Value = serde_json::from_slice(&json_area[..json_end]).map_err(|_| {
160            LuksError::MalformedHeader {
161                what: "json parse",
162                need: json_end,
163                got: json_area.len(),
164            }
165        })?;
166
167        let keyslots = root
168            .get("keyslots")
169            .and_then(Value::as_object)
170            .map(|m| {
171                m.iter()
172                    .filter_map(|(id, v)| parse_keyslot(id, v))
173                    .collect()
174            })
175            .unwrap_or_default();
176        let segments = root
177            .get("segments")
178            .and_then(Value::as_object)
179            .map(|m| {
180                m.iter()
181                    .filter_map(|(id, v)| parse_segment(id, v))
182                    .collect()
183            })
184            .unwrap_or_default();
185        let digests = root
186            .get("digests")
187            .and_then(Value::as_object)
188            .map(|m| m.values().filter_map(parse_digest).collect())
189            .unwrap_or_default();
190
191        Ok(Luks2Header {
192            version,
193            hdr_size,
194            keyslots,
195            segments,
196            digests,
197        })
198    }
199
200    /// The first `crypt` data segment (the one we decrypt).
201    #[must_use]
202    pub fn crypt_segment(&self) -> Option<&Luks2Segment> {
203        self.segments.iter().find(|s| !s.encryption.is_empty())
204    }
205}
206
207fn parse_keyslot(id: &str, v: &Value) -> Option<Luks2Keyslot> {
208    let af = v.get("af")?;
209    let area = v.get("area")?;
210    let kdf_v = v.get("kdf")?;
211    let kdf = match kdf_v.get("type").and_then(Value::as_str)? {
212        "pbkdf2" => Luks2Kdf::Pbkdf2 {
213            hash: kdf_v
214                .get("hash")
215                .and_then(Value::as_str)
216                .unwrap_or("sha256")
217                .to_string(),
218            iterations: as_u64(kdf_v.get("iterations")?) as u32,
219            salt: b64(kdf_v.get("salt")?),
220        },
221        kind @ ("argon2i" | "argon2id") => Luks2Kdf::Argon2 {
222            kind: kind.to_string(),
223            time: as_u64(kdf_v.get("time")?) as u32,
224            memory: as_u64(kdf_v.get("memory")?) as u32,
225            cpus: as_u64(kdf_v.get("cpus")?) as u32,
226            salt: b64(kdf_v.get("salt")?),
227        },
228        _ => return None,
229    };
230    Some(Luks2Keyslot {
231        id: id.to_string(),
232        key_size: as_u64(v.get("key_size")?) as usize,
233        af_stripes: as_u64(af.get("stripes")?) as u32,
234        af_hash: af
235            .get("hash")
236            .and_then(Value::as_str)
237            .unwrap_or("sha256")
238            .to_string(),
239        area_offset: as_u64(area.get("offset")?),
240        area_size: as_u64(area.get("size")?),
241        area_encryption: area.get("encryption").and_then(Value::as_str)?.to_string(),
242        kdf,
243    })
244}
245
246fn parse_segment(id: &str, v: &Value) -> Option<Luks2Segment> {
247    Some(Luks2Segment {
248        id: id.to_string(),
249        offset: as_u64(v.get("offset")?),
250        encryption: v.get("encryption").and_then(Value::as_str)?.to_string(),
251        sector_size: as_u64(v.get("sector_size")?) as usize,
252        iv_tweak: u128::from(as_u64(v.get("iv_tweak").unwrap_or(&Value::Null))),
253    })
254}
255
256fn parse_digest(v: &Value) -> Option<Luks2Digest> {
257    Some(Luks2Digest {
258        kind: v.get("type").and_then(Value::as_str)?.to_string(),
259        hash: v
260            .get("hash")
261            .and_then(Value::as_str)
262            .unwrap_or("sha256")
263            .to_string(),
264        iterations: as_u64(v.get("iterations").unwrap_or(&Value::Null)) as u32,
265        salt: b64(v.get("salt")?),
266        digest: b64(v.get("digest")?),
267        keyslots: v
268            .get("keyslots")
269            .and_then(Value::as_array)
270            .map(|a| {
271                a.iter()
272                    .filter_map(|x| x.as_str().map(str::to_string))
273                    .collect()
274            })
275            .unwrap_or_default(),
276    })
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    fn build_luks2(json: &str) -> Vec<u8> {
284        let hdr_size = LUKS2_BIN_HDR_LEN + 12288;
285        let mut buf = vec![0u8; hdr_size];
286        buf[0..6].copy_from_slice(&LUKS_MAGIC);
287        buf[6..8].copy_from_slice(&2u16.to_be_bytes());
288        buf[8..16].copy_from_slice(&(hdr_size as u64).to_be_bytes());
289        buf[LUKS2_BIN_HDR_LEN..LUKS2_BIN_HDR_LEN + json.len()].copy_from_slice(json.as_bytes());
290        buf
291    }
292
293    const ORACLE_JSON: &str = r#"{
294      "keyslots":{"0":{"type":"luks2","key_size":64,
295        "af":{"type":"luks1","stripes":4000,"hash":"sha256"},
296        "area":{"type":"raw","offset":"32768","size":"258048","encryption":"aes-xts-plain64","key_size":64},
297        "kdf":{"type":"argon2id","time":4,"memory":32,"cpus":1,"salt":"pUcL4RKUCh/iGgNoo6PW95eKTZm6RsS4Ds7Kn4gCdVs="}}},
298      "segments":{"0":{"type":"crypt","offset":"16777216","size":"dynamic","iv_tweak":"0","encryption":"aes-xts-plain64","sector_size":4096}},
299      "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"segments":["0"],"hash":"sha256","iterations":1000,
300        "salt":"IUaJeXA4vB2CruGetgh6GOlfgvB2WoD2UzWGb2K6KLc=","digest":"yqVonIxK8BFwHFIsUaacNsXcz65r6EnYBVgF2fDPgps="}}}"#;
301
302    #[test]
303    fn parses_luks2_keyslot_segment_digest() {
304        let h = Luks2Header::parse(&build_luks2(ORACLE_JSON)).unwrap();
305        assert_eq!(h.version, 2);
306        assert_eq!(h.keyslots.len(), 1);
307        let k = &h.keyslots[0];
308        assert_eq!(k.key_size, 64);
309        assert_eq!(k.af_stripes, 4000);
310        assert_eq!(k.area_offset, 32768);
311        assert_eq!(k.area_encryption, "aes-xts-plain64");
312        match &k.kdf {
313            Luks2Kdf::Argon2 {
314                kind,
315                time,
316                memory,
317                cpus,
318                salt,
319            } => {
320                assert_eq!(kind, "argon2id");
321                assert_eq!((*time, *memory, *cpus), (4, 32, 1));
322                assert_eq!(salt.len(), 32);
323            }
324            Luks2Kdf::Pbkdf2 { .. } => panic!("expected argon2id"), // cov:unreachable: fixture keyslot 0 is always argon2id
325        }
326        let seg = h.crypt_segment().unwrap();
327        assert_eq!(seg.offset, 16_777_216);
328        assert_eq!(seg.sector_size, 4096);
329        assert_eq!(seg.iv_tweak, 0);
330        assert_eq!(h.digests.len(), 1);
331        assert_eq!(h.digests[0].kind, "pbkdf2");
332        assert_eq!(h.digests[0].digest.len(), 32);
333        assert_eq!(h.digests[0].keyslots, vec!["0".to_string()]);
334    }
335
336    #[test]
337    fn rejects_non_luks2() {
338        assert!(matches!(
339            Luks2Header::parse(&[0u8; 5000]).unwrap_err(),
340            LuksError::NotLuks { .. }
341        ));
342    }
343
344    #[test]
345    fn rejects_wrong_version() {
346        let mut b = build_luks2(ORACLE_JSON);
347        b[6..8].copy_from_slice(&1u16.to_be_bytes());
348        assert!(matches!(
349            Luks2Header::parse(&b).unwrap_err(),
350            LuksError::UnsupportedVersion { version: 1 }
351        ));
352    }
353
354    #[test]
355    fn rejects_bad_json() {
356        let b = build_luks2("{not valid json");
357        assert!(matches!(
358            Luks2Header::parse(&b).unwrap_err(),
359            LuksError::MalformedHeader { .. }
360        ));
361    }
362
363    #[test]
364    fn parses_pbkdf2_keyslot_variant() {
365        let json = r#"{"keyslots":{"0":{"key_size":32,
366          "af":{"stripes":4000,"hash":"sha1"},
367          "area":{"offset":"32768","size":"128000","encryption":"aes-xts-plain64"},
368          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":50000,"salt":"IUaJeXA4vB2CruGetgh6GOlfgvB2WoD2UzWGb2K6KLc="}}},
369          "segments":{},"digests":{}}"#;
370        let h = Luks2Header::parse(&build_luks2(json)).unwrap();
371        match &h.keyslots[0].kdf {
372            Luks2Kdf::Pbkdf2 {
373                hash,
374                iterations,
375                salt,
376            } => {
377                assert_eq!(hash, "sha256");
378                assert_eq!(*iterations, 50000);
379                assert_eq!(salt.len(), 32);
380            }
381            Luks2Kdf::Argon2 { .. } => panic!("expected pbkdf2"), // cov:unreachable: fixture keyslot 0 is always pbkdf2
382        }
383    }
384
385    #[test]
386    fn as_u64_non_numeric_field_defaults_zero() {
387        // key_size given as a JSON bool exercises as_u64's `_ => 0` fallback.
388        let json = r#"{"keyslots":{"0":{"key_size":true,
389          "af":{"stripes":4000,"hash":"sha256"},
390          "area":{"offset":"32768","size":"128000","encryption":"aes-xts-plain64"},
391          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
392          "segments":{},"digests":{}}"#;
393        let h = Luks2Header::parse(&build_luks2(json)).unwrap();
394        assert_eq!(h.keyslots.len(), 1);
395        assert_eq!(h.keyslots[0].key_size, 0);
396    }
397
398    #[test]
399    fn unknown_kdf_type_drops_keyslot() {
400        // An unrecognized kdf type hits parse_keyslot's `_ => return None`, so the
401        // keyslot is dropped rather than mis-parsed.
402        let json = r#"{"keyslots":{"0":{"key_size":64,
403          "af":{"stripes":4000,"hash":"sha256"},
404          "area":{"offset":"32768","size":"128000","encryption":"aes-xts-plain64"},
405          "kdf":{"type":"scrypt","salt":"AAAA"}}},
406          "segments":{},"digests":{}}"#;
407        let h = Luks2Header::parse(&build_luks2(json)).unwrap();
408        assert!(h.keyslots.is_empty());
409    }
410}