Skip to main content

luks/
volume.rs

1//! Public API: parse a LUKS1 or LUKS2 container and unlock it from a passphrase.
2//!
3//! [`LuksVolume::unlock_with_passphrase`] auto-detects the version. Both run the
4//! same shape — parse the header, try each keyslot (KDF → decrypt AF material →
5//! AF-merge → master key), verify the master key against the digest, and return a
6//! [`DecryptedPayload`] that decrypts the data segment on demand.
7
8use std::io::{Read, Seek, SeekFrom};
9
10use crate::af;
11use crate::crypto::{derive_key, derive_key_argon2, xts_decrypt_area, Argon2Params};
12use crate::error::{LuksError, Result};
13use crate::header::{Luks1Header, LUKS1_PHDR_LEN, MK_DIGEST_LEN, SECTOR};
14use crate::header2::{Luks2Header, Luks2Kdf, LUKS2_BIN_HDR_LEN};
15
16/// Namespace for opening a LUKS volume. State lives in [`DecryptedPayload`].
17pub struct LuksVolume;
18
19/// The cipher/version facts of an unlocked volume.
20#[derive(Debug, Clone)]
21pub struct VolumeInfo {
22    /// LUKS version (1 or 2).
23    pub version: u16,
24    /// Cipher name, e.g. `aes`.
25    pub cipher_name: String,
26    /// Cipher mode fed to the sector cipher, e.g. `xts-plain64`.
27    pub cipher_mode: String,
28    /// Master-key length in bytes.
29    pub key_bytes: u32,
30    /// Encryption sector size in bytes (512 or 4096).
31    pub sector_size: usize,
32}
33
34/// Split a LUKS2 `encryption` string (`aes-xts-plain64`) into (name, mode). For a
35/// LUKS1 header the mode is already separate, so `cipher_mode_of` is the identity.
36fn split_encryption(enc: &str) -> (String, String) {
37    match enc.split_once('-') {
38        Some((name, mode)) => (name.to_string(), mode.to_string()),
39        None => (enc.to_string(), String::new()),
40    }
41}
42
43impl LuksVolume {
44    /// Unlock a LUKS1 or LUKS2 container, auto-detecting the version from the
45    /// header.
46    ///
47    /// # Errors
48    /// See [`Self::unlock1_with_passphrase`] / [`Self::unlock2_with_passphrase`].
49    pub fn unlock_with_passphrase<R: Read + Seek>(
50        mut reader: R,
51        passphrase: &[u8],
52    ) -> Result<DecryptedPayload<R>> {
53        let mut ver = [0u8; 8];
54        reader.seek(SeekFrom::Start(0))?;
55        read_fill(&mut reader, &mut ver)?;
56        reader.seek(SeekFrom::Start(0))?;
57        match crate::bytes::be_u16(&ver, 6) {
58            1 => Self::unlock1_with_passphrase(reader, passphrase),
59            2 => Self::unlock2_with_passphrase(reader, passphrase),
60            other => {
61                if ver.starts_with(&crate::header::LUKS_MAGIC) {
62                    Err(LuksError::UnsupportedVersion { version: other })
63                } else {
64                    Err(LuksError::NotLuks {
65                        found: crate::bytes::bytes_n::<6>(&ver, 0),
66                    })
67                }
68            }
69        }
70    }
71
72    /// Unlock a LUKS1 container from `reader` with `passphrase`.
73    ///
74    /// # Errors
75    /// Header-parse errors, [`LuksError::NoActiveKeyslot`], or
76    /// [`LuksError::AuthenticationFailed`] on a wrong passphrase.
77    pub fn unlock1_with_passphrase<R: Read + Seek>(
78        mut reader: R,
79        passphrase: &[u8],
80    ) -> Result<DecryptedPayload<R>> {
81        let mut hdr_buf = vec![0u8; LUKS1_PHDR_LEN];
82        reader.seek(SeekFrom::Start(0))?;
83        read_fill(&mut reader, &mut hdr_buf)?;
84        let header = Luks1Header::parse(&hdr_buf)?;
85        if header.active_keyslots().next().is_none() {
86            return Err(LuksError::NoActiveKeyslot);
87        }
88        let key_bytes = header.key_bytes as usize;
89        let master_key = recover_master_key1(&mut reader, &header, passphrase, key_bytes)?;
90        let total_size = reader.seek(SeekFrom::End(0))?;
91        Ok(DecryptedPayload {
92            reader,
93            master_key,
94            cipher_mode: header.cipher_mode.clone(),
95            payload_offset: header.payload_byte_offset(),
96            sector_size: SECTOR as usize,
97            iv_tweak: 0,
98            total_size,
99            position: 0,
100            info: VolumeInfo {
101                version: 1,
102                cipher_name: header.cipher_name.clone(),
103                cipher_mode: header.cipher_mode.clone(),
104                key_bytes: header.key_bytes,
105                sector_size: SECTOR as usize,
106            },
107        })
108    }
109
110    /// Unlock a LUKS2 container from `reader` with `passphrase`.
111    ///
112    /// # Errors
113    /// Header/JSON-parse errors, [`LuksError::NoActiveKeyslot`] if there is no
114    /// crypt segment or keyslot, or [`LuksError::AuthenticationFailed`].
115    pub fn unlock2_with_passphrase<R: Read + Seek>(
116        mut reader: R,
117        passphrase: &[u8],
118    ) -> Result<DecryptedPayload<R>> {
119        // Read the binary header to learn hdr_size, then the whole header+JSON.
120        let mut bin = vec![0u8; LUKS2_BIN_HDR_LEN];
121        reader.seek(SeekFrom::Start(0))?;
122        read_fill(&mut reader, &mut bin)?;
123        let hdr_size = crate::bytes::be_u64(&bin, 8).max(LUKS2_BIN_HDR_LEN as u64) as usize;
124        let mut full = vec![0u8; hdr_size];
125        reader.seek(SeekFrom::Start(0))?;
126        read_available(&mut reader, &mut full)?;
127        let header = Luks2Header::parse(&full)?;
128
129        let segment = header
130            .crypt_segment()
131            .cloned()
132            .ok_or(LuksError::NoActiveKeyslot)?;
133        if header.keyslots.is_empty() {
134            return Err(LuksError::NoActiveKeyslot);
135        }
136
137        let master_key = recover_master_key2(&mut reader, &header, passphrase)?;
138        let total_size = reader.seek(SeekFrom::End(0))?;
139        let (cipher_name, cipher_mode) = split_encryption(&segment.encryption);
140        let key_bytes = master_key.len() as u32;
141        Ok(DecryptedPayload {
142            reader,
143            master_key,
144            cipher_mode: cipher_mode.clone(),
145            payload_offset: segment.offset,
146            sector_size: segment.sector_size.max(512),
147            iv_tweak: segment.iv_tweak,
148            total_size,
149            position: 0,
150            info: VolumeInfo {
151                version: 2,
152                cipher_name,
153                cipher_mode,
154                key_bytes,
155                sector_size: segment.sector_size.max(512),
156            },
157        })
158    }
159}
160
161/// LUKS1: try each active keyslot until one yields a master key whose digest
162/// matches.
163fn recover_master_key1<R: Read + Seek>(
164    reader: &mut R,
165    header: &Luks1Header,
166    passphrase: &[u8],
167    key_bytes: usize,
168) -> Result<Vec<u8>> {
169    for slot in header.active_keyslots() {
170        let slot_key = derive_key(
171            &header.hash_spec,
172            passphrase,
173            &slot.salt,
174            slot.iterations,
175            key_bytes,
176        )?;
177        let material_len = af::material_len(key_bytes, slot.stripes as usize);
178        let mut material = vec![0u8; material_len];
179        reader.seek(SeekFrom::Start(
180            u64::from(slot.key_material_offset) * SECTOR,
181        ))?;
182        if read_available(reader, &mut material)? < material_len {
183            continue;
184        }
185        xts_decrypt_area(&header.cipher_mode, &slot_key, &mut material, 512, 0)?;
186        let candidate = af::merge(
187            &header.hash_spec,
188            &material,
189            key_bytes,
190            slot.stripes as usize,
191        )?; // cov:unreachable: hash_spec already validated by the slot-key derive_key above
192        let digest = derive_key(
193            &header.hash_spec,
194            &candidate,
195            &header.mk_digest_salt,
196            header.mk_digest_iter,
197            MK_DIGEST_LEN,
198        )?; // cov:unreachable: hash_spec already validated by the slot-key derive_key above
199        if digest == header.mk_digest {
200            return Ok(candidate);
201        }
202    }
203    Err(LuksError::AuthenticationFailed)
204}
205
206/// LUKS2: try each keyslot (Argon2 or PBKDF2 KDF) and verify against a digest
207/// that references it.
208fn recover_master_key2<R: Read + Seek>(
209    reader: &mut R,
210    header: &Luks2Header,
211    passphrase: &[u8],
212) -> Result<Vec<u8>> {
213    for slot in &header.keyslots {
214        let slot_key = match &slot.kdf {
215            Luks2Kdf::Argon2 {
216                kind,
217                time,
218                memory,
219                cpus,
220                salt,
221            } => derive_key_argon2(
222                &Argon2Params {
223                    kind,
224                    time: *time,
225                    memory: *memory,
226                    cpus: *cpus,
227                    salt,
228                },
229                passphrase,
230                slot.key_size,
231            )?,
232            Luks2Kdf::Pbkdf2 {
233                hash,
234                iterations,
235                salt,
236            } => derive_key(hash, passphrase, salt, *iterations, slot.key_size)?,
237        };
238
239        let (_, area_mode) = split_encryption(&slot.area_encryption);
240        let material_len = af::material_len(slot.key_size, slot.af_stripes as usize);
241        let mut material = vec![0u8; material_len];
242        reader.seek(SeekFrom::Start(slot.area_offset))?;
243        if read_available(reader, &mut material)? < material_len {
244            continue;
245        }
246        xts_decrypt_area(&area_mode, &slot_key, &mut material, 512, 0)?;
247        let candidate = af::merge(
248            &slot.af_hash,
249            &material,
250            slot.key_size,
251            slot.af_stripes as usize,
252        )?;
253
254        for dig in &header.digests {
255            if dig.kind == "pbkdf2" && dig.keyslots.iter().any(|k| k == &slot.id) {
256                let d = derive_key(
257                    &dig.hash,
258                    &candidate,
259                    &dig.salt,
260                    dig.iterations,
261                    dig.digest.len(),
262                )?;
263                if d == dig.digest {
264                    return Ok(candidate);
265                }
266            }
267        }
268    }
269    Err(LuksError::AuthenticationFailed)
270}
271
272/// A plaintext view of an unlocked LUKS payload.
273pub struct DecryptedPayload<R> {
274    reader: R,
275    master_key: Vec<u8>,
276    cipher_mode: String,
277    payload_offset: u64,
278    sector_size: usize,
279    iv_tweak: u128,
280    total_size: u64,
281    position: u64,
282    info: VolumeInfo,
283}
284
285impl<R: Read + Seek> DecryptedPayload<R> {
286    /// Cipher/version facts of the volume.
287    #[must_use]
288    pub fn info(&self) -> &VolumeInfo {
289        &self.info
290    }
291
292    /// The recovered master key (sensitive).
293    #[must_use]
294    pub fn master_key(&self) -> &[u8] {
295        &self.master_key
296    }
297
298    /// Size of the encrypted payload in bytes.
299    #[must_use]
300    pub fn payload_size(&self) -> u64 {
301        self.total_size.saturating_sub(self.payload_offset)
302    }
303
304    /// Read decrypted payload bytes at payload-relative `offset` into `buf`,
305    /// filling it completely (bytes past the end read back as zero).
306    ///
307    /// # Errors
308    /// Propagates I/O errors and any cipher error.
309    pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
310        let ss = self.sector_size as u64;
311        let step = (self.sector_size / 512).max(1) as u128;
312        let mut done = 0usize;
313        while done < buf.len() {
314            let pos = offset + done as u64;
315            let unit = pos / ss;
316            let within = (pos % ss) as usize;
317            let physical = self.payload_offset + unit * ss;
318
319            let mut ct = vec![0u8; self.sector_size];
320            self.reader.seek(SeekFrom::Start(physical))?;
321            read_available(&mut self.reader, &mut ct)?;
322            let tweak = self.iv_tweak + u128::from(unit) * step;
323            xts_decrypt_area(
324                &self.cipher_mode,
325                &self.master_key,
326                &mut ct,
327                self.sector_size,
328                tweak,
329            )?; // cov:unreachable: cipher_mode + master-key size validated during unlock
330
331            let take = (self.sector_size - within).min(buf.len() - done);
332            buf[done..done + take].copy_from_slice(&ct[within..within + take]);
333            done += take;
334        }
335        Ok(())
336    }
337}
338
339impl<R: Read + Seek> Read for DecryptedPayload<R> {
340    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
341        let size = self.payload_size();
342        if self.position >= size {
343            return Ok(0);
344        }
345        let n = (buf.len() as u64).min(size - self.position) as usize;
346        self.read_at(self.position, &mut buf[..n])
347            .map_err(|e| std::io::Error::other(e.to_string()))?;
348        self.position += n as u64;
349        Ok(n)
350    }
351}
352
353impl<R: Read + Seek> Seek for DecryptedPayload<R> {
354    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
355        let size = self.payload_size();
356        let new = match pos {
357            SeekFrom::Start(o) => i128::from(o),
358            SeekFrom::End(o) => i128::from(size) + i128::from(o),
359            SeekFrom::Current(o) => i128::from(self.position) + i128::from(o),
360        };
361        if new < 0 {
362            return Err(std::io::Error::new(
363                std::io::ErrorKind::InvalidInput,
364                "seek before start",
365            ));
366        }
367        self.position = new as u64;
368        Ok(self.position)
369    }
370}
371
372/// Read exactly `buf.len()` bytes, erroring on premature EOF.
373fn read_fill<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
374    reader.read_exact(buf)?;
375    Ok(())
376}
377
378/// Read up to `buf.len()` bytes, zero-filling the remainder on EOF. Returns the
379/// number of real bytes read.
380fn read_available<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
381    let mut filled = 0;
382    while filled < buf.len() {
383        match reader.read(&mut buf[filled..]) {
384            Ok(0) => break,
385            Ok(n) => filled += n,
386            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
387            Err(e) => return Err(e.into()),
388        }
389    }
390    for b in &mut buf[filled..] {
391        *b = 0;
392    }
393    Ok(filled)
394}
395
396#[cfg(test)]
397mod tests {
398    //! Hermetic round-trip tests: build a synthetic LUKS container in memory with
399    //! the same audited primitives, unlock it, and assert the plaintext comes
400    //! back. These are Tier-3 self-consistency scaffolding — the real correctness
401    //! proof is the `cryptsetup` oracle (`tests/oracle_luks{1,2}.rs`).
402
403    use super::*;
404    use crate::af;
405    use crate::crypto::{derive_key, derive_key_argon2, Argon2Params};
406    use crate::header::{
407        KEYSLOT_LEN, KEY_DISABLED, KEY_ENABLED, LUKS1_PHDR_LEN, LUKS_MAGIC, LUKS_NUM_KEYS,
408    };
409    use crate::header2::LUKS2_BIN_HDR_LEN;
410    use aes::cipher::KeyInit;
411    use aes::Aes256;
412    use base64::Engine;
413    use std::io::Cursor;
414    use xts_mode::Xts128;
415
416    const PASS: &[u8] = b"luks-TEST";
417
418    /// XTS-encrypt `buf` in place, one `unit_size`-byte unit at a time, with the
419    /// per-unit plain64 tweak `base + u * (unit_size/512)` — the encrypt inverse
420    /// of `xts_decrypt_area` (64-byte AES-256 key only, which is all the fixtures
421    /// use).
422    fn xts_encrypt(key: &[u8], buf: &mut [u8], unit_size: usize, base: u128) {
423        let step = (unit_size / 512).max(1) as u128;
424        let (k1, k2) = key.split_at(32);
425        let xts = Xts128::<Aes256>::new(Aes256::new(k1.into()), Aes256::new(k2.into()));
426        for (u, chunk) in buf.chunks_mut(unit_size).enumerate() {
427            let tweak = (base + u as u128 * step).to_le_bytes();
428            xts.encrypt_sector(chunk, tweak);
429        }
430    }
431
432    fn pbkdf2_sha256(pw: &[u8], salt: &[u8], iters: u32, len: usize) -> Vec<u8> {
433        let mut out = vec![0u8; len];
434        pbkdf2::pbkdf2_hmac::<sha2::Sha256>(pw, salt, iters, &mut out);
435        out
436    }
437
438    fn known_plain(len: usize) -> Vec<u8> {
439        (0..len).map(|i| (i as u8) ^ 0x3c).collect()
440    }
441
442    // ---- LUKS1 ------------------------------------------------------------
443
444    const L1_KM_OFF: u32 = 2; // sector 2 = byte 1024 (past the 592-byte phdr)
445    const L1_PAYLOAD_OFF: u32 = 4; // sector 4 = byte 2048
446    const L1_STRIPES: u32 = 8;
447    const L1_KEY_BYTES: u32 = 64;
448    const L1_ITERS: u32 = 5;
449    const L1_MK_ITER: u32 = 7;
450
451    /// Build a valid LUKS1 container (`aes` / `xts-plain64`, AES-256-XTS) unlocking
452    /// to `master`, plus the 3-sector plaintext payload. Returns (image, plain).
453    fn build_luks1(master: &[u8]) -> (Vec<u8>, Vec<u8>) {
454        let salt = [0x11u8; 32];
455        let mk_salt = [0x22u8; 32];
456        let slot_key = pbkdf2_sha256(PASS, &salt, L1_ITERS, L1_KEY_BYTES as usize);
457
458        let mut material =
459            af::af_split::<sha2::Sha256>(master, L1_KEY_BYTES as usize, L1_STRIPES as usize, 0x5a);
460        xts_encrypt(&slot_key, &mut material, 512, 0);
461
462        let mk_digest = pbkdf2_sha256(master, &mk_salt, L1_MK_ITER, 20);
463
464        let mut payload = known_plain(3 * 512);
465        let plain = payload.clone();
466        xts_encrypt(master, &mut payload, 512, 0);
467
468        let payload_byte = L1_PAYLOAD_OFF as usize * 512;
469        let mut img = vec![0u8; payload_byte + payload.len()];
470        img[0..6].copy_from_slice(&LUKS_MAGIC);
471        img[6..8].copy_from_slice(&1u16.to_be_bytes());
472        img[8..11].copy_from_slice(b"aes");
473        img[40..51].copy_from_slice(b"xts-plain64");
474        img[72..78].copy_from_slice(b"sha256");
475        img[104..108].copy_from_slice(&L1_PAYLOAD_OFF.to_be_bytes());
476        img[108..112].copy_from_slice(&L1_KEY_BYTES.to_be_bytes());
477        img[112..132].copy_from_slice(&mk_digest);
478        img[132..164].copy_from_slice(&mk_salt);
479        img[164..168].copy_from_slice(&L1_MK_ITER.to_be_bytes());
480        img[168..204].copy_from_slice(b"b22690e1-a392-4ecc-83b1-c1cf21200116");
481
482        for i in 0..LUKS_NUM_KEYS {
483            let base = 208 + i * KEYSLOT_LEN;
484            if i == 0 {
485                img[base..base + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
486                img[base + 4..base + 8].copy_from_slice(&L1_ITERS.to_be_bytes());
487                img[base + 8..base + 40].copy_from_slice(&salt);
488                img[base + 40..base + 44].copy_from_slice(&L1_KM_OFF.to_be_bytes());
489                img[base + 44..base + 48].copy_from_slice(&L1_STRIPES.to_be_bytes());
490            } else {
491                img[base..base + 4].copy_from_slice(&KEY_DISABLED.to_be_bytes());
492            }
493        }
494
495        let km_byte = L1_KM_OFF as usize * 512;
496        img[km_byte..km_byte + material.len()].copy_from_slice(&material);
497        img[payload_byte..payload_byte + payload.len()].copy_from_slice(&payload);
498        assert!(
499            LUKS1_PHDR_LEN <= km_byte,
500            "phdr must not overlap key material"
501        );
502        (img, plain)
503    }
504
505    #[test]
506    fn luks1_roundtrip_via_autodetect() {
507        let master: Vec<u8> = (0..64u8)
508            .map(|x| x.wrapping_mul(7).wrapping_add(3))
509            .collect();
510        let (img, plain) = build_luks1(&master);
511        let mut vol = LuksVolume::unlock_with_passphrase(Cursor::new(img), PASS).unwrap();
512
513        assert_eq!(vol.info().version, 1);
514        assert_eq!(vol.info().cipher_name, "aes");
515        assert_eq!(vol.info().cipher_mode, "xts-plain64");
516        assert_eq!(vol.info().key_bytes, 64);
517        assert_eq!(vol.info().sector_size, 512);
518        assert_eq!(vol.master_key(), &master[..]);
519        assert_eq!(vol.payload_size(), 3 * 512);
520
521        for lba in 0..3u64 {
522            let mut buf = [0u8; 512];
523            vol.read_at(lba * 512, &mut buf).unwrap();
524            assert_eq!(
525                &buf[..],
526                &plain[lba as usize * 512..lba as usize * 512 + 512]
527            );
528        }
529    }
530
531    #[test]
532    fn luks1_read_seek_traits() {
533        let master = vec![0xABu8; 64];
534        let (img, plain) = build_luks1(&master);
535        let mut vol = LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS).unwrap();
536
537        // Seek variants.
538        assert_eq!(vol.seek(SeekFrom::Start(512)).unwrap(), 512);
539        assert_eq!(vol.seek(SeekFrom::Current(-256)).unwrap(), 256);
540        let end = vol.seek(SeekFrom::End(0)).unwrap();
541        assert_eq!(end, 3 * 512);
542        assert!(vol.seek(SeekFrom::End(-1)).is_ok());
543        assert!(vol.seek(SeekFrom::Start(0)).is_ok());
544
545        // Read the whole payload back through Read.
546        let mut got = Vec::new();
547        std::io::Read::read_to_end(&mut vol, &mut got).unwrap();
548        assert_eq!(got, plain);
549        // At EOF a further read yields 0.
550        let mut z = [0u8; 16];
551        assert_eq!(std::io::Read::read(&mut vol, &mut z).unwrap(), 0);
552    }
553
554    #[test]
555    fn luks1_seek_before_start_errors() {
556        let master = vec![0x01u8; 64];
557        let (img, _) = build_luks1(&master);
558        let mut vol = LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS).unwrap();
559        assert!(vol.seek(SeekFrom::Start(0)).is_ok());
560        assert!(vol.seek(SeekFrom::Current(-1)).is_err());
561    }
562
563    #[test]
564    fn luks1_wrong_passphrase() {
565        let master = vec![0x5Cu8; 64];
566        let (img, _) = build_luks1(&master);
567        assert!(matches!(
568            LuksVolume::unlock1_with_passphrase(Cursor::new(img), b"wrong"),
569            Err(LuksError::AuthenticationFailed)
570        ));
571    }
572
573    #[test]
574    fn luks1_no_active_keyslot() {
575        let master = vec![0x5Cu8; 64];
576        let (mut img, _) = build_luks1(&master);
577        // Disable keyslot 0.
578        img[208..212].copy_from_slice(&KEY_DISABLED.to_be_bytes());
579        assert!(matches!(
580            LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS),
581            Err(LuksError::NoActiveKeyslot)
582        ));
583    }
584
585    #[test]
586    fn luks1_short_key_material_is_skipped() {
587        // Active keyslot whose key material lies past EOF: read_available returns
588        // short, the slot is skipped, and unlock ends in AuthenticationFailed.
589        let mut img = vec![0u8; LUKS1_PHDR_LEN];
590        img[0..6].copy_from_slice(&LUKS_MAGIC);
591        img[6..8].copy_from_slice(&1u16.to_be_bytes());
592        img[8..11].copy_from_slice(b"aes");
593        img[40..51].copy_from_slice(b"xts-plain64");
594        img[72..78].copy_from_slice(b"sha256");
595        img[104..108].copy_from_slice(&4u32.to_be_bytes());
596        img[108..112].copy_from_slice(&64u32.to_be_bytes());
597        let base = 208;
598        img[base..base + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
599        img[base + 4..base + 8].copy_from_slice(&5u32.to_be_bytes());
600        img[base + 40..base + 44].copy_from_slice(&1000u32.to_be_bytes()); // sector 1000 >> EOF
601        img[base + 44..base + 48].copy_from_slice(&8u32.to_be_bytes());
602        assert!(matches!(
603            LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS),
604            Err(LuksError::AuthenticationFailed)
605        ));
606    }
607
608    // ---- version dispatch / bad headers -----------------------------------
609
610    #[test]
611    fn not_a_luks_container() {
612        assert!(matches!(
613            LuksVolume::unlock_with_passphrase(Cursor::new(vec![0u8; 8]), PASS),
614            Err(LuksError::NotLuks { .. })
615        ));
616    }
617
618    #[test]
619    fn unsupported_version() {
620        let mut img = vec![0u8; 8];
621        img[0..6].copy_from_slice(&LUKS_MAGIC);
622        img[6..8].copy_from_slice(&3u16.to_be_bytes());
623        assert!(matches!(
624            LuksVolume::unlock_with_passphrase(Cursor::new(img), PASS),
625            Err(LuksError::UnsupportedVersion { version: 3 })
626        ));
627    }
628
629    #[test]
630    fn split_encryption_without_dash() {
631        assert_eq!(split_encryption("aes"), ("aes".to_string(), String::new()));
632        assert_eq!(
633            split_encryption("aes-xts-plain64"),
634            ("aes".to_string(), "xts-plain64".to_string())
635        );
636    }
637
638    // ---- LUKS2 ------------------------------------------------------------
639
640    const L2_AREA_OFF: u64 = 8192;
641    const L2_SEG_OFF: u64 = 12288;
642    const L2_STRIPES: usize = 8;
643    const L2_DIG_ITER: u32 = 5;
644
645    /// Assemble a LUKS2 container from a keyslot key + KDF JSON fragment, unlocking
646    /// to `master`. Returns (image, plain payload).
647    fn build_luks2(
648        master: &[u8],
649        slot_key: &[u8],
650        kdf_json: &str,
651        sector_size: usize,
652    ) -> (Vec<u8>, Vec<u8>) {
653        let b64 = base64::engine::general_purpose::STANDARD;
654        let dig_salt = [0x22u8; 16];
655        let digest = pbkdf2_sha256(master, &dig_salt, L2_DIG_ITER, 32);
656
657        let mut material = af::af_split::<sha2::Sha256>(master, 64, L2_STRIPES, 0x5a);
658        xts_encrypt(slot_key, &mut material, 512, 0);
659
660        let mut payload = known_plain(sector_size); // one data unit
661        let plain = payload.clone();
662        xts_encrypt(master, &mut payload, sector_size, 0);
663
664        let json = format!(
665            concat!(
666                "{{\"keyslots\":{{\"0\":{{\"key_size\":64,",
667                "\"af\":{{\"type\":\"luks1\",\"stripes\":{stripes},\"hash\":\"sha256\"}},",
668                "\"area\":{{\"type\":\"raw\",\"offset\":\"{area}\",\"size\":\"512\",\"encryption\":\"aes-xts-plain64\",\"key_size\":64}},",
669                "\"kdf\":{kdf}}}}},",
670                "\"segments\":{{\"0\":{{\"type\":\"crypt\",\"offset\":\"{seg}\",\"size\":\"dynamic\",\"iv_tweak\":\"0\",\"encryption\":\"aes-xts-plain64\",\"sector_size\":{ss}}}}},",
671                "\"digests\":{{\"0\":{{\"type\":\"pbkdf2\",\"keyslots\":[\"0\"],\"segments\":[\"0\"],\"hash\":\"sha256\",\"iterations\":{diter},\"salt\":\"{dsalt}\",\"digest\":\"{dig}\"}}}}}}"
672            ),
673            stripes = L2_STRIPES,
674            area = L2_AREA_OFF,
675            kdf = kdf_json,
676            seg = L2_SEG_OFF,
677            ss = sector_size,
678            diter = L2_DIG_ITER,
679            dsalt = b64.encode(dig_salt),
680            dig = b64.encode(&digest),
681        );
682
683        let hdr_size = LUKS2_BIN_HDR_LEN + 4096; // 8192
684        assert!(json.len() < 4096);
685        let total = L2_SEG_OFF as usize + payload.len();
686        let mut img = vec![0u8; total];
687        img[0..6].copy_from_slice(&LUKS_MAGIC);
688        img[6..8].copy_from_slice(&2u16.to_be_bytes());
689        img[8..16].copy_from_slice(&(hdr_size as u64).to_be_bytes());
690        img[LUKS2_BIN_HDR_LEN..LUKS2_BIN_HDR_LEN + json.len()].copy_from_slice(json.as_bytes());
691        let a = L2_AREA_OFF as usize;
692        img[a..a + material.len()].copy_from_slice(&material);
693        let s = L2_SEG_OFF as usize;
694        img[s..s + payload.len()].copy_from_slice(&payload);
695        (img, plain)
696    }
697
698    #[test]
699    fn luks2_argon2id_roundtrip() {
700        let master = vec![0xC3u8; 64];
701        let salt = [0x77u8; 16];
702        let slot_key = derive_key_argon2(
703            &Argon2Params {
704                kind: "argon2id",
705                time: 1,
706                memory: 32,
707                cpus: 1,
708                salt: &salt,
709            },
710            PASS,
711            64,
712        )
713        .unwrap();
714        let b64 = base64::engine::general_purpose::STANDARD;
715        let kdf = format!(
716            "{{\"type\":\"argon2id\",\"time\":1,\"memory\":32,\"cpus\":1,\"salt\":\"{}\"}}",
717            b64.encode(salt)
718        );
719        let (img, plain) = build_luks2(&master, &slot_key, &kdf, 4096);
720
721        let mut vol = LuksVolume::unlock_with_passphrase(Cursor::new(img), PASS).unwrap();
722        assert_eq!(vol.info().version, 2);
723        assert_eq!(vol.info().cipher_name, "aes");
724        assert_eq!(vol.info().cipher_mode, "xts-plain64");
725        assert_eq!(vol.info().sector_size, 4096);
726        assert_eq!(vol.info().key_bytes, 64);
727        assert_eq!(vol.master_key(), &master[..]);
728
729        let mut buf = [0u8; 512];
730        vol.read_at(0, &mut buf).unwrap();
731        assert_eq!(&buf[..], &plain[..512]);
732    }
733
734    #[test]
735    fn luks2_pbkdf2_roundtrip() {
736        let master = vec![0x1Eu8; 64];
737        let salt = [0x88u8; 16];
738        let slot_key = derive_key("sha256", PASS, &salt, 5, 64).unwrap();
739        let b64 = base64::engine::general_purpose::STANDARD;
740        let kdf = format!(
741            "{{\"type\":\"pbkdf2\",\"hash\":\"sha256\",\"iterations\":5,\"salt\":\"{}\"}}",
742            b64.encode(salt)
743        );
744        let (img, plain) = build_luks2(&master, &slot_key, &kdf, 512);
745
746        let mut vol = LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS).unwrap();
747        assert_eq!(vol.info().sector_size, 512);
748        let mut buf = [0u8; 512];
749        vol.read_at(0, &mut buf).unwrap();
750        assert_eq!(&buf[..], &plain[..512]);
751    }
752
753    #[test]
754    fn luks2_no_crypt_segment() {
755        let json = r#"{"keyslots":{"0":{"key_size":64,
756          "af":{"stripes":8,"hash":"sha256"},
757          "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
758          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
759          "segments":{},"digests":{}}"#;
760        let img = build_luks2_json(json);
761        assert!(matches!(
762            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
763            Err(LuksError::NoActiveKeyslot)
764        ));
765    }
766
767    #[test]
768    fn luks2_no_keyslots() {
769        let json = r#"{"keyslots":{},
770          "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
771          "digests":{}}"#;
772        let img = build_luks2_json(json);
773        assert!(matches!(
774            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
775            Err(LuksError::NoActiveKeyslot)
776        ));
777    }
778
779    #[test]
780    fn luks2_short_key_material_is_skipped() {
781        // Keyslot area past EOF: material read is short, slot skipped -> auth fail.
782        let json = r#"{"keyslots":{"0":{"key_size":64,
783          "af":{"stripes":8,"hash":"sha256"},
784          "area":{"offset":"9999999","size":"512","encryption":"aes-xts-plain64"},
785          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
786          "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
787          "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"hash":"sha256","iterations":5,"salt":"AAAA","digest":"AAAA"}}}"#;
788        let img = build_luks2_json(json);
789        assert!(matches!(
790            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
791            Err(LuksError::AuthenticationFailed)
792        ));
793    }
794
795    /// Minimal LUKS2 image carrying an arbitrary JSON metadata document (no valid
796    /// key material) — for the header-level error-path tests above.
797    fn build_luks2_json(json: &str) -> Vec<u8> {
798        let hdr_size = LUKS2_BIN_HDR_LEN + 4096;
799        let total = 16384usize;
800        let mut img = vec![0u8; total];
801        img[0..6].copy_from_slice(&LUKS_MAGIC);
802        img[6..8].copy_from_slice(&2u16.to_be_bytes());
803        img[8..16].copy_from_slice(&(hdr_size as u64).to_be_bytes());
804        img[LUKS2_BIN_HDR_LEN..LUKS2_BIN_HDR_LEN + json.len()].copy_from_slice(json.as_bytes());
805        img
806    }
807
808    // ---- reachable KDF/hash error propagation -----------------------------
809
810    #[test]
811    fn luks1_unsupported_hash_errors() {
812        // An unsupported hash spec makes the very first slot-key derive_key fail,
813        // and unlock surfaces a loud Unsupported error (fail-loud, never panic).
814        let mut img = vec![0u8; 4096];
815        img[0..6].copy_from_slice(&LUKS_MAGIC);
816        img[6..8].copy_from_slice(&1u16.to_be_bytes());
817        img[8..11].copy_from_slice(b"aes");
818        img[40..51].copy_from_slice(b"xts-plain64");
819        img[72..75].copy_from_slice(b"md5");
820        img[104..108].copy_from_slice(&4u32.to_be_bytes());
821        img[108..112].copy_from_slice(&64u32.to_be_bytes());
822        let base = 208;
823        img[base..base + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
824        img[base + 4..base + 8].copy_from_slice(&5u32.to_be_bytes());
825        img[base + 40..base + 44].copy_from_slice(&1u32.to_be_bytes());
826        img[base + 44..base + 48].copy_from_slice(&8u32.to_be_bytes());
827        assert!(matches!(
828            LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS),
829            Err(LuksError::Unsupported { what: "hash", .. })
830        ));
831    }
832
833    #[test]
834    fn luks2_bad_argon2_params_errors() {
835        // memory cost 0 fails Argon2's Params::new inside recover_master_key2.
836        let json = r#"{"keyslots":{"0":{"key_size":64,
837          "af":{"stripes":8,"hash":"sha256"},
838          "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
839          "kdf":{"type":"argon2id","time":1,"memory":0,"cpus":1,"salt":"AAAAAAAAAAA="}}},
840          "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
841          "digests":{}}"#;
842        let img = build_luks2_json(json);
843        assert!(matches!(
844            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
845            Err(LuksError::Unsupported { .. })
846        ));
847    }
848
849    #[test]
850    fn luks2_unsupported_af_hash_errors() {
851        // Valid KDF, readable material, but an unsupported AF hash makes af::merge
852        // fail after the keyslot decrypt.
853        let json = r#"{"keyslots":{"0":{"key_size":64,
854          "af":{"stripes":8,"hash":"md5"},
855          "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
856          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
857          "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
858          "digests":{}}"#;
859        let img = build_luks2_json(json);
860        assert!(matches!(
861            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
862            Err(LuksError::Unsupported { what: "hash", .. })
863        ));
864    }
865
866    #[test]
867    fn luks2_unsupported_digest_hash_errors() {
868        // Reaches the digest loop, where an unsupported digest hash errors.
869        let json = r#"{"keyslots":{"0":{"key_size":64,
870          "af":{"stripes":8,"hash":"sha256"},
871          "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
872          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
873          "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
874          "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"hash":"md5","iterations":5,"salt":"AAAA","digest":"AAAA"}}}"#;
875        let img = build_luks2_json(json);
876        assert!(matches!(
877            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
878            Err(LuksError::Unsupported { what: "hash", .. })
879        ));
880    }
881
882    #[test]
883    fn luks2_digest_mismatch_falls_through() {
884        // Two digests exercise both digest-loop branches: digest "0" references the
885        // keyslot but its bytes don't match (the wrong-value fall-through), while
886        // digest "1" references another keyslot (the skip branch). Neither
887        // verifies, so unlock ends in AuthenticationFailed.
888        let json = r#"{"keyslots":{"0":{"key_size":64,
889          "af":{"stripes":8,"hash":"sha256"},
890          "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
891          "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
892          "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
893          "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"hash":"sha256","iterations":5,"salt":"AAAA","digest":"AAAA"},
894                     "1":{"type":"pbkdf2","keyslots":["9"],"hash":"sha256","iterations":5,"salt":"AAAA","digest":"AAAA"}}}"#;
895        let img = build_luks2_json(json);
896        assert!(matches!(
897            LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
898            Err(LuksError::AuthenticationFailed)
899        ));
900    }
901
902    // ---- read_available I/O arms ------------------------------------------
903
904    struct InterruptOnce(bool);
905    impl Read for InterruptOnce {
906        fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
907            if self.0 {
908                Ok(0)
909            } else {
910                self.0 = true;
911                Err(std::io::Error::from(std::io::ErrorKind::Interrupted))
912            }
913        }
914    }
915
916    struct AlwaysError;
917    impl Read for AlwaysError {
918        fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
919            Err(std::io::Error::other("boom"))
920        }
921    }
922
923    #[test]
924    fn read_available_retries_on_interrupted() {
925        let mut r = InterruptOnce(false);
926        let mut buf = [0xFFu8; 4];
927        assert_eq!(read_available(&mut r, &mut buf).unwrap(), 0);
928        assert_eq!(buf, [0u8; 4]); // zero-filled on EOF
929    }
930
931    #[test]
932    fn read_available_propagates_hard_error() {
933        let mut r = AlwaysError;
934        let mut buf = [0u8; 4];
935        assert!(read_available(&mut r, &mut buf).is_err());
936    }
937}