Skip to main content

luks/
lib.rs

1#[cfg(feature = "_write")]
2use byteorder::WriteBytesExt;
3use byteorder::{BigEndian, ReadBytesExt};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::collections::HashMap;
7use std::fmt;
8use std::io::{Read, Seek, Write};
9use std::str::FromStr;
10use thiserror::Error;
11
12#[cfg(feature = "_challenge_response")]
13pub mod challenge_response;
14
15#[cfg(feature = "_challenge_response")]
16use crate::challenge_response::ChallengeResponseSlot;
17
18use aes::cipher::KeyInit;
19use base64::Engine as _;
20#[cfg(feature = "_write")]
21use rand::RngExt;
22use sha2::Sha512;
23use std::io::SeekFrom;
24use std::path::Path;
25use std::process::{Command, Stdio};
26use xts_mode::{Xts128, get_tweak_default};
27
28pub mod af;
29pub mod hash;
30pub mod kdf;
31pub mod key;
32pub mod keyslot;
33
34pub use af::{LUKS1_AF_STRIPES, Luks2Af, Luks2AfType};
35pub use hash::{
36    HASH_SHA256, HASH_SHA512, LUKS2_CHECKSUM_ALG_ID_LEN, Luks2HashAlg, SHA256_DIGEST_SIZE, SHA512_DIGEST_SIZE,
37};
38pub use kdf::Luks2Kdf;
39pub use key::{UnlockKey, VolumeKey};
40pub use keyslot::{
41    KeySlotId, Luks2Area, Luks2AreaEncryption, Luks2KeySize, Luks2Keyslot, Luks2KeyslotPriority,
42    Luks2ReencryptDirection, Luks2ReencryptMode,
43};
44
45/// A representation of a LUKS device, including its header and keyslots.
46///
47/// This structure holds the primary metadata for the encrypted device.
48#[derive(Serialize, Deserialize)]
49pub struct LuksDevice {
50    /// The LUKS header (either version 1 or 2).
51    pub header: LuksHeader,
52    /// A map of keyslot IDs to their raw, encrypted data area.
53    pub keyslots: HashMap<KeySlotId, Vec<u8>>,
54    /// The volume key, if the device has been successfully unlocked.
55    #[serde(skip)]
56    pub unlocked_key: Option<VolumeKey>,
57}
58
59impl LuksDevice {
60    /// Returns a list of keyslot IDs that are associated with a challenge-response token.
61    #[cfg(feature = "_challenge_response")]
62    pub fn get_challenge_response_keyslots(&self) -> Vec<(KeySlotId, Option<u32>, ChallengeResponseSlot)> {
63        let mut results = Vec::new();
64        match &self.header {
65            LuksHeader::V1 => {}
66            LuksHeader::V2(h) => {
67                for token in h.metadata.tokens.values() {
68                    if let Luks2Token::ChallengeResponse {
69                        keyslots,
70                        serial,
71                        slot,
72                    } = token
73                    {
74                        for id in keyslots {
75                            results.push((id.clone(), *serial, slot.clone()));
76                        }
77                    }
78                }
79            }
80        }
81        results
82    }
83
84    /// Unlocks the device with a passphrase, storing the volume key in the device.
85    pub fn unlock(&mut self, keyslot_id: &KeySlotId, key: &UnlockKey) -> Result<(), LuksError> {
86        let volume_key = self.get_volume_key(keyslot_id, key)?;
87        self.unlocked_key = Some(volume_key);
88
89        if self.verify(keyslot_id)? {
90            Ok(())
91        } else {
92            self.unlocked_key = None;
93            Err(LuksError::Kdf("Passphrase verification failed".to_string()))
94        }
95    }
96
97    /// Writes the LUKS device to a writer.
98    #[cfg(feature = "_write")]
99    pub fn to_writer<W: Write + Seek>(&self, mut writer: W) -> Result<(), LuksError> {
100        self.header.to_writer(&mut writer)?;
101
102        match &self.header {
103            LuksHeader::V1 => return Err(LuksError::UnsupportedVersion(1)),
104            LuksHeader::V2(h) => {
105                for (id, slot) in &h.metadata.keyslots {
106                    let area = slot.area();
107                    let data = self.keyslots.get(id).ok_or_else(|| {
108                        LuksError::InvalidHeader(format!("Data for keyslot {} not found", id))
109                    })?;
110                    writer.seek(std::io::SeekFrom::Start(area.offset()))?;
111                    writer.write_all(data)?;
112                }
113            }
114        }
115
116        Ok(())
117    }
118
119    /// Verifies that the current unlocked volume key matches a specific keyslot.
120    ///
121    /// # Errors
122    ///
123    /// Returns `LuksError::Locked` if the device has not been unlocked.
124    pub fn verify(&self, keyslot_id: &KeySlotId) -> Result<bool, LuksError> {
125        let volume_key = self.unlocked_key.as_ref().ok_or(LuksError::Locked)?;
126
127        let h2 = match &self.header {
128            LuksHeader::V1 => return Err(LuksError::UnsupportedVersion(1)),
129            LuksHeader::V2(h) => h,
130        };
131
132        // Find the digest associated with this keyslot
133        let (_digest_id, digest) = h2
134            .metadata
135            .digests
136            .iter()
137            .find(|(_, d)| match d {
138                Luks2Digest::Pbkdf2 { keyslots, .. } => keyslots.contains(keyslot_id),
139            })
140            .ok_or_else(|| LuksError::InvalidHeader(format!("No digest found for keyslot {}", keyslot_id)))?;
141
142        let (digest_hash, digest_salt, expected_digest, digest_iterations) = match digest {
143            Luks2Digest::Pbkdf2 {
144                hash,
145                salt,
146                digest,
147                iterations,
148                ..
149            } => (hash, salt, digest, iterations),
150        };
151
152        // 5. Verify the volume key using the digest
153        let kdf_digest_salt = base64::engine::general_purpose::STANDARD
154            .decode(digest_salt)
155            .map_err(|e| LuksError::Kdf(format!("Invalid salt base64 in digest: {}", e)))?;
156
157        let expected_bytes = base64::engine::general_purpose::STANDARD
158            .decode(expected_digest)
159            .map_err(|e| LuksError::Kdf(format!("Invalid digest base64 in digest: {}", e)))?;
160
161        let mut verification_output = vec![0u8; expected_bytes.len()];
162        if digest_hash == &Luks2HashAlg::Sha256 {
163            pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(
164                volume_key.expose_bytes(),
165                &kdf_digest_salt,
166                *digest_iterations,
167                &mut verification_output,
168            )
169            .map_err(|e| LuksError::Kdf(format!("PBKDF2 SHA256 error: {}", e)))?;
170        } else if digest_hash == &Luks2HashAlg::Sha512 {
171            pbkdf2::pbkdf2::<hmac::Hmac<Sha512>>(
172                volume_key.expose_bytes(),
173                &kdf_digest_salt,
174                *digest_iterations,
175                &mut verification_output,
176            )
177            .map_err(|e| LuksError::Kdf(format!("PBKDF2 SHA512 error: {}", e)))?;
178        } else {
179            return Err(LuksError::UnsupportedChecksumAlg(digest_hash.to_string()));
180        }
181
182        Ok(verification_output == expected_bytes)
183    }
184
185    /// Derives the volume key using a passphrase and a specific keyslot.
186    pub fn get_volume_key(&self, keyslot_id: &KeySlotId, key: &UnlockKey) -> Result<VolumeKey, LuksError> {
187        let h2 = match &self.header {
188            LuksHeader::V1 => return Err(LuksError::UnsupportedVersion(1)),
189            LuksHeader::V2(h) => h,
190        };
191
192        let keyslot = h2
193            .metadata
194            .keyslots
195            .get(keyslot_id)
196            .ok_or_else(|| LuksError::InvalidHeader(format!("Keyslot {} not found", keyslot_id)))?;
197
198        let (kdf, key_size, area, af) = match keyslot {
199            Luks2Keyslot::Luks2 {
200                kdf,
201                key_size,
202                area,
203                af,
204                ..
205            } => (kdf, u64::from(*key_size), area, af),
206            Luks2Keyslot::Reencrypt { .. } => {
207                return Err(LuksError::Kdf(
208                    "Verification for reencrypt keyslots not yet supported".to_string(),
209                ));
210            }
211        };
212
213        let Luks2Area::Raw { encryption, .. } = area else {
214            return Err(LuksError::InvalidHeader(
215                "LUKS2 keyslot must have area type 'raw'".to_string(),
216            ));
217        };
218
219        // 1. Derive the keyslot key from the passphrase
220        let keyslot_key = kdf.derive_key(key, &h2.salt, key_size as usize)?;
221        // 2. Get the encrypted data from the captured keyslots
222        let encrypted_data = self
223            .keyslots
224            .get(keyslot_id)
225            .ok_or_else(|| LuksError::InvalidHeader(format!("Data for keyslot {} not captured", keyslot_id)))?;
226
227        // 3. Decrypt the area
228        if *encryption != Luks2AreaEncryption::AesXtsPlain64 {
229            return Err(LuksError::UnsupportedChecksumAlg(format!(
230                "Area encryption {} is not supported",
231                encryption
232            )));
233        }
234
235        let mut decrypted_data = encrypted_data.clone();
236        if key_size == (AES128_KEY_SIZE as u64 * 2) {
237            let cipher_1 = aes::Aes128::new_from_slice(&keyslot_key[0..AES128_KEY_SIZE])
238                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
239            let cipher_2 = aes::Aes128::new_from_slice(&keyslot_key[AES128_KEY_SIZE..AES128_KEY_SIZE * 2])
240                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
241            let xts = Xts128::new(cipher_1, cipher_2);
242
243            for (i, chunk) in decrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
244                xts.decrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
245            }
246        } else if key_size == (AES256_KEY_SIZE as u64 * 2) {
247            let cipher_1 = aes::Aes256::new_from_slice(&keyslot_key[0..AES256_KEY_SIZE])
248                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
249            let cipher_2 = aes::Aes256::new_from_slice(&keyslot_key[AES256_KEY_SIZE..AES256_KEY_SIZE * 2])
250                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
251            let xts = Xts128::new(cipher_1, cipher_2);
252
253            for (i, chunk) in decrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
254                xts.decrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
255            }
256        } else {
257            return Err(LuksError::Kdf(format!(
258                "Unsupported key size {} for AES-XTS",
259                key_size
260            )));
261        }
262
263        // 4. Merge AF stripes to get the volume key
264        let volume_key_bytes = crate::af::merge(
265            &decrypted_data[0..(key_size as u64 * af.stripes as u64) as usize],
266            &af.hash,
267            af.stripes,
268            key_size as usize,
269        )?;
270        VolumeKey::new(volume_key_bytes)
271    }
272
273    /// Maps the LUKS device using dmsetup.
274    pub fn map_with_dmsetup(&self, name: &str, backing_device: &Path) -> Result<(), LuksError> {
275        let volume_key = self.unlocked_key.as_ref().ok_or(LuksError::Locked)?;
276
277        let h2 = match &self.header {
278            LuksHeader::V1 => return Err(LuksError::UnsupportedVersion(1)),
279            LuksHeader::V2(h) => h,
280        };
281
282        // Find the crypt segment
283        let segment = h2
284            .metadata
285            .segments
286            .iter()
287            .find_map(|(_, s)| match s {
288                Luks2Segment::Crypt { .. } => Some(s),
289            })
290            .ok_or_else(|| LuksError::InvalidHeader("No crypt segment found".to_string()))?;
291
292        let Luks2Segment::Crypt {
293            offset,
294            iv_tweak,
295            size,
296            encryption,
297            ..
298        } = segment;
299
300        // Calculate sectors
301        let num_sectors = match size {
302            Luks2SegmentSize::U64(s) => s / SECTOR_SIZE as u64,
303            Luks2SegmentSize::Dynamic => {
304                let mut file = std::fs::File::open(backing_device)?;
305                let total_size = file.seek(SeekFrom::End(0))?;
306                (total_size - offset.0) / SECTOR_SIZE as u64
307            }
308        };
309
310        // Construct the table string
311        // Table format: <start> <length> <target_type> <cipher> <key> <iv_offset> <device_path> <offset>
312        let table = format!(
313            "0 {} crypt {} {} {} {} {}\n",
314            num_sectors,
315            encryption,
316            to_hex(volume_key.expose_bytes()),
317            iv_tweak.0,
318            backing_device.to_string_lossy(),
319            offset.0 / SECTOR_SIZE as u64
320        );
321
322        // Call dmsetup create <name>
323        let mut child = Command::new("dmsetup")
324            .arg("create")
325            .arg(name)
326            .stdin(Stdio::piped())
327            .spawn()?;
328
329        // Pipe the table to stdin
330        if let Some(mut child_stdin) = child.stdin.take() {
331            child_stdin.write_all(table.as_bytes())?;
332        }
333
334        let status = child.wait()?;
335        if !status.success() {
336            return Err(LuksError::DmSetup(format!("exit code {}", status)));
337        }
338
339        Ok(())
340    }
341
342    /// Changes the unlock key for a specific keyslot without changing the volume key.
343    ///
344    /// This only updates the metadata in memory. You must call [`to_writer`] to persist the changes.
345    #[cfg(feature = "_write")]
346    pub fn change_unlock_key(
347        &mut self,
348        keyslot_id: &KeySlotId,
349        old_key: &UnlockKey,
350        new_key: &UnlockKey,
351    ) -> Result<(), LuksError> {
352        let volume_key = self.get_volume_key(keyslot_id, old_key)?;
353        self.update_keyslot(keyslot_id, new_key, &volume_key)
354    }
355
356    #[cfg(feature = "_write")]
357    fn update_keyslot(
358        &mut self,
359        keyslot_id: &KeySlotId,
360        key: &UnlockKey,
361        volume_key: &VolumeKey,
362    ) -> Result<(), LuksError> {
363        let h2 = match &mut self.header {
364            LuksHeader::V1 => return Err(LuksError::UnsupportedVersion(1)),
365            LuksHeader::V2(h) => h,
366        };
367
368        let keyslot = h2
369            .metadata
370            .keyslots
371            .get_mut(keyslot_id)
372            .ok_or_else(|| LuksError::InvalidHeader(format!("Keyslot {} not found", keyslot_id)))?;
373
374        let (kdf, key_size, area, af) = match keyslot {
375            Luks2Keyslot::Luks2 {
376                kdf,
377                key_size,
378                area,
379                af,
380                ..
381            } => (kdf, u64::from(*key_size), area, af),
382            Luks2Keyslot::Reencrypt { .. } => {
383                return Err(LuksError::Kdf(
384                    "Changing passphrase for reencrypt keyslots not supported".to_string(),
385                ));
386            }
387        };
388
389        let Luks2Area::Raw { encryption, .. } = area else {
390            return Err(LuksError::InvalidHeader(
391                "LUKS2 keyslot must have area type 'raw'".to_string(),
392            ));
393        };
394
395        // 1. Generate new KDF salt
396        let mut new_salt = vec![0u8; KDF_SALT_SIZE];
397        rand::rng().fill(&mut new_salt[..]);
398        let new_salt_b64 = base64::engine::general_purpose::STANDARD.encode(new_salt);
399
400        // Update salt in KDF
401        match kdf {
402            Luks2Kdf::Argon2i { salt, .. } => *salt = new_salt_b64,
403            Luks2Kdf::Argon2id { salt, .. } => *salt = new_salt_b64,
404            Luks2Kdf::Pbkdf2 { salt, .. } => *salt = new_salt_b64,
405        }
406
407        // 2. Derive the new keyslot key
408        let keyslot_key = kdf.derive_key(key, &h2.salt, key_size as usize)?;
409        // 3. AF-split the volume key
410        let mut random_stripes =
411            vec![0u8; (volume_key.expose_bytes().len() as u32 * (af.stripes - 1)) as usize];
412        rand::rng().fill(&mut random_stripes[..]);
413        let split_data = crate::af::split(
414            volume_key.expose_bytes(),
415            &af.hash,
416            af.stripes,
417            volume_key.expose_bytes().len(),
418            random_stripes,
419        )?;
420
421        // 4. Encrypt the area
422        if *encryption != Luks2AreaEncryption::AesXtsPlain64 {
423            return Err(LuksError::UnsupportedChecksumAlg(format!(
424                "Area encryption {} is not supported",
425                encryption
426            )));
427        }
428
429        let mut encrypted_data = split_data.clone();
430        if key_size == (AES128_KEY_SIZE as u64 * 2) {
431            let cipher_1 = aes::Aes128::new_from_slice(&keyslot_key[0..AES128_KEY_SIZE])
432                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
433            let cipher_2 = aes::Aes128::new_from_slice(&keyslot_key[AES128_KEY_SIZE..AES128_KEY_SIZE * 2])
434                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
435            let xts = Xts128::new(cipher_1, cipher_2);
436
437            for (i, chunk) in encrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
438                xts.encrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
439            }
440        } else if key_size == (AES256_KEY_SIZE as u64 * 2) {
441            let cipher_1 = aes::Aes256::new_from_slice(&keyslot_key[0..AES256_KEY_SIZE])
442                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
443            let cipher_2 = aes::Aes256::new_from_slice(&keyslot_key[AES256_KEY_SIZE..AES256_KEY_SIZE * 2])
444                .map_err(|e| LuksError::Kdf(format!("Cipher error: {}", e)))?;
445            let xts = Xts128::new(cipher_1, cipher_2);
446
447            for (i, chunk) in encrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
448                xts.encrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
449            }
450        } else {
451            return Err(LuksError::Kdf(format!(
452                "Unsupported key size {} for AES-XTS",
453                key_size
454            )));
455        }
456
457        // 5. Update self.keyslots
458        self.keyslots.insert(keyslot_id.clone(), encrypted_data);
459
460        // Add/remove tokens if necessary
461        #[cfg(feature = "_challenge_response")]
462        {
463            if let Some(cr) = key.challenge_response() {
464                // Check if a token already exists for this keyslot
465                let h2 = match &mut self.header {
466                    LuksHeader::V2(h) => h,
467                    _ => unreachable!(),
468                };
469
470                let mut found = false;
471                for token in h2.metadata.tokens.values_mut() {
472                    if let Luks2Token::ChallengeResponse {
473                        keyslots,
474                        serial: _,
475                        slot: _,
476                    } = token
477                    {
478                        if keyslots.contains(keyslot_id) {
479                            found = true;
480                            break;
481                        }
482                    }
483                }
484
485                if !found {
486                    // Create a new token
487                    let mut next_id = 0;
488                    while h2.metadata.tokens.contains_key(&next_id.to_string()) {
489                        next_id += 1;
490                    }
491
492                    match cr {
493                        crate::key::ChallengeResponseKey::Hardware { serial, slot } => {
494                            h2.metadata.tokens.insert(
495                                next_id.to_string(),
496                                Luks2Token::ChallengeResponse {
497                                    keyslots: vec![keyslot_id.clone()],
498                                    serial: *serial,
499                                    slot: slot.clone(),
500                                },
501                            );
502                        }
503                        crate::key::ChallengeResponseKey::Software { .. } => {
504                            h2.metadata.tokens.insert(
505                                next_id.to_string(),
506                                Luks2Token::ChallengeResponse {
507                                    keyslots: vec![keyslot_id.clone()],
508                                    serial: None,
509                                    slot: ChallengeResponseSlot::Slot1, // Default slot for software
510                                },
511                            );
512                        }
513                    }
514                }
515            } else {
516                // Remove token if it exists and only points to this keyslot
517                let h2 = match &mut self.header {
518                    LuksHeader::V2(h) => h,
519                    _ => unreachable!(),
520                };
521
522                let mut token_to_remove = None;
523                for (id, token) in &mut h2.metadata.tokens {
524                    if let Luks2Token::ChallengeResponse {
525                        keyslots,
526                        serial: _,
527                        slot: _,
528                    } = token
529                    {
530                        if keyslots.contains(keyslot_id) {
531                            keyslots.retain(|k| k != keyslot_id);
532                            if keyslots.is_empty() {
533                                token_to_remove = Some(id.clone());
534                            }
535                        }
536                    }
537                }
538                if let Some(id) = token_to_remove {
539                    h2.metadata.tokens.remove(&id);
540                }
541            }
542        }
543
544        Ok(())
545    }
546}
547
548/// Returns true if the device at the given path is a LUKS device.
549///
550/// This only checks the magic signature at the beginning of the device.
551pub fn is_luks_device<P: AsRef<Path>>(path: P) -> Result<bool, LuksError> {
552    let mut file = std::fs::File::open(path)?;
553    let mut buffer = [0u8; LUKS_MAGIC_SIZE];
554    match file.read_exact(&mut buffer) {
555        Ok(_) => Ok(buffer == LUKS_MAGIC),
556        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
557        Err(e) => Err(LuksError::Io(e)),
558    }
559}
560
561/// The magic signature for LUKS devices: "LUKS\xBA\xBE".
562pub const LUKS_MAGIC: [u8; 6] = *b"LUKS\xBA\xBE";
563
564/// Size of the LUKS magic signature in bytes.
565pub const LUKS_MAGIC_SIZE: usize = 6;
566/// Size of the LUKS version field in bytes.
567pub const LUKS_VERSION_SIZE: usize = 2;
568
569/// Size of the LUKS2 label field in bytes.
570pub const LUKS2_LABEL_SIZE: usize = 48;
571/// Size of the LUKS2 salt field in bytes.
572pub const LUKS2_SALT_SIZE: usize = 64;
573/// Size of the LUKS2 uuid field in bytes.
574pub const LUKS2_UUID_SIZE: usize = 40;
575/// Size of the LUKS2 subsystem field in bytes.
576pub const LUKS2_SUBSYSTEM_SIZE: usize = 48;
577/// Size of the LUKS2 checksum field in bytes.
578pub const LUKS2_CHECKSUM_SIZE: usize = 64;
579
580/// The standard sector size in bytes.
581pub const SECTOR_SIZE: usize = 512;
582
583/// The size of a KDF salt in bytes.
584pub const KDF_SALT_SIZE: usize = 32;
585
586/// The offset in bytes where the LUKS2 checksum field begins.
587pub const LUKS2_CHECKSUM_OFFSET: usize = 448;
588/// The size of the LUKS2 binary header area in bytes.
589pub const LUKS2_BINARY_HEADER_SIZE: usize = 4096;
590
591/// The block size for the AES cipher in bytes.
592pub const AES_BLOCK_SIZE: usize = 16;
593/// The key size for AES-128 in bytes.
594pub const AES128_KEY_SIZE: usize = 16;
595/// The key size for AES-256 in bytes.
596pub const AES256_KEY_SIZE: usize = 32;
597
598/// The number of anti-forensic stripes used by the LUKS1 AF feature.
599///
600/// For historical reasons, this value is always 4000.
601/// Default size of the LUKS2 JSON area in bytes.
602pub const LUKS2_DEFAULT_JSON_SIZE: u64 = 12288;
603/// Default size of the LUKS2 keyslots area in bytes.
604pub const LUKS2_DEFAULT_KEYSLOTS_SIZE: u64 = 4161536;
605
606/// Errors that can occur when working with LUKS devices.
607#[derive(Error, Debug)]
608pub enum LuksError {
609    /// An I/O error occurred.
610    #[error("IO error: {0}")]
611    Io(#[from] std::io::Error),
612    /// The LUKS magic signature is invalid or missing.
613    #[error("Invalid LUKS magic: {0:?}")]
614    InvalidMagic([u8; LUKS_MAGIC_SIZE]),
615    /// The LUKS version is not supported by this library.
616    #[error("Unsupported LUKS version: {0}")]
617    UnsupportedVersion(u16),
618    /// An error occurred while parsing JSON metadata.
619    #[error("JSON error: {0}")]
620    Json(#[from] serde_json::Error),
621    /// The LUKS2 header or metadata is malformed or invalid.
622    #[error("Invalid LUKS2 header: {0}")]
623    InvalidHeader(String),
624    /// The header checksum verification failed.
625    #[error("Checksum verification failed: expected {expected}, got {actual}")]
626    InvalidChecksum { expected: String, actual: String },
627    /// The requested checksum or hash algorithm is not supported.
628    #[error("Unsupported checksum algorithm: {0}")]
629    UnsupportedChecksumAlg(String),
630    /// An error occurred during key derivation (KDF).
631    #[error("KDF error: {0}")]
632    Kdf(String),
633    /// An error occurred when interacting with `dmsetup`.
634    #[error("dmsetup failed: {0}")]
635    DmSetup(String),
636    /// The operation requires an unlocked device, but it is currently locked.
637    #[error("Device is locked")]
638    Locked,
639    #[cfg(feature = "_challenge_response")]
640    /// An error occurred during challenge-response authentication.
641    #[error("Challenge-response error: {0}")]
642    ChallengeResponse(String),
643}
644
645/// A 64-bit unsigned integer that is represented as a decimal string in JSON.
646///
647/// This is necessary because JSON's standard number type is a double-precision floating point
648/// value (IEEE 754), which cannot accurately represent the full range of 64-bit integers
649/// without losing precision. By encoding large integers as strings, LUKS2 ensures that
650/// values like offsets and sizes remain exact.
651#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
652pub struct Luks2U64(pub u64);
653
654impl Serialize for Luks2U64 {
655    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
656    where
657        S: serde::Serializer,
658    {
659        serializer.serialize_str(&self.0.to_string())
660    }
661}
662
663impl<'de> Deserialize<'de> for Luks2U64 {
664    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
665    where
666        D: serde::Deserializer<'de>,
667    {
668        let s = String::deserialize(deserializer)?;
669        s.parse::<u64>().map(Luks2U64).map_err(serde::de::Error::custom)
670    }
671}
672
673/// A LUKS2 token, used to store additional metadata or references to external keys.
674#[derive(Debug, Clone, Serialize, Deserialize)]
675#[serde(tag = "type", rename_all = "lowercase")]
676pub enum Luks2Token {
677    /// A standard Linux kernel keyring token.
678    #[serde(rename = "luks2-keyring")]
679    Keyring {
680        /// The IDs of the keyslots associated with this token.
681        keyslots: Vec<KeySlotId>,
682        /// The description of the key in the keyring.
683        key_description: String,
684    },
685    /// A `luks-rs` specific keyring token.
686    #[serde(rename = "luks-rs-keyring")]
687    LuksRsKeyring {
688        /// The IDs of the keyslots associated with this token.
689        keyslots: Vec<KeySlotId>,
690        /// The description of the key in the keyring.
691        key_description: String,
692    },
693    #[cfg(feature = "_challenge_response")]
694    /// A challenge-response token.
695    #[serde(rename = "luks2-challenge-response")]
696    ChallengeResponse {
697        /// The IDs of the keyslots associated with this token.
698        keyslots: Vec<KeySlotId>,
699        /// The serial number of the challenge-response device.
700        serial: Option<u32>,
701        /// The slot on the device to use.
702        slot: ChallengeResponseSlot,
703    },
704}
705
706/// The size of a LUKS2 segment.
707#[derive(Debug, Clone, PartialEq, Eq)]
708pub enum Luks2SegmentSize {
709    /// A fixed size in bytes.
710    U64(u64),
711    /// A dynamic size that occupies all remaining space on the device.
712    Dynamic,
713}
714
715impl Serialize for Luks2SegmentSize {
716    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
717    where
718        S: serde::Serializer,
719    {
720        match self {
721            Luks2SegmentSize::U64(v) => serializer.serialize_str(&v.to_string()),
722            Luks2SegmentSize::Dynamic => serializer.serialize_str("dynamic"),
723        }
724    }
725}
726
727impl<'de> Deserialize<'de> for Luks2SegmentSize {
728    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
729    where
730        D: serde::Deserializer<'de>,
731    {
732        let s = String::deserialize(deserializer)?;
733        if s == "dynamic" {
734            Ok(Luks2SegmentSize::Dynamic)
735        } else {
736            s.parse::<u64>()
737                .map(Luks2SegmentSize::U64)
738                .map_err(serde::de::Error::custom)
739        }
740    }
741}
742
743/// A LUKS2 segment, representing a range of data on the device.
744#[derive(Debug, Clone, Serialize, Deserialize)]
745#[serde(tag = "type", rename_all = "lowercase")]
746pub enum Luks2Segment {
747    /// A cryptographic segment.
748    Crypt {
749        /// The offset of the segment in bytes.
750        offset: Luks2U64,
751        /// The IV tweak for the segment.
752        iv_tweak: Luks2U64,
753        /// The size of the segment.
754        size: Luks2SegmentSize,
755        /// The encryption algorithm used for this segment.
756        encryption: String,
757        /// The sector size in bytes.
758        sector_size: u32,
759    },
760}
761
762/// A LUKS2 digest, used to verify the volume key.
763#[derive(Debug, Clone, Serialize, Deserialize)]
764#[serde(tag = "type", rename_all = "lowercase")]
765pub enum Luks2Digest {
766    /// A PBKDF2-based digest.
767    Pbkdf2 {
768        /// The IDs of the keyslots associated with this digest.
769        keyslots: Vec<KeySlotId>,
770        /// The IDs of the segments associated with this digest.
771        segments: Vec<String>,
772        /// The hash algorithm used.
773        hash: Luks2HashAlg,
774        /// The number of iterations.
775        iterations: u32,
776        /// The base64-encoded salt.
777        salt: String,
778        /// The base64-encoded expected digest.
779        digest: String,
780    },
781}
782
783/// Global configuration for a LUKS2 device.
784#[derive(Debug, Clone, Serialize, Deserialize)]
785pub struct Luks2Config {
786    /// The size of the JSON area in bytes.
787    pub json_size: Luks2U64,
788    /// The size of the keyslots area in bytes.
789    pub keyslots_size: Luks2U64,
790    /// Any global flags set on the device.
791    pub flags: Option<Vec<String>>,
792}
793
794/// The complete LUKS2 metadata, typically stored in the JSON area of the header.
795#[derive(Debug, Clone, Serialize, Deserialize)]
796pub struct Luks2Metadata {
797    /// A map of keyslot IDs to their settings.
798    #[serde(deserialize_with = "crate::keyslot::deserialize_and_validate_keyslots")]
799    pub keyslots: HashMap<KeySlotId, Luks2Keyslot>,
800    /// A map of token IDs to their settings.
801    pub tokens: HashMap<String, Luks2Token>,
802    /// A map of segment IDs to their settings.
803    pub segments: HashMap<String, Luks2Segment>,
804    /// A map of digest IDs to their settings.
805    pub digests: HashMap<String, Luks2Digest>,
806    /// Global device configuration.
807    pub config: Luks2Config,
808}
809
810/// A LUKS device UUID.
811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
812#[serde(transparent)]
813pub struct LuksDeviceUuid(String);
814
815impl LuksDeviceUuid {
816    /// Returns the UUID as a string slice.
817    pub fn as_str(&self) -> &str {
818        &self.0
819    }
820}
821
822impl FromStr for LuksDeviceUuid {
823    type Err = LuksError;
824
825    fn from_str(s: &str) -> Result<Self, Self::Err> {
826        let s = s.trim_matches('\0');
827        if s.is_empty() {
828            return Err(LuksError::InvalidHeader("UUID is empty".to_string()));
829        }
830        if s.len() >= LUKS2_UUID_SIZE {
831            return Err(LuksError::InvalidHeader(format!(
832                "UUID too long: {} (max {})",
833                s.len(),
834                LUKS2_UUID_SIZE - 1
835            )));
836        }
837        if !s.chars().all(|c| c.is_ascii_hexdigit() || c == '-') {
838            return Err(LuksError::InvalidHeader(format!(
839                "UUID contains invalid characters: {}",
840                s
841            )));
842        }
843        Ok(LuksDeviceUuid(s.to_string()))
844    }
845}
846
847impl fmt::Display for LuksDeviceUuid {
848    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849        f.write_str(&self.0)
850    }
851}
852
853impl PartialEq<&str> for LuksDeviceUuid {
854    fn eq(&self, other: &&str) -> bool {
855        &self.0 == *other
856    }
857}
858
859/// The primary binary header for LUKS2.
860#[derive(Debug, Clone, Serialize, Deserialize)]
861pub struct Luks2Header {
862    /// The LUKS version (should be 2).
863    pub version: u16,
864    /// The total size of the header, including the binary part and the JSON area.
865    pub hdr_size: u64,
866    /// The sequence ID, incremented on each header update.
867    pub seqid: u64,
868    /// An optional label for the device.
869    pub label: String,
870    /// The hash algorithm used for calculating the header checksum.
871    pub checksum_alg: Luks2HashAlg,
872    /// The salt used for the header.
873    #[serde(with = "serde_arrays")]
874    pub salt: [u8; LUKS2_SALT_SIZE],
875    /// The UUID of the device.
876    pub uuid: LuksDeviceUuid,
877    /// An optional subsystem label for the device.
878    pub subsystem: String,
879    /// The offset of this header in bytes from the start of the device.
880    pub hdr_offset: u64,
881    /// The header checksum.
882    #[serde(with = "serde_arrays")]
883    pub checksum: [u8; LUKS2_CHECKSUM_SIZE],
884    /// The associated JSON metadata.
885    pub metadata: Luks2Metadata,
886}
887
888mod serde_arrays {
889    use serde::{Deserialize, Deserializer, Serialize, Serializer};
890
891    pub fn serialize<S>(bytes: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error>
892    where
893        S: Serializer,
894    {
895        bytes.as_slice().serialize(serializer)
896    }
897
898    pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 64], D::Error>
899    where
900        D: Deserializer<'de>,
901    {
902        let v: Vec<u8> = Vec::deserialize(deserializer)?;
903        v.try_into()
904            .map_err(|_| serde::de::Error::custom("expected array of length 64"))
905    }
906}
907
908impl Luks2Header {
909    /// Returns the number of configured keyslots.
910    pub fn num_keyslots(&self) -> usize {
911        self.metadata.keyslots.len()
912    }
913
914    /// Writes the header and its metadata to a writer.
915    #[cfg(feature = "_write")]
916    pub fn to_writer<W: Write + Seek>(&self, mut writer: W) -> Result<(), LuksError> {
917        // Serialize JSON metadata
918        let json_str = serde_json::to_string(&self.metadata)?;
919        let json_bytes = json_str.as_bytes();
920        let json_size = json_bytes.len() as u64;
921
922        // Calculate binary header size (fixed at 4096)
923        let binary_header_size = LUKS2_BINARY_HEADER_SIZE as u64;
924
925        // Prepare binary header buffer
926        let mut binary_header = vec![0u8; LUKS2_BINARY_HEADER_SIZE];
927        let mut cursor = std::io::Cursor::new(&mut binary_header);
928
929        // Write magic
930        cursor.write_all(&LUKS_MAGIC)?;
931
932        // Write version
933        cursor.write_u16::<BigEndian>(self.version)?;
934
935        // Write hdr_size (binary_header_size + JSON size)
936        cursor.write_u64::<BigEndian>(binary_header_size + json_size)?;
937
938        // Write seqid
939        cursor.write_u64::<BigEndian>(self.seqid)?;
940
941        // Write label (48 bytes)
942        let mut label_buf = [0u8; LUKS2_LABEL_SIZE];
943        let label_bytes = self.label.as_bytes();
944        let label_len = std::cmp::min(label_bytes.len(), LUKS2_LABEL_SIZE);
945        label_buf[..label_len].copy_from_slice(&label_bytes[..label_len]);
946        cursor.write_all(&label_buf)?;
947
948        // Write checksum algorithm (32 bytes)
949        cursor.write_all(&self.checksum_alg.to_bytes())?;
950
951        // Write salt (64 bytes)
952        cursor.write_all(&self.salt)?;
953
954        // Write uuid (40 bytes)
955        let mut uuid_buf = [0u8; LUKS2_UUID_SIZE];
956        let uuid_bytes = self.uuid.as_str().as_bytes();
957        let uuid_len = std::cmp::min(uuid_bytes.len(), LUKS2_UUID_SIZE);
958        uuid_buf[..uuid_len].copy_from_slice(&uuid_bytes[..uuid_len]);
959        cursor.write_all(&uuid_buf)?;
960
961        // Write subsystem (48 bytes)
962        let mut subsystem_buf = [0u8; LUKS2_SUBSYSTEM_SIZE];
963        let subsystem_bytes = self.subsystem.as_bytes();
964        let subsystem_len = std::cmp::min(subsystem_bytes.len(), LUKS2_SUBSYSTEM_SIZE);
965        subsystem_buf[..subsystem_len].copy_from_slice(&subsystem_bytes[..subsystem_len]);
966        cursor.write_all(&subsystem_buf)?;
967
968        // Write hdr_offset
969        cursor.write_u64::<BigEndian>(self.hdr_offset)?;
970
971        // Calculate checksum
972        if self.checksum_alg == Luks2HashAlg::Sha256 {
973            let mut hasher = Sha256::new();
974            // The checksum is calculated with the checksum field zeroed out
975            // binary_header already has it as zeroed out because it was initialized with vec![0u8; LUKS2_BINARY_HEADER_SIZE]
976            hasher.update(&binary_header);
977            hasher.update(json_bytes);
978            let calculated = hasher.finalize();
979
980            // Store checksum in the buffer
981            binary_header[LUKS2_CHECKSUM_OFFSET..LUKS2_CHECKSUM_OFFSET + SHA256_DIGEST_SIZE]
982                .copy_from_slice(calculated.as_slice());
983        } else {
984            return Err(LuksError::UnsupportedChecksumAlg(self.checksum_alg.to_string()));
985        }
986
987        // Write binary header to writer
988        writer.seek(std::io::SeekFrom::Start(self.hdr_offset))?;
989        writer.write_all(&binary_header)?;
990
991        // Write JSON metadata
992        writer.write_all(json_bytes)?;
993
994        Ok(())
995    }
996}
997
998/// A LUKS header, either version 1 or 2.
999#[derive(Debug, Clone, Serialize, Deserialize)]
1000pub enum LuksHeader {
1001    /// A LUKS1 header. Currently only used as a placeholder.
1002    V1,
1003    /// A LUKS2 header.
1004    V2(Luks2Header),
1005}
1006
1007impl LuksHeader {
1008    /// Returns the number of configured keyslots.
1009    pub fn num_keyslots(&self) -> usize {
1010        match self {
1011            LuksHeader::V1 => 8, // LUKS1 always has 8 keyslot entries in the header
1012            LuksHeader::V2(h) => h.num_keyslots(),
1013        }
1014    }
1015
1016    /// Writes the header to a writer.
1017    #[cfg(feature = "_write")]
1018    pub fn to_writer<W: Write + Seek>(&self, writer: W) -> Result<(), LuksError> {
1019        match self {
1020            LuksHeader::V1 => Err(LuksError::UnsupportedVersion(1)),
1021            LuksHeader::V2(h) => h.to_writer(writer),
1022        }
1023    }
1024
1025    /// Opens a LUKS device from a reader.
1026    ///
1027    /// This reads the header and all keyslot data areas from the reader.
1028    pub fn open<R: Read + Seek>(mut reader: R) -> Result<LuksDevice, LuksError> {
1029        let header = Self::from_reader(&mut reader)?;
1030        let mut keyslots = HashMap::new();
1031
1032        match &header {
1033            LuksHeader::V1 => return Err(LuksError::UnsupportedVersion(1)),
1034            LuksHeader::V2(h) => {
1035                for (id, slot) in &h.metadata.keyslots {
1036                    let area = slot.area();
1037                    let mut data = vec![0u8; area.size() as usize];
1038                    reader.seek(SeekFrom::Start(area.offset()))?;
1039                    reader.read_exact(&mut data)?;
1040                    keyslots.insert(id.clone(), data);
1041                }
1042            }
1043        }
1044
1045        Ok(LuksDevice {
1046            header,
1047            keyslots,
1048            unlocked_key: None,
1049        })
1050    }
1051
1052    /// Reads a LUKS header from a reader.
1053    ///
1054    /// This only reads the header metadata, not the keyslot data areas.
1055    pub fn from_reader<R: Read + Seek>(mut reader: R) -> Result<Self, LuksError> {
1056        let mut magic = [0u8; LUKS_MAGIC_SIZE];
1057        reader.read_exact(&mut magic)?;
1058
1059        if magic != LUKS_MAGIC {
1060            return Err(LuksError::InvalidMagic(magic));
1061        }
1062
1063        let version = reader.read_u16::<BigEndian>()?;
1064        match version {
1065            1 => Ok(LuksHeader::V1),
1066            2 => {
1067                // Read the rest of the 4096-byte binary header
1068                let mut binary_header = vec![0u8; LUKS2_BINARY_HEADER_SIZE];
1069                binary_header[0..LUKS_MAGIC_SIZE].copy_from_slice(&magic);
1070                binary_header[LUKS_MAGIC_SIZE..LUKS_MAGIC_SIZE + LUKS_VERSION_SIZE]
1071                    .copy_from_slice(&version.to_be_bytes());
1072
1073                reader.read_exact(&mut binary_header[LUKS_MAGIC_SIZE + LUKS_VERSION_SIZE..])?;
1074
1075                let mut cursor = std::io::Cursor::new(&binary_header[LUKS_MAGIC_SIZE + LUKS_VERSION_SIZE..]);
1076
1077                let hdr_size = cursor.read_u64::<BigEndian>()?;
1078                let seqid = cursor.read_u64::<BigEndian>()?;
1079
1080                let mut label_buf = [0u8; LUKS2_LABEL_SIZE];
1081                cursor.read_exact(&mut label_buf)?;
1082                let label = String::from_utf8_lossy(&label_buf).trim_matches('\0').to_string();
1083
1084                let mut checksum_alg_buf = [0u8; LUKS2_CHECKSUM_ALG_ID_LEN];
1085                cursor.read_exact(&mut checksum_alg_buf)?;
1086                let checksum_alg = Luks2HashAlg::from_str(&String::from_utf8_lossy(&checksum_alg_buf))?;
1087
1088                let mut salt = [0u8; LUKS2_SALT_SIZE];
1089                cursor.read_exact(&mut salt)?;
1090
1091                let mut uuid_buf = [0u8; LUKS2_UUID_SIZE];
1092                cursor.read_exact(&mut uuid_buf)?;
1093                let uuid = LuksDeviceUuid::from_str(&String::from_utf8_lossy(&uuid_buf))?;
1094
1095                let mut subsystem_buf = [0u8; LUKS2_SUBSYSTEM_SIZE];
1096                cursor.read_exact(&mut subsystem_buf)?;
1097                let subsystem = String::from_utf8_lossy(&subsystem_buf)
1098                    .trim_matches('\0')
1099                    .to_string();
1100
1101                // TODO must match the physical header offset on the device (in bytes). If it does
1102                // not match, the header is misplaced and must not be used. It is a prevention to
1103                // partition resize or manipulation with the device start offset.
1104                let hdr_offset = cursor.read_u64::<BigEndian>()?;
1105
1106                let mut checksum = [0u8; LUKS2_CHECKSUM_SIZE];
1107                checksum.copy_from_slice(
1108                    &binary_header[LUKS2_CHECKSUM_OFFSET..LUKS2_CHECKSUM_OFFSET + LUKS2_CHECKSUM_SIZE],
1109                );
1110
1111                // Read JSON area
1112                if hdr_size < LUKS2_BINARY_HEADER_SIZE as u64 {
1113                    return Err(LuksError::InvalidHeader(format!(
1114                        "Header size {} is too small",
1115                        hdr_size
1116                    )));
1117                }
1118                let json_size = hdr_size - LUKS2_BINARY_HEADER_SIZE as u64;
1119                let mut json_buf = vec![0u8; json_size as usize];
1120                reader.read_exact(&mut json_buf)?;
1121
1122                // Verify checksum
1123                if checksum_alg == Luks2HashAlg::Sha256 {
1124                    let mut hasher = Sha256::new();
1125
1126                    // The checksum is calculated with the checksum field itself zeroed out
1127                    let mut csum_buf = binary_header.clone();
1128                    for i in 0..LUKS2_CHECKSUM_SIZE {
1129                        csum_buf[LUKS2_CHECKSUM_OFFSET + i] = 0;
1130                    }
1131                    hasher.update(&csum_buf);
1132                    hasher.update(&json_buf);
1133
1134                    let calculated = hasher.finalize();
1135                    // SHA256 is 32 bytes, but the checksum field is 64 bytes (padded with zeros)
1136                    if calculated.as_slice() != &checksum[0..SHA256_DIGEST_SIZE] {
1137                        return Err(LuksError::InvalidChecksum {
1138                            expected: to_hex(&checksum[0..SHA256_DIGEST_SIZE]),
1139                            actual: to_hex(calculated.as_slice()),
1140                        });
1141                    }
1142                } else {
1143                    return Err(LuksError::UnsupportedChecksumAlg(checksum_alg.to_string()));
1144                }
1145
1146                // Parse JSON
1147                let json_str = String::from_utf8_lossy(&json_buf).trim_matches('\0').to_string();
1148                let metadata: Luks2Metadata = serde_json::from_str(&json_str)?;
1149
1150                Ok(LuksHeader::V2(Luks2Header {
1151                    version,
1152                    hdr_size,
1153                    seqid,
1154                    label,
1155                    checksum_alg,
1156                    salt,
1157                    uuid,
1158                    subsystem,
1159                    hdr_offset,
1160                    checksum,
1161                    metadata,
1162                }))
1163            }
1164            _ => Err(LuksError::UnsupportedVersion(version)),
1165        }
1166    }
1167}
1168
1169fn to_hex(bytes: &[u8]) -> String {
1170    bytes.iter().map(|b| format!("{:02x}", b)).collect()
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use super::*;
1176    #[cfg(feature = "_write")]
1177    use byteorder::BigEndian;
1178    #[cfg(feature = "_write")]
1179    use byteorder::WriteBytesExt;
1180    #[cfg(feature = "_write")]
1181    use std::io::{Cursor, Write};
1182
1183    #[test]
1184    #[cfg(feature = "_write")]
1185    fn test_detect_luks2_with_checksum() {
1186        let mut binary_header = vec![0u8; LUKS2_BINARY_HEADER_SIZE];
1187        let json_data = format!(
1188            r#"{{
1189            "keyslots": {{}},
1190            "tokens": {{}},
1191            "segments": {{}},
1192            "digests": {{}},
1193            "config": {{
1194                "json_size": "{}",
1195                "keyslots_size": "{}"
1196            }}
1197        }}"#,
1198            LUKS2_DEFAULT_JSON_SIZE, LUKS2_DEFAULT_KEYSLOTS_SIZE
1199        );
1200        let hdr_size = LUKS2_BINARY_HEADER_SIZE as u64 + json_data.len() as u64;
1201
1202        {
1203            let mut cursor = Cursor::new(&mut binary_header);
1204            cursor.write_all(&LUKS_MAGIC).unwrap();
1205            cursor.write_u16::<BigEndian>(2).unwrap();
1206            cursor.write_u64::<BigEndian>(hdr_size).unwrap();
1207            cursor.write_u64::<BigEndian>(1).unwrap(); // seqid
1208
1209            let mut label = [0u8; LUKS2_LABEL_SIZE];
1210            label[..4].copy_from_slice(b"test");
1211            cursor.write_all(&label).unwrap();
1212
1213            cursor.write_all(&Luks2HashAlg::Sha256.to_bytes()).unwrap();
1214
1215            cursor.write_all(&[0u8; LUKS2_SALT_SIZE]).unwrap();
1216
1217            let mut uuid = [0u8; LUKS2_UUID_SIZE];
1218            uuid[..4].copy_from_slice(b"abcd");
1219            cursor.write_all(&uuid).unwrap();
1220
1221            let subsystem = [0u8; LUKS2_SUBSYSTEM_SIZE];
1222            cursor.write_all(&subsystem).unwrap();
1223
1224            cursor.write_u64::<BigEndian>(0).unwrap(); // hdr_offset
1225        }
1226
1227        // Calculate checksum (with csum field as zeros, which it already is in our buffer)
1228        let mut hasher = Sha256::new();
1229        hasher.update(&binary_header);
1230        hasher.update(json_data.as_bytes());
1231        let result = hasher.finalize();
1232
1233        // Put checksum into binary header
1234        binary_header[LUKS2_CHECKSUM_OFFSET..LUKS2_CHECKSUM_OFFSET + SHA256_DIGEST_SIZE]
1235            .copy_from_slice(&result);
1236
1237        let mut buf = binary_header;
1238        buf.extend_from_slice(json_data.as_bytes());
1239
1240        let cursor = Cursor::new(buf);
1241        let header = LuksHeader::from_reader(cursor).unwrap();
1242
1243        if let LuksHeader::V2(h) = header {
1244            assert_eq!(h.version, 2);
1245            assert_eq!(h.label, "test");
1246            assert_eq!(h.uuid, "abcd");
1247        } else {
1248            panic!("Expected LUKS2 header");
1249        }
1250    }
1251
1252    #[test]
1253    #[cfg(feature = "_write")]
1254    fn test_device_roundtrip() {
1255        use aes::cipher::KeyInit;
1256        use base64::Engine;
1257        use rand::RngExt;
1258        use xts_mode::{Xts128, get_tweak_default};
1259        let mut rng = rand::rng();
1260
1261        // 1. Setup metadata
1262        let mut salt = [0u8; LUKS2_SALT_SIZE];
1263        rng.fill(&mut salt);
1264
1265        let volume_key_size = AES128_KEY_SIZE * 2;
1266        let volume_key = vec![0x42u8; volume_key_size];
1267        let passphrase = "correct horse battery staple";
1268        let unlock_key = UnlockKey::from(passphrase);
1269
1270        // Keyslot 0
1271        let mut keyslot_salt = [0u8; 32];
1272        rng.fill(&mut keyslot_salt);
1273        let keyslot_salt_b64 = base64::engine::general_purpose::STANDARD.encode(keyslot_salt);
1274
1275        let kdf = Luks2Kdf::Pbkdf2 {
1276            hash: Luks2HashAlg::Sha256,
1277            iterations: 1000,
1278            salt: keyslot_salt_b64,
1279        };
1280
1281        let keyslot_key = kdf.derive_key(&unlock_key, &salt, volume_key_size).unwrap();
1282
1283        // AF split the volume key
1284        let mut random_stripes = vec![0u8; volume_key_size * (LUKS1_AF_STRIPES - 1) as usize];
1285        rng.fill(&mut random_stripes[..]);
1286        let encrypted_keyslot_data = crate::af::split(
1287            &volume_key,
1288            &Luks2HashAlg::Sha256,
1289            LUKS1_AF_STRIPES,
1290            volume_key_size,
1291            random_stripes,
1292        )
1293        .unwrap();
1294
1295        // Encrypt the AF stripes with the keyslot key
1296        let mut encrypted_data = encrypted_keyslot_data.clone();
1297        let cipher_1 = aes::Aes128::new_from_slice(&keyslot_key[0..AES128_KEY_SIZE]).unwrap();
1298        let cipher_2 = aes::Aes128::new_from_slice(&keyslot_key[AES128_KEY_SIZE..AES128_KEY_SIZE * 2]).unwrap();
1299        let xts = Xts128::new(cipher_1, cipher_2);
1300        for (i, chunk) in encrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
1301            xts.encrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
1302        }
1303
1304        let keyslot = Luks2Keyslot::Luks2 {
1305            key_size: Luks2KeySize::Size32,
1306            priority: Some(Luks2KeyslotPriority::Normal),
1307            af: Luks2Af {
1308                af_type: Luks2AfType::Luks1,
1309                stripes: LUKS1_AF_STRIPES,
1310                hash: Luks2HashAlg::Sha256,
1311            },
1312            area: Luks2Area::Raw {
1313                encryption: Luks2AreaEncryption::AesXtsPlain64,
1314                key_size: Luks2KeySize::Size32,
1315                offset: Luks2U64(32768),
1316                size: Luks2U64(encrypted_data.len() as u64),
1317            },
1318            kdf,
1319        };
1320
1321        // Digest
1322        let mut digest_salt = [0u8; 32];
1323        rng.fill(&mut digest_salt);
1324        let digest_salt_b64 = base64::engine::general_purpose::STANDARD.encode(digest_salt);
1325
1326        let mut expected_digest = vec![0u8; 32];
1327        pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(&volume_key, &digest_salt, 1000, &mut expected_digest).unwrap();
1328        let expected_digest_b64 = base64::engine::general_purpose::STANDARD.encode(expected_digest);
1329
1330        let digest = Luks2Digest::Pbkdf2 {
1331            keyslots: vec![KeySlotId::new("0")],
1332            segments: vec!["0".to_string()],
1333            hash: Luks2HashAlg::Sha256,
1334            iterations: 1000,
1335            salt: digest_salt_b64,
1336            digest: expected_digest_b64,
1337        };
1338
1339        let segment = Luks2Segment::Crypt {
1340            offset: Luks2U64(16777216), // 16MB offset
1341            iv_tweak: Luks2U64(0),
1342            size: Luks2SegmentSize::Dynamic,
1343            encryption: "aes-xts-plain64".to_string(),
1344            sector_size: 512,
1345        };
1346
1347        let mut keyslots = HashMap::new();
1348        keyslots.insert(KeySlotId::new("0"), keyslot);
1349
1350        let mut digests = HashMap::new();
1351        digests.insert("0".to_string(), digest);
1352
1353        let mut segments = HashMap::new();
1354        segments.insert("0".to_string(), segment);
1355
1356        let header = Luks2Header {
1357            version: 2,
1358            hdr_size: 0, // Will be recalculated
1359            seqid: 1,
1360            label: "test".to_string(),
1361            checksum_alg: Luks2HashAlg::Sha256,
1362            salt,
1363            uuid: LuksDeviceUuid::from_str("00000000-0000-0000-0000-000000000000").unwrap(),
1364            subsystem: "test".to_string(),
1365            hdr_offset: 0,
1366            checksum: [0u8; LUKS2_CHECKSUM_SIZE],
1367            metadata: Luks2Metadata {
1368                keyslots,
1369                tokens: HashMap::new(),
1370                segments,
1371                digests,
1372                config: Luks2Config {
1373                    json_size: Luks2U64(LUKS2_DEFAULT_JSON_SIZE),
1374                    keyslots_size: Luks2U64(LUKS2_DEFAULT_KEYSLOTS_SIZE),
1375                    flags: None,
1376                },
1377            },
1378        };
1379
1380        let mut captured_keyslots = HashMap::new();
1381        captured_keyslots.insert(KeySlotId::new("0"), encrypted_data);
1382
1383        let device = LuksDevice {
1384            header: LuksHeader::V2(header),
1385            keyslots: captured_keyslots,
1386            unlocked_key: None,
1387        };
1388
1389        // 2. Write to buffer
1390        let mut buf = Cursor::new(Vec::new());
1391        device.to_writer(&mut buf).expect("to_writer failed");
1392
1393        // 3. Read back and verify
1394        buf.set_position(0);
1395        let mut read_device = LuksHeader::open(&mut buf).expect("open failed");
1396
1397        let key = UnlockKey::from(passphrase);
1398        read_device
1399            .unlock(&KeySlotId::new("0"), &key)
1400            .expect("unlock failed");
1401        assert!(read_device.verify(&KeySlotId::new("0")).expect("verify failed"));
1402    }
1403
1404    #[test]
1405    #[cfg(feature = "_write")]
1406    fn test_change_unlock_key() {
1407        use aes::cipher::KeyInit;
1408        use base64::Engine;
1409        use rand::RngExt;
1410        use xts_mode::{Xts128, get_tweak_default};
1411        let mut rng = rand::rng();
1412
1413        const TEST_ITERATIONS: u32 = 1000;
1414
1415        // 1. Setup metadata
1416        let mut salt = [0u8; LUKS2_SALT_SIZE];
1417        rng.fill(&mut salt);
1418
1419        let volume_key_size = AES128_KEY_SIZE * 2;
1420        let volume_key = vec![0x42u8; volume_key_size];
1421        let old_passphrase = "old passphrase";
1422        let new_passphrase = "new passphrase";
1423        let old_key = UnlockKey::from(old_passphrase);
1424        let new_key = UnlockKey::from(new_passphrase);
1425
1426        // Keyslot 0
1427        let mut keyslot_salt = [0u8; KDF_SALT_SIZE];
1428        rng.fill(&mut keyslot_salt);
1429        let keyslot_salt_b64 = base64::engine::general_purpose::STANDARD.encode(keyslot_salt);
1430
1431        let kdf = Luks2Kdf::Pbkdf2 {
1432            hash: Luks2HashAlg::Sha256,
1433            iterations: TEST_ITERATIONS,
1434            salt: keyslot_salt_b64,
1435        };
1436
1437        let keyslot_key = kdf.derive_key(&old_key, &salt, volume_key_size).unwrap();
1438
1439        // AF split the volume key
1440        let mut random_stripes = vec![0u8; volume_key_size * (LUKS1_AF_STRIPES - 1) as usize];
1441        rng.fill(&mut random_stripes[..]);
1442        let encrypted_keyslot_data = crate::af::split(
1443            &volume_key,
1444            &Luks2HashAlg::Sha256,
1445            LUKS1_AF_STRIPES,
1446            volume_key_size,
1447            random_stripes,
1448        )
1449        .unwrap();
1450
1451        // Encrypt the AF stripes with the keyslot key
1452        let mut encrypted_data = encrypted_keyslot_data.clone();
1453        let cipher_1 = aes::Aes128::new_from_slice(&keyslot_key[0..AES128_KEY_SIZE]).unwrap();
1454        let cipher_2 = aes::Aes128::new_from_slice(&keyslot_key[AES128_KEY_SIZE..AES128_KEY_SIZE * 2]).unwrap();
1455        let xts = Xts128::new(cipher_1, cipher_2);
1456        for (i, chunk) in encrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
1457            xts.encrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
1458        }
1459
1460        let keyslot = Luks2Keyslot::Luks2 {
1461            key_size: Luks2KeySize::Size32,
1462            priority: Some(Luks2KeyslotPriority::Normal),
1463            af: Luks2Af {
1464                af_type: Luks2AfType::Luks1,
1465                stripes: LUKS1_AF_STRIPES,
1466                hash: Luks2HashAlg::Sha256,
1467            },
1468            area: Luks2Area::Raw {
1469                encryption: Luks2AreaEncryption::AesXtsPlain64,
1470                key_size: Luks2KeySize::Size32,
1471                offset: Luks2U64(32768),
1472                size: Luks2U64(encrypted_data.len() as u64),
1473            },
1474            kdf,
1475        };
1476
1477        // Digest
1478        let mut digest_salt = [0u8; KDF_SALT_SIZE];
1479        rng.fill(&mut digest_salt);
1480        let digest_salt_b64 = base64::engine::general_purpose::STANDARD.encode(digest_salt);
1481
1482        let mut expected_digest = vec![0u8; SHA256_DIGEST_SIZE];
1483        pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(&volume_key, &digest_salt, TEST_ITERATIONS, &mut expected_digest)
1484            .unwrap();
1485        let expected_digest_b64 = base64::engine::general_purpose::STANDARD.encode(expected_digest);
1486
1487        let ks0 = KeySlotId::new("0");
1488        let digest = Luks2Digest::Pbkdf2 {
1489            keyslots: vec![ks0.clone()],
1490            segments: vec!["0".to_string()],
1491            hash: Luks2HashAlg::Sha256,
1492            iterations: TEST_ITERATIONS,
1493            salt: digest_salt_b64,
1494            digest: expected_digest_b64,
1495        };
1496
1497        let mut keyslots = HashMap::new();
1498        keyslots.insert(ks0.clone(), keyslot);
1499
1500        let mut digests = HashMap::new();
1501        digests.insert("0".to_string(), digest);
1502
1503        let header = Luks2Header {
1504            version: 2,
1505            hdr_size: 0,
1506            seqid: 1,
1507            label: "test".to_string(),
1508            checksum_alg: Luks2HashAlg::Sha256,
1509            salt,
1510            uuid: LuksDeviceUuid::from_str("00000000-0000-0000-0000-000000000000").unwrap(),
1511            subsystem: "test".to_string(),
1512            hdr_offset: 0,
1513            checksum: [0u8; LUKS2_CHECKSUM_SIZE],
1514            metadata: Luks2Metadata {
1515                keyslots,
1516                tokens: HashMap::new(),
1517                segments: HashMap::new(),
1518                digests,
1519                config: Luks2Config {
1520                    json_size: Luks2U64(LUKS2_DEFAULT_JSON_SIZE),
1521                    keyslots_size: Luks2U64(LUKS2_DEFAULT_KEYSLOTS_SIZE),
1522                    flags: None,
1523                },
1524            },
1525        };
1526
1527        let mut captured_keyslots = HashMap::new();
1528        captured_keyslots.insert(ks0.clone(), encrypted_data);
1529
1530        let mut device = LuksDevice {
1531            header: LuksHeader::V2(header),
1532            keyslots: captured_keyslots,
1533            unlocked_key: None,
1534        };
1535
1536        // 2. Change unlock key
1537        device
1538            .change_unlock_key(&ks0, &old_key, &new_key)
1539            .expect("change_unlock_key failed");
1540
1541        // 3. Verify
1542        device.unlock(&ks0, &new_key).expect("unlock failed");
1543        assert!(device.verify(&ks0).expect("verify with new passphrase failed"));
1544
1545        device
1546            .unlock(&ks0, &old_key)
1547            .expect_err("unlock with old passphrase should fail");
1548
1549        // 4. Verify volume key is still the same
1550        let derived_volume_key = device
1551            .get_volume_key(&ks0, &new_key)
1552            .expect("get_volume_key failed");
1553        assert_eq!(volume_key, derived_volume_key.expose_bytes());
1554    }
1555
1556    #[test]
1557    #[cfg(feature = "_write")]
1558    fn test_unlock() {
1559        use aes::cipher::KeyInit;
1560        use base64::Engine;
1561        use rand::RngExt;
1562        use xts_mode::{Xts128, get_tweak_default};
1563        let mut rng = rand::rng();
1564
1565        // 1. Setup metadata
1566        let mut salt = [0u8; LUKS2_SALT_SIZE];
1567        rng.fill(&mut salt);
1568
1569        let volume_key_size = AES128_KEY_SIZE * 2;
1570        let volume_key = vec![0x42u8; volume_key_size];
1571        let passphrase = "correct horse battery staple";
1572        let key = UnlockKey::from(passphrase);
1573
1574        // Keyslot 0
1575        let mut keyslot_salt = [0u8; KDF_SALT_SIZE];
1576        rng.fill(&mut keyslot_salt);
1577        let keyslot_salt_b64 = base64::engine::general_purpose::STANDARD.encode(keyslot_salt);
1578
1579        let kdf = Luks2Kdf::Pbkdf2 {
1580            hash: Luks2HashAlg::Sha256,
1581            iterations: 1000,
1582            salt: keyslot_salt_b64,
1583        };
1584
1585        let keyslot_key = kdf.derive_key(&key, &salt, volume_key_size).unwrap();
1586
1587        // AF split the volume key
1588        let mut random_stripes = vec![0u8; volume_key_size * (LUKS1_AF_STRIPES - 1) as usize];
1589        rng.fill(&mut random_stripes[..]);
1590        let encrypted_keyslot_data = crate::af::split(
1591            &volume_key,
1592            &Luks2HashAlg::Sha256,
1593            LUKS1_AF_STRIPES,
1594            volume_key_size,
1595            random_stripes,
1596        )
1597        .unwrap();
1598
1599        // Encrypt the AF stripes with the keyslot key
1600        let mut encrypted_data = encrypted_keyslot_data.clone();
1601        let cipher_1 = aes::Aes128::new_from_slice(&keyslot_key[0..AES128_KEY_SIZE]).unwrap();
1602        let cipher_2 = aes::Aes128::new_from_slice(&keyslot_key[AES128_KEY_SIZE..AES128_KEY_SIZE * 2]).unwrap();
1603        let xts = Xts128::new(cipher_1, cipher_2);
1604        for (i, chunk) in encrypted_data.chunks_mut(SECTOR_SIZE).enumerate() {
1605            xts.encrypt_area(chunk, SECTOR_SIZE, (i as u64).into(), |t| get_tweak_default(t));
1606        }
1607
1608        let keyslot = Luks2Keyslot::Luks2 {
1609            key_size: Luks2KeySize::Size32,
1610            priority: Some(Luks2KeyslotPriority::Normal),
1611            af: Luks2Af {
1612                af_type: Luks2AfType::Luks1,
1613                stripes: LUKS1_AF_STRIPES,
1614                hash: Luks2HashAlg::Sha256,
1615            },
1616            area: Luks2Area::Raw {
1617                encryption: Luks2AreaEncryption::AesXtsPlain64,
1618                key_size: Luks2KeySize::Size32,
1619                offset: Luks2U64(32768),
1620                size: Luks2U64(encrypted_data.len() as u64),
1621            },
1622            kdf,
1623        };
1624
1625        // Digest
1626        let mut digest_salt = [0u8; KDF_SALT_SIZE];
1627        rng.fill(&mut digest_salt);
1628        let digest_salt_b64 = base64::engine::general_purpose::STANDARD.encode(digest_salt);
1629
1630        let mut expected_digest = vec![0u8; SHA256_DIGEST_SIZE];
1631        pbkdf2::pbkdf2::<hmac::Hmac<Sha256>>(&volume_key, &digest_salt, 1000, &mut expected_digest).unwrap();
1632        let expected_digest_b64 = base64::engine::general_purpose::STANDARD.encode(expected_digest);
1633
1634        let ks0 = KeySlotId::new("0");
1635        let digest = Luks2Digest::Pbkdf2 {
1636            keyslots: vec![ks0.clone()],
1637            segments: vec!["0".to_string()],
1638            hash: Luks2HashAlg::Sha256,
1639            iterations: 1000,
1640            salt: digest_salt_b64,
1641            digest: expected_digest_b64,
1642        };
1643
1644        let mut keyslots = HashMap::new();
1645        keyslots.insert(ks0.clone(), keyslot);
1646
1647        let mut digests = HashMap::new();
1648        digests.insert("0".to_string(), digest);
1649
1650        let header = Luks2Header {
1651            version: 2,
1652            hdr_size: 0,
1653            seqid: 1,
1654            label: "test".to_string(),
1655            checksum_alg: Luks2HashAlg::Sha256,
1656            salt,
1657            uuid: LuksDeviceUuid::from_str("00000000-0000-0000-0000-000000000000").unwrap(),
1658            subsystem: "test".to_string(),
1659            hdr_offset: 0,
1660            checksum: [0u8; LUKS2_CHECKSUM_SIZE],
1661            metadata: Luks2Metadata {
1662                keyslots,
1663                tokens: HashMap::new(),
1664                segments: HashMap::new(),
1665                digests,
1666                config: Luks2Config {
1667                    json_size: Luks2U64(LUKS2_DEFAULT_JSON_SIZE),
1668                    keyslots_size: Luks2U64(LUKS2_DEFAULT_KEYSLOTS_SIZE),
1669                    flags: None,
1670                },
1671            },
1672        };
1673
1674        let mut captured_keyslots = HashMap::new();
1675        captured_keyslots.insert(ks0.clone(), encrypted_data);
1676
1677        let mut device = LuksDevice {
1678            header: LuksHeader::V2(header),
1679            keyslots: captured_keyslots,
1680            unlocked_key: None,
1681        };
1682
1683        // 2. Unlock
1684        device.unlock(&ks0, &key).expect("unlock failed");
1685
1686        // 3. Verify
1687        assert!(device.unlocked_key.is_some());
1688        assert_eq!(volume_key, device.unlocked_key.as_ref().unwrap().expose_bytes());
1689    }
1690
1691    #[test]
1692    #[cfg(feature = "_write")]
1693    fn test_header_roundtrip() {
1694        let mut salt = [0u8; LUKS2_SALT_SIZE];
1695        salt[0..4].copy_from_slice(b"salt");
1696        let header = Luks2Header {
1697            version: 2,
1698            hdr_size: 16384, // Will be recalculated in to_writer anyway
1699            seqid: 1,
1700            label: "test".to_string(),
1701            checksum_alg: Luks2HashAlg::Sha256,
1702            salt,
1703            uuid: LuksDeviceUuid::from_str("00000000-0000-0000-0000-000000000000").unwrap(),
1704            subsystem: "test".to_string(),
1705            hdr_offset: 0,
1706            checksum: [0u8; LUKS2_CHECKSUM_SIZE], // Will be calculated in to_writer
1707            metadata: Luks2Metadata {
1708                keyslots: HashMap::new(),
1709                tokens: HashMap::new(),
1710                segments: HashMap::new(),
1711                digests: HashMap::new(),
1712                config: Luks2Config {
1713                    json_size: Luks2U64(LUKS2_DEFAULT_JSON_SIZE),
1714                    keyslots_size: Luks2U64(LUKS2_DEFAULT_KEYSLOTS_SIZE),
1715                    flags: None,
1716                },
1717            },
1718        };
1719
1720        let mut buf = Cursor::new(Vec::new());
1721        header.to_writer(&mut buf).expect("to_writer failed");
1722
1723        buf.set_position(0);
1724        let read_header = LuksHeader::from_reader(&mut buf).expect("from_reader failed");
1725
1726        if let LuksHeader::V2(h) = read_header {
1727            assert_eq!(h.version, header.version);
1728            assert_eq!(h.label, header.label);
1729            assert_eq!(h.uuid, header.uuid);
1730            assert_eq!(h.subsystem, header.subsystem);
1731            assert_eq!(h.checksum_alg, header.checksum_alg);
1732            assert_eq!(h.salt, header.salt);
1733            assert_eq!(h.hdr_offset, header.hdr_offset);
1734            // hdr_size is recalculated, so check if it's correct
1735            assert!(h.hdr_size >= LUKS2_BINARY_HEADER_SIZE as u64);
1736        } else {
1737            panic!("Expected LUKS2 header");
1738        }
1739    }
1740
1741    #[test]
1742    #[cfg(feature = "_write")]
1743    fn test_num_keyslots() {
1744        let mut binary_header = vec![0u8; LUKS2_BINARY_HEADER_SIZE];
1745        let json_data = format!(
1746            r#"{{
1747            "keyslots": {{
1748                "0": {{
1749                    "type": "luks2",
1750                    "key_size": 64,
1751                    "priority": 1,
1752                    "af": {{ "type": "luks1", "stripes": 4000, "hash": "{}" }},
1753                    "area": {{ "type": "raw", "encryption": "aes-xts-plain64", "key_size": 64, "offset": "32768", "size": "131072" }},
1754                    "kdf": {{ "type": "argon2i", "time": 4, "memory": 235980, "cpus": 2, "salt": "z6vz4xK7cjan92rDA5JF8O6Jk2HouV0O8DMB6GlztVk=" }}
1755                }},
1756                "1": {{
1757                    "type": "luks2",
1758                    "key_size": 64,
1759                    "priority": 1,
1760                    "af": {{ "type": "luks1", "stripes": 4000, "hash": "{}" }},
1761                    "area": {{ "type": "raw", "encryption": "aes-xts-plain64", "key_size": 64, "offset": "163840", "size": "131072" }},
1762                    "kdf": {{ "type": "pbkdf2", "hash": "sha256", "iterations": 1774240, "salt": "vWcwY3rx2fKpXW2Q6oSCNf8j5bvdJyEzB6BNXECGDsI=" }}
1763                }}
1764            }},
1765            "tokens": {{}},
1766            "segments": {{}},
1767            "digests": {{}},
1768            "config": {{
1769                "json_size": "{}",
1770                "keyslots_size": "{}"
1771            }}
1772        }}"#,
1773            HASH_SHA256, HASH_SHA256, LUKS2_DEFAULT_JSON_SIZE, LUKS2_DEFAULT_KEYSLOTS_SIZE
1774        );
1775        let hdr_size = LUKS2_BINARY_HEADER_SIZE as u64 + json_data.len() as u64;
1776
1777        {
1778            let mut cursor = Cursor::new(&mut binary_header);
1779            cursor.write_all(&LUKS_MAGIC).unwrap();
1780            cursor.write_u16::<BigEndian>(2).unwrap();
1781            cursor.write_u64::<BigEndian>(hdr_size).unwrap();
1782            cursor.write_u64::<BigEndian>(1).unwrap();
1783
1784            let label = [0u8; LUKS2_LABEL_SIZE];
1785            cursor.write_all(&label).unwrap();
1786
1787            cursor.write_all(&Luks2HashAlg::Sha256.to_bytes()).unwrap();
1788
1789            cursor.write_all(&[0u8; LUKS2_SALT_SIZE]).unwrap();
1790
1791            let mut uuid = [0u8; LUKS2_UUID_SIZE];
1792            uuid[..4].copy_from_slice(b"abcd");
1793            cursor.write_all(&uuid).unwrap();
1794
1795            let subsystem = [0u8; LUKS2_SUBSYSTEM_SIZE];
1796            cursor.write_all(&subsystem).unwrap();
1797
1798            cursor.write_u64::<BigEndian>(0).unwrap();
1799        }
1800
1801        let mut hasher = Sha256::new();
1802        hasher.update(&binary_header);
1803        hasher.update(json_data.as_bytes());
1804        let result = hasher.finalize();
1805
1806        binary_header[LUKS2_CHECKSUM_OFFSET..LUKS2_CHECKSUM_OFFSET + SHA256_DIGEST_SIZE]
1807            .copy_from_slice(&result);
1808
1809        let mut buf = binary_header;
1810        buf.extend_from_slice(json_data.as_bytes());
1811
1812        let cursor = Cursor::new(buf);
1813        let header = LuksHeader::from_reader(cursor).unwrap();
1814
1815        assert_eq!(header.num_keyslots(), 2);
1816    }
1817
1818    #[test]
1819    fn test_luks_uuid_parsing() {
1820        assert!(LuksDeviceUuid::from_str("550e8400-e29b-41d4-a716-446655440000").is_ok());
1821        assert!(LuksDeviceUuid::from_str("abcd").is_ok());
1822        assert!(LuksDeviceUuid::from_str("").is_err());
1823        assert!(LuksDeviceUuid::from_str("invalid-char!").is_err());
1824        assert!(LuksDeviceUuid::from_str(&"a".repeat(40)).is_err());
1825    }
1826
1827    #[test]
1828    fn test_parse_full_example_json() {
1829        let json_data = r#"{
1830            "keyslots": {
1831                "0": {
1832                    "type": "luks2",
1833                    "key_size": 32,
1834                    "af": {
1835                        "type": "luks1",
1836                        "stripes": 4000,
1837                        "hash": "sha256"
1838                    },
1839                    "area": {
1840                        "type": "raw",
1841                        "encryption": "aes-xts-plain64",
1842                        "key_size": 32,
1843                        "offset": "32768",
1844                        "size": "131072"
1845                    },
1846                    "kdf": {
1847                        "type": "argon2i",
1848                        "time": 4,
1849                        "memory": 235980,
1850                        "cpus": 2,
1851                        "salt": "z6vz4xK7cjan92rDA5JF8O6Jk2HouV0O8DMB6GlztVk="
1852                    }
1853                },
1854                "1": {
1855                    "type": "luks2",
1856                    "key_size": 32,
1857                    "af": {
1858                        "type": "luks1",
1859                        "stripes": 4000,
1860                        "hash": "sha256"
1861                    },
1862                    "area": {
1863                        "type": "raw",
1864                        "encryption": "aes-xts-plain64",
1865                        "key_size": 32,
1866                        "offset": "163840",
1867                        "size": "131072"
1868                    },
1869                    "kdf": {
1870                        "type": "pbkdf2",
1871                        "hash": "sha256",
1872                        "iterations": 1774240,
1873                        "salt": "vWcwY3rx2fKpXW2Q6oSCNf8j5bvdJyEzB6BNXECGDsI="
1874                    }
1875                }
1876            },
1877            "tokens": {
1878                "0": {
1879                    "type": "luks2-keyring",
1880                    "keyslots": [
1881                        "1"
1882                    ],
1883                    "key_description": "MyKeyringKeyID"
1884                }
1885            },
1886            "segments": {
1887                "0": {
1888                    "type": "crypt",
1889                    "offset": "4194304",
1890                    "iv_tweak": "0",
1891                    "size": "dynamic",
1892                    "encryption": "aes-xts-plain64",
1893                    "sector_size": 512
1894                }
1895            },
1896            "digests": {
1897                "0": {
1898                    "type": "pbkdf2",
1899                    "keyslots": [
1900                        "0",
1901                        "1"
1902                    ],
1903                    "segments": [
1904                        "0"
1905                    ],
1906                    "hash": "sha256",
1907                    "iterations": 110890,
1908                    "salt": "G8gqtKhS96IbogHyJLO+t9kmjLkx+DM3HHJqQtgc2Dk=",
1909                    "digest": "C9JWko5m+oYmjg6R0t/98cGGzLr/4UaG3hImSJMivfc="
1910                }
1911            },
1912            "config": {
1913                "json_size": "12288",
1914                "keyslots_size": "4161536",
1915                "flags": [
1916                    "allow-discards"
1917                ]
1918            }
1919        }"#;
1920        let metadata: Luks2Metadata = serde_json::from_str(json_data).unwrap();
1921        assert_eq!(metadata.keyslots.len(), 2);
1922        assert_eq!(metadata.tokens.len(), 1);
1923        assert_eq!(metadata.segments.len(), 1);
1924        assert_eq!(metadata.digests.len(), 1);
1925        assert_eq!(metadata.config.json_size, Luks2U64(12288));
1926
1927        let ks0 = metadata.keyslots.get(&KeySlotId::new("0")).unwrap();
1928        let Luks2Keyslot::Luks2 { key_size, kdf, .. } = ks0 else {
1929            panic!("Expected Luks2 keyslot")
1930        };
1931        assert_eq!(*key_size, Luks2KeySize::Size32);
1932        assert!(matches!(kdf, Luks2Kdf::Argon2i { .. }));
1933
1934        let ks1 = metadata.keyslots.get(&KeySlotId::new("1")).unwrap();
1935        let Luks2Keyslot::Luks2 { kdf, .. } = ks1 else {
1936            panic!("Expected Luks2 keyslot")
1937        };
1938        assert!(matches!(kdf, Luks2Kdf::Pbkdf2 { .. }));
1939
1940        let token0 = metadata.tokens.get("0").unwrap();
1941        assert!(matches!(token0, Luks2Token::Keyring { .. }));
1942
1943        let segment0 = metadata.segments.get("0").unwrap();
1944        let Luks2Segment::Crypt { size, .. } = segment0;
1945        assert_eq!(size, &Luks2SegmentSize::Dynamic);
1946
1947        let digest0 = metadata.digests.get("0").unwrap();
1948        assert!(matches!(digest0, Luks2Digest::Pbkdf2 { .. }));
1949    }
1950
1951    #[test]
1952    fn test_parse_luks_rs_keyring_token() {
1953        let json_data = r#"{
1954            "keyslots": {},
1955            "tokens": {
1956                "0": {
1957                    "type": "luks-rs-keyring",
1958                    "keyslots": ["0"],
1959                    "key_description": "MyLuksRsKey"
1960                }
1961            },
1962            "segments": {},
1963            "digests": {},
1964            "config": { "json_size": "12288", "keyslots_size": "4161536" }
1965        }"#;
1966        let metadata: Luks2Metadata = serde_json::from_str(json_data).unwrap();
1967        let token = metadata.tokens.get("0").unwrap();
1968        if let Luks2Token::LuksRsKeyring {
1969            keyslots,
1970            key_description,
1971        } = token
1972        {
1973            assert_eq!(keyslots, &vec![KeySlotId::new("0")]);
1974            assert_eq!(key_description, "MyLuksRsKey");
1975        } else {
1976            panic!("Expected LuksRsKeyring token");
1977        }
1978    }
1979
1980    #[test]
1981    fn test_invalid_keyslot_area_type() {
1982        let json_data = r#"{
1983            "keyslots": {
1984                "0": {
1985                    "type": "luks2",
1986                    "key_size": 32,
1987                    "af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
1988                    "area": { "type": "none", "encryption": "aes-xts-plain64", "key_size": 32, "offset": "32768", "size": "131072" },
1989                    "kdf": { "type": "argon2i", "time": 4, "memory": 235980, "cpus": 2, "salt": "salt" }
1990                }
1991            },
1992            "tokens": {},
1993            "segments": {},
1994            "digests": {},
1995            "config": { "json_size": "12288", "keyslots_size": "4161536" }
1996        }"#;
1997        let result: Result<Luks2Metadata, _> = serde_json::from_str(json_data);
1998        assert!(result.is_err());
1999        assert!(
2000            result
2001                .unwrap_err()
2002                .to_string()
2003                .contains("LUKS2 keyslot must have area type 'raw'")
2004        );
2005    }
2006
2007    #[test]
2008    fn test_parse_reencrypt_keyslot() {
2009        let json_data = r#"{
2010            "keyslots": {
2011                "0": {
2012                    "type": "reencrypt",
2013                    "mode": "reencrypt",
2014                    "direction": "forward",
2015                    "key_size": "1",
2016                    "priority": 1,
2017                    "af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
2018                    "area": { "type": "none", "encryption": "aes-xts-plain64", "key_size": 32, "offset": "32768", "size": "131072" },
2019                    "kdf": { "type": "argon2i", "time": 4, "memory": 235980, "cpus": 2, "salt": "salt" }
2020                }
2021            },
2022            "tokens": {},
2023            "segments": {},
2024            "digests": {},
2025            "config": { "json_size": "12288", "keyslots_size": "4161536" }
2026        }"#;
2027        let metadata: Luks2Metadata = serde_json::from_str(json_data).unwrap();
2028        let slot = metadata.keyslots.get(&KeySlotId::new("0")).unwrap();
2029        let Luks2Keyslot::Reencrypt {
2030            mode,
2031            direction,
2032            key_size,
2033            ..
2034        } = slot
2035        else {
2036            panic!("Expected Reencrypt keyslot")
2037        };
2038        assert_eq!(*mode, Luks2ReencryptMode::Reencrypt);
2039        assert_eq!(*direction, Luks2ReencryptDirection::Forward);
2040        assert_eq!(key_size, "1");
2041    }
2042
2043    #[test]
2044    fn test_parse_reencrypt_area_types() {
2045        let json_data = r#"{
2046            "keyslots": {
2047                "checksum_slot": {
2048                    "type": "reencrypt",
2049                    "mode": "reencrypt",
2050                    "direction": "forward",
2051                    "key_size": "1",
2052                    "af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
2053                    "area": { "type": "checksum", "hash": "sha256", "sector_size": 512, "offset": "32768", "size": "131072" },
2054                    "kdf": { "type": "argon2i", "time": 4, "memory": 235980, "cpus": 2, "salt": "salt" }
2055                },
2056                "datashift_slot": {
2057                    "type": "reencrypt",
2058                    "mode": "reencrypt",
2059                    "direction": "forward",
2060                    "key_size": "1",
2061                    "af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
2062                    "area": { "type": "datashift", "shift_size": "4096", "offset": "32768", "size": "131072" },
2063                    "kdf": { "type": "argon2i", "time": 4, "memory": 235980, "cpus": 2, "salt": "salt" }
2064                },
2065                "datashift_checksum_slot": {
2066                    "type": "reencrypt",
2067                    "mode": "reencrypt",
2068                    "direction": "forward",
2069                    "key_size": "1",
2070                    "af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
2071                    "area": { "type": "datashift-checksum", "hash": "sha256", "sector_size": 512, "shift_size": "4096", "offset": "32768", "size": "131072" },
2072                    "kdf": { "type": "argon2i", "time": 4, "memory": 235980, "cpus": 2, "salt": "salt" }
2073                }
2074            },
2075            "tokens": {},
2076            "segments": {},
2077            "digests": {},
2078            "config": { "json_size": "12288", "keyslots_size": "4161536" }
2079        }"#;
2080        let metadata: Luks2Metadata = serde_json::from_str(json_data).unwrap();
2081
2082        let checksum_slot = metadata.keyslots.get(&KeySlotId::new("checksum_slot")).unwrap();
2083        if let Luks2Keyslot::Reencrypt { area, .. } = checksum_slot {
2084            assert!(matches!(area, Luks2Area::Checksum { .. }));
2085        } else {
2086            panic!("Expected Reencrypt keyslot")
2087        }
2088
2089        let datashift_slot = metadata.keyslots.get(&KeySlotId::new("datashift_slot")).unwrap();
2090        if let Luks2Keyslot::Reencrypt { area, .. } = datashift_slot {
2091            assert!(matches!(area, Luks2Area::Datashift { .. }));
2092        } else {
2093            panic!("Expected Reencrypt keyslot")
2094        }
2095
2096        let datashift_checksum_slot = metadata
2097            .keyslots
2098            .get(&KeySlotId::new("datashift_checksum_slot"))
2099            .unwrap();
2100        if let Luks2Keyslot::Reencrypt { area, .. } = datashift_checksum_slot {
2101            assert!(matches!(area, Luks2Area::DatashiftChecksum { .. }));
2102        } else {
2103            panic!("Expected Reencrypt keyslot")
2104        }
2105    }
2106
2107    #[test]
2108    fn test_parse_argon2id_kdf() {
2109        let json_data = r#"{
2110                    "keyslots": {
2111                        "0": {
2112                            "type": "luks2",
2113                            "key_size": 32,
2114                            "af": { "type": "luks1", "stripes": 4000, "hash": "sha256" },
2115                            "area": { "type": "raw", "encryption": "aes-xts-plain64", "key_size": 32, "offset": "32768", "size": "131072" },
2116                            "kdf": { "type": "argon2id", "time": 4, "memory": 235980, "cpus": 2, "salt": "salt" }
2117                        }
2118                    },
2119                    "tokens": {},
2120                    "segments": {},
2121                    "digests": {},
2122                    "config": { "json_size": "12288", "keyslots_size": "4161536" }
2123                }"#;
2124        let metadata: Luks2Metadata = serde_json::from_str(json_data).unwrap();
2125        let slot = metadata.keyslots.get(&KeySlotId::new("0")).unwrap();
2126        let Luks2Keyslot::Luks2 { kdf, .. } = slot else {
2127            panic!("Expected Luks2 keyslot")
2128        };
2129        assert!(matches!(
2130            kdf,
2131            Luks2Kdf::Argon2id {
2132                time: 4,
2133                memory: 235980,
2134                cpus: 2,
2135                ..
2136            }
2137        ));
2138    }
2139
2140    #[test]
2141    fn test_is_luks_device() {
2142        let path = std::env::temp_dir().join("test_luks_magic");
2143        std::fs::write(&path, &LUKS_MAGIC).unwrap();
2144        assert!(is_luks_device(&path).unwrap());
2145        std::fs::remove_file(&path).unwrap();
2146
2147        let path = std::env::temp_dir().join("test_not_luks_magic");
2148        std::fs::write(&path, b"NOTLUK").unwrap();
2149        assert!(!is_luks_device(&path).unwrap());
2150        std::fs::remove_file(&path).unwrap();
2151
2152        let path = std::env::temp_dir().join("test_short_luks_magic");
2153        std::fs::write(&path, b"LUKS").unwrap();
2154        assert!(!is_luks_device(&path).unwrap());
2155        std::fs::remove_file(&path).unwrap();
2156    }
2157
2158    #[test]
2159    #[cfg(feature = "_challenge_response")]
2160    fn test_challenge_response_e2e() {
2161        use crate::hash::SHA256_DIGEST_SIZE;
2162        use std::collections::HashMap;
2163
2164        const TEST_KEY_SIZE: usize = 64;
2165        const TEST_AREA_SIZE: usize = 131072;
2166        const TEST_ITERATIONS: u32 = 1000;
2167        const TEST_VOL_KEY_SIZE: usize = 64;
2168        const TEST_CR_SECRET: &[u8] = &[0x01, 0x02, 0x03, 0x04];
2169
2170        // 1. Setup a mock LuksDevice with a password-only keyslot
2171        let json_metadata = format!(
2172            r#"{{
2173            "keyslots": {{
2174                "0": {{
2175                    "type": "luks2",
2176                    "key_size": {key_size},
2177                    "af": {{ "type": "luks1", "stripes": 4000, "hash": "sha256" }},
2178                    "area": {{ "type": "raw", "encryption": "aes-xts-plain64", "key_size": {key_size}, "offset": "{area_offset}", "size": "{area_size}" }},
2179                    "kdf": {{ "type": "argon2id", "time": 1, "memory": 1024, "cpus": 1, "salt": "c2FsdA==" }}
2180                }}
2181            }},
2182            "tokens": {{}},
2183            "segments": {{}},
2184            "digests": {{
2185                "0": {{
2186                    "type": "pbkdf2",
2187                    "keyslots": ["0"],
2188                    "segments": [],
2189                    "hash": "sha256",
2190                    "iterations": {iterations},
2191                    "salt": "c2FsdA==",
2192                    "digest": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
2193                }}
2194            }},
2195            "config": {{ "json_size": "{json_size}", "keyslots_size": "{keyslots_size}" }}
2196        }}"#,
2197            key_size = TEST_KEY_SIZE,
2198            area_offset = LUKS2_BINARY_HEADER_SIZE * 8,
2199            area_size = TEST_AREA_SIZE,
2200            iterations = TEST_ITERATIONS,
2201            json_size = LUKS2_DEFAULT_JSON_SIZE,
2202            keyslots_size = LUKS2_DEFAULT_KEYSLOTS_SIZE
2203        );
2204
2205        let metadata: Luks2Metadata = serde_json::from_str(&json_metadata).unwrap();
2206        let header = LuksHeader::V2(Luks2Header {
2207            version: 2,
2208            hdr_size: (LUKS2_BINARY_HEADER_SIZE * 4) as u64,
2209            seqid: 1,
2210            label: "test".to_string(),
2211            checksum_alg: Luks2HashAlg::Sha256,
2212            salt: [0u8; LUKS2_SALT_SIZE],
2213            uuid: LuksDeviceUuid::from_str("00000000-0000-0000-0000-000000000000").unwrap(),
2214            subsystem: "".to_string(),
2215            hdr_offset: 0,
2216            checksum: [0u8; LUKS2_CHECKSUM_SIZE],
2217            metadata,
2218        });
2219
2220        // Initialize keyslot data with some dummy data (enough for AF-split area)
2221        let mut keyslots = HashMap::new();
2222        let slot0 = KeySlotId::from("0");
2223        keyslots.insert(slot0.clone(), vec![0u8; TEST_AREA_SIZE]);
2224
2225        let mut device = LuksDevice {
2226            header,
2227            keyslots: keyslots.clone(),
2228            unlocked_key: None,
2229        };
2230
2231        // We need a real volume key that would "decrypt" our dummy area to something verifiable
2232        // But since we are testing the KDF/CR flow, we can manually set the digest to match
2233        // what PBKDF2(volume_key) would produce.
2234        let volume_key_bytes = vec![0x42u8; TEST_VOL_KEY_SIZE];
2235        let volume_key = VolumeKey::new(volume_key_bytes.clone()).unwrap();
2236
2237        // Update the digest to match our volume key so verify() works
2238        if let LuksHeader::V2(ref mut h) = device.header {
2239            if let Some(crate::Luks2Digest::Pbkdf2 { digest, salt, .. }) = h.metadata.digests.get_mut("0") {
2240                let salt_bytes = base64::engine::general_purpose::STANDARD.decode(&salt).unwrap();
2241                let mut expected_digest = vec![0u8; SHA256_DIGEST_SIZE];
2242                pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha256>>(
2243                    &volume_key_bytes,
2244                    &salt_bytes,
2245                    TEST_ITERATIONS,
2246                    &mut expected_digest,
2247                )
2248                .unwrap();
2249                *digest = base64::engine::general_purpose::STANDARD.encode(expected_digest);
2250            }
2251        }
2252
2253        let old_password = "old-password".to_string();
2254        let old_key = UnlockKey::from_passphrase(old_password);
2255
2256        // Manually "encrypt" the volume key into the keyslot area so get_volume_key works
2257        // This is necessary because we don't have a real disk.
2258        device.update_keyslot(&slot0, &old_key, &volume_key).unwrap();
2259
2260        // 2. Add challenge-response by changing the passphrase
2261        let new_password = "new-password".to_string();
2262        let new_key =
2263            UnlockKey::from_passphrase(new_password).with_software_challenge_response(TEST_CR_SECRET.to_vec());
2264
2265        device.change_unlock_key(&slot0, &old_key, &new_key).unwrap();
2266
2267        // 3. Verify unlocking works with both password and CR
2268        let mut device_to_unlock = device;
2269
2270        // Verify the library automatically created the challenge-response token
2271        let cr_slots = device_to_unlock.get_challenge_response_keyslots();
2272        assert_eq!(cr_slots.len(), 1, "Token should be automatically created");
2273        let (slot_id, _serial, _slot) = &cr_slots[0];
2274        assert_eq!(slot_id, &slot0);
2275
2276        device_to_unlock
2277            .unlock(&slot0, &new_key)
2278            .expect("Should unlock with CR");
2279        assert!(device_to_unlock.unlocked_key.is_some());
2280
2281        // 4. Verify that changing BACK to a simple password removes the token
2282        let password_only_key = UnlockKey::from_passphrase("simple-password".to_string());
2283        device_to_unlock
2284            .change_unlock_key(&slot0, &new_key, &password_only_key)
2285            .unwrap();
2286        assert_eq!(
2287            device_to_unlock.get_challenge_response_keyslots().len(),
2288            0,
2289            "Token should be removed"
2290        );
2291
2292        // 5. Verify unlocking fails with wrong CR secret (re-enroll first)
2293        device_to_unlock
2294            .change_unlock_key(&slot0, &password_only_key, &new_key)
2295            .unwrap();
2296        let wrong_cr_key = UnlockKey::from_passphrase("new-password".to_string())
2297            .with_software_challenge_response(vec![0x00, 0x00, 0x00, 0x00]);
2298        let result = device_to_unlock.get_volume_key(&slot0, &wrong_cr_key);
2299        let vk = result.unwrap();
2300        device_to_unlock.unlocked_key = Some(vk);
2301        assert!(!device_to_unlock.verify(&slot0).unwrap());
2302    }
2303}