1use std::io::{Read, Seek, SeekFrom};
9use std::time::{Duration, Instant};
10
11use crate::af;
12use crate::crypto::{
13 derive_key_argon2_within, derive_key_within, xts_decrypt_area, Argon2Params, DERIVATION_BUDGET,
14 UNLOCK_BUDGET,
15};
16use crate::error::{LuksError, Result};
17use crate::header::{Luks1Header, LUKS1_PHDR_LEN, MK_DIGEST_LEN, SECTOR};
18use crate::header2::{Luks2Header, Luks2Kdf, LUKS2_BIN_HDR_LEN};
19
20pub struct LuksVolume;
22
23#[derive(Debug, Clone)]
25pub struct VolumeInfo {
26 pub version: u16,
28 pub cipher_name: String,
30 pub cipher_mode: String,
32 pub key_bytes: u32,
34 pub sector_size: usize,
36}
37
38fn split_encryption(enc: &str) -> (String, String) {
41 match enc.split_once('-') {
42 Some((name, mode)) => (name.to_string(), mode.to_string()),
43 None => (enc.to_string(), String::new()),
44 }
45}
46
47impl LuksVolume {
48 pub fn unlock_with_passphrase<R: Read + Seek>(
54 reader: R,
55 passphrase: &[u8],
56 ) -> Result<DecryptedPayload<R>> {
57 Self::unlock_with_passphrase_within(reader, passphrase, UNLOCK_BUDGET)
58 }
59
60 pub fn unlock_with_passphrase_within<R: Read + Seek>(
71 mut reader: R,
72 passphrase: &[u8],
73 budget: Duration,
74 ) -> Result<DecryptedPayload<R>> {
75 let mut ver = [0u8; 8];
76 reader.seek(SeekFrom::Start(0))?;
77 read_fill(&mut reader, &mut ver)?;
78 reader.seek(SeekFrom::Start(0))?;
79 match crate::bytes::be_u16(&ver, 6) {
80 1 => Self::unlock1_with_passphrase_within(reader, passphrase, budget),
81 2 => Self::unlock2_with_passphrase_within(reader, passphrase, budget),
82 other => {
83 if ver.starts_with(&crate::header::LUKS_MAGIC) {
84 Err(LuksError::UnsupportedVersion { version: other })
85 } else {
86 Err(LuksError::NotLuks {
87 found: crate::bytes::bytes_n::<6>(&ver, 0),
88 })
89 }
90 }
91 }
92 }
93
94 pub fn unlock1_with_passphrase_within<R: Read + Seek>(
104 reader: R,
105 passphrase: &[u8],
106 budget: Duration,
107 ) -> Result<DecryptedPayload<R>> {
108 Self::unlock1_inner(reader, passphrase, UnlockDeadline::new(budget))
109 }
110
111 pub fn unlock1_with_passphrase<R: Read + Seek>(
117 reader: R,
118 passphrase: &[u8],
119 ) -> Result<DecryptedPayload<R>> {
120 Self::unlock1_inner(reader, passphrase, UnlockDeadline::new(UNLOCK_BUDGET))
121 }
122
123 fn unlock1_inner<R: Read + Seek>(
124 mut reader: R,
125 passphrase: &[u8],
126 deadline: UnlockDeadline,
127 ) -> Result<DecryptedPayload<R>> {
128 let mut hdr_buf = vec![0u8; LUKS1_PHDR_LEN];
129 reader.seek(SeekFrom::Start(0))?;
130 read_fill(&mut reader, &mut hdr_buf)?;
131 let header = Luks1Header::parse(&hdr_buf)?;
132 if header.active_keyslots().next().is_none() {
133 return Err(LuksError::NoActiveKeyslot);
134 }
135 let key_bytes = header.key_bytes as usize;
136 let master_key =
137 recover_master_key1(&mut reader, &header, passphrase, key_bytes, deadline)?;
138 let total_size = reader.seek(SeekFrom::End(0))?;
139 Ok(DecryptedPayload {
140 reader,
141 master_key,
142 cipher_mode: header.cipher_mode.clone(),
143 payload_offset: header.payload_byte_offset(),
144 sector_size: SECTOR as usize,
145 iv_tweak: 0,
146 total_size,
147 position: 0,
148 info: VolumeInfo {
149 version: 1,
150 cipher_name: header.cipher_name.clone(),
151 cipher_mode: header.cipher_mode.clone(),
152 key_bytes: header.key_bytes,
153 sector_size: SECTOR as usize,
154 },
155 })
156 }
157
158 pub fn unlock2_with_passphrase_within<R: Read + Seek>(
168 reader: R,
169 passphrase: &[u8],
170 budget: Duration,
171 ) -> Result<DecryptedPayload<R>> {
172 Self::unlock2_inner(reader, passphrase, UnlockDeadline::new(budget))
173 }
174
175 pub fn unlock2_with_passphrase<R: Read + Seek>(
181 reader: R,
182 passphrase: &[u8],
183 ) -> Result<DecryptedPayload<R>> {
184 Self::unlock2_inner(reader, passphrase, UnlockDeadline::new(UNLOCK_BUDGET))
185 }
186
187 fn unlock2_inner<R: Read + Seek>(
188 mut reader: R,
189 passphrase: &[u8],
190 deadline: UnlockDeadline,
191 ) -> Result<DecryptedPayload<R>> {
192 let mut bin = vec![0u8; LUKS2_BIN_HDR_LEN];
194 reader.seek(SeekFrom::Start(0))?;
195 read_fill(&mut reader, &mut bin)?;
196 let hdr_size = crate::bytes::be_u64(&bin, 8).max(LUKS2_BIN_HDR_LEN as u64) as usize;
197 let mut full = vec![0u8; hdr_size];
198 reader.seek(SeekFrom::Start(0))?;
199 read_available(&mut reader, &mut full)?;
200 let header = Luks2Header::parse(&full)?;
201
202 let segment = header
203 .crypt_segment()
204 .cloned()
205 .ok_or(LuksError::NoActiveKeyslot)?;
206 if header.keyslots.is_empty() {
207 return Err(LuksError::NoActiveKeyslot);
208 }
209
210 let master_key = recover_master_key2(&mut reader, &header, passphrase, deadline)?;
211 let total_size = reader.seek(SeekFrom::End(0))?;
212 let (cipher_name, cipher_mode) = split_encryption(&segment.encryption);
213 let key_bytes = master_key.len() as u32;
214 Ok(DecryptedPayload {
215 reader,
216 master_key,
217 cipher_mode: cipher_mode.clone(),
218 payload_offset: segment.offset,
219 sector_size: segment.sector_size.max(512),
220 iv_tweak: segment.iv_tweak,
221 total_size,
222 position: 0,
223 info: VolumeInfo {
224 version: 2,
225 cipher_name,
226 cipher_mode,
227 key_bytes,
228 sector_size: segment.sector_size.max(512),
229 },
230 })
231 }
232}
233
234#[derive(Debug, Clone, Copy)]
241struct UnlockDeadline {
242 started: Instant,
243 total: Duration,
244}
245
246impl UnlockDeadline {
247 fn new(total: Duration) -> Self {
248 Self {
249 started: Instant::now(),
250 total,
251 }
252 }
253
254 fn remaining(self, slots_tried: usize) -> Result<Duration> {
262 self.total
263 .checked_sub(self.started.elapsed())
264 .filter(|left| !left.is_zero())
265 .map(|left| left.min(DERIVATION_BUDGET))
266 .ok_or(LuksError::UnlockBudgetExceeded {
267 slots_tried,
268 budget_secs: self.total.as_secs(),
269 })
270 }
271}
272
273fn recover_master_key1<R: Read + Seek>(
276 reader: &mut R,
277 header: &Luks1Header,
278 passphrase: &[u8],
279 key_bytes: usize,
280 deadline: UnlockDeadline,
281) -> Result<Vec<u8>> {
282 for (tried, slot) in header.active_keyslots().enumerate() {
283 let slot_key = derive_key_within(
284 &header.hash_spec,
285 passphrase,
286 &slot.salt,
287 slot.iterations,
288 key_bytes,
289 deadline.remaining(tried)?,
290 )?;
291 let material_len = af::material_len(key_bytes, slot.stripes as usize)?;
292 let mut material = vec![0u8; material_len];
293 reader.seek(SeekFrom::Start(
294 u64::from(slot.key_material_offset) * SECTOR,
295 ))?;
296 if read_available(reader, &mut material)? < material_len {
297 continue;
298 }
299 xts_decrypt_area(&header.cipher_mode, &slot_key, &mut material, 512, 0)?;
300 let candidate = af::merge(
301 &header.hash_spec,
302 &material,
303 key_bytes,
304 slot.stripes as usize,
305 )?; let digest = derive_key_within(
307 &header.hash_spec,
308 &candidate,
309 &header.mk_digest_salt,
310 header.mk_digest_iter,
311 MK_DIGEST_LEN,
312 deadline.remaining(tried)?,
313 )?; if digest == header.mk_digest {
315 return Ok(candidate);
316 }
317 }
318 Err(LuksError::AuthenticationFailed)
319}
320
321fn recover_master_key2<R: Read + Seek>(
331 reader: &mut R,
332 header: &Luks2Header,
333 passphrase: &[u8],
334 deadline: UnlockDeadline,
335) -> Result<Vec<u8>> {
336 for (tried, slot) in header.keyslots.iter().enumerate() {
337 let slot_key = match &slot.kdf {
338 Luks2Kdf::Argon2 {
339 kind,
340 time,
341 memory,
342 cpus,
343 salt,
344 } => derive_key_argon2_within(
345 &Argon2Params {
346 kind,
347 time: *time,
348 memory: *memory,
349 cpus: *cpus,
350 salt,
351 },
352 passphrase,
353 slot.key_size,
354 deadline.remaining(tried)?,
355 )?,
356 Luks2Kdf::Pbkdf2 {
357 hash,
358 iterations,
359 salt,
360 } => derive_key_within(
361 hash,
362 passphrase,
363 salt,
364 *iterations,
365 slot.key_size,
366 deadline.remaining(tried)?,
367 )?,
368 };
369
370 let (_, area_mode) = split_encryption(&slot.area_encryption);
371 let material_len = af::material_len(slot.key_size, slot.af_stripes as usize)?;
372 let mut material = vec![0u8; material_len];
373 reader.seek(SeekFrom::Start(slot.area_offset))?;
374 if read_available(reader, &mut material)? < material_len {
375 continue;
376 }
377 xts_decrypt_area(&area_mode, &slot_key, &mut material, 512, 0)?;
378 let candidate = af::merge(
379 &slot.af_hash,
380 &material,
381 slot.key_size,
382 slot.af_stripes as usize,
383 )?;
384
385 for dig in &header.digests {
386 if dig.kind == "pbkdf2" && dig.keyslots.iter().any(|k| k == &slot.id) {
387 let d = derive_key_within(
388 &dig.hash,
389 &candidate,
390 &dig.salt,
391 dig.iterations,
392 dig.digest.len(),
393 deadline.remaining(tried)?,
394 )?;
395 if d == dig.digest {
396 return Ok(candidate);
397 }
398 }
399 }
400 }
401 Err(LuksError::AuthenticationFailed)
402}
403
404pub struct DecryptedPayload<R> {
406 reader: R,
407 master_key: Vec<u8>,
408 cipher_mode: String,
409 payload_offset: u64,
410 sector_size: usize,
411 iv_tweak: u128,
412 total_size: u64,
413 position: u64,
414 info: VolumeInfo,
415}
416
417impl<R: Read + Seek> DecryptedPayload<R> {
418 #[must_use]
420 pub fn info(&self) -> &VolumeInfo {
421 &self.info
422 }
423
424 #[must_use]
426 pub fn master_key(&self) -> &[u8] {
427 &self.master_key
428 }
429
430 #[must_use]
432 pub fn payload_size(&self) -> u64 {
433 self.total_size.saturating_sub(self.payload_offset)
434 }
435
436 pub fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
442 let ss = self.sector_size as u64;
443 let step = (self.sector_size / 512).max(1) as u128;
444 let mut done = 0usize;
445 while done < buf.len() {
446 let pos = offset + done as u64;
447 let unit = pos / ss;
448 let within = (pos % ss) as usize;
449 let physical = self.payload_offset + unit * ss;
450
451 let mut ct = vec![0u8; self.sector_size];
452 self.reader.seek(SeekFrom::Start(physical))?;
453 read_available(&mut self.reader, &mut ct)?;
454 let tweak = self.iv_tweak + u128::from(unit) * step;
455 xts_decrypt_area(
456 &self.cipher_mode,
457 &self.master_key,
458 &mut ct,
459 self.sector_size,
460 tweak,
461 )?; let take = (self.sector_size - within).min(buf.len() - done);
464 buf[done..done + take].copy_from_slice(&ct[within..within + take]);
465 done += take;
466 }
467 Ok(())
468 }
469}
470
471impl<R: Read + Seek> Read for DecryptedPayload<R> {
472 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
473 let size = self.payload_size();
474 if self.position >= size {
475 return Ok(0);
476 }
477 let n = (buf.len() as u64).min(size - self.position) as usize;
478 self.read_at(self.position, &mut buf[..n])
479 .map_err(|e| std::io::Error::other(e.to_string()))?;
480 self.position += n as u64;
481 Ok(n)
482 }
483}
484
485impl<R: Read + Seek> Seek for DecryptedPayload<R> {
486 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
487 let size = self.payload_size();
488 let new = match pos {
489 SeekFrom::Start(o) => i128::from(o),
490 SeekFrom::End(o) => i128::from(size) + i128::from(o),
491 SeekFrom::Current(o) => i128::from(self.position) + i128::from(o),
492 };
493 if new < 0 {
494 return Err(std::io::Error::new(
495 std::io::ErrorKind::InvalidInput,
496 "seek before start",
497 ));
498 }
499 self.position = new as u64;
500 Ok(self.position)
501 }
502}
503
504fn read_fill<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
506 reader.read_exact(buf)?;
507 Ok(())
508}
509
510fn read_available<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
513 let mut filled = 0;
514 while filled < buf.len() {
515 match reader.read(&mut buf[filled..]) {
516 Ok(0) => break,
517 Ok(n) => filled += n,
518 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
519 Err(e) => return Err(e.into()),
520 }
521 }
522 for b in &mut buf[filled..] {
523 *b = 0;
524 }
525 Ok(filled)
526}
527
528#[cfg(test)]
529mod tests {
530 use std::time::Duration;
536
537 use super::*;
538 use crate::af;
539 use crate::crypto::{derive_key_argon2_within, derive_key_within, Argon2Params};
540 use crate::header::{
541 KEYSLOT_LEN, KEY_DISABLED, KEY_ENABLED, LUKS1_PHDR_LEN, LUKS_MAGIC, LUKS_NUM_KEYS,
542 };
543 use crate::header2::LUKS2_BIN_HDR_LEN;
544 use aes::cipher::KeyInit;
545 use aes::Aes256;
546 use base64::Engine;
547 use std::io::Cursor;
548 use xts_mode::Xts128;
549
550 const PASS: &[u8] = b"luks-TEST";
551
552 fn xts_encrypt(key: &[u8], buf: &mut [u8], unit_size: usize, base: u128) {
557 let step = (unit_size / 512).max(1) as u128;
558 let (k1, k2) = key.split_at(32);
559 let xts = Xts128::<Aes256>::new(Aes256::new(k1.into()), Aes256::new(k2.into()));
560 for (u, chunk) in buf.chunks_mut(unit_size).enumerate() {
561 let tweak = (base + u as u128 * step).to_le_bytes();
562 xts.encrypt_sector(chunk, tweak);
563 }
564 }
565
566 fn pbkdf2_sha256(pw: &[u8], salt: &[u8], iters: u32, len: usize) -> Vec<u8> {
567 let mut out = vec![0u8; len];
568 pbkdf2::pbkdf2_hmac::<sha2::Sha256>(pw, salt, iters, &mut out);
569 out
570 }
571
572 fn known_plain(len: usize) -> Vec<u8> {
573 (0..len).map(|i| (i as u8) ^ 0x3c).collect()
574 }
575
576 const L1_KM_OFF: u32 = 2; const L1_PAYLOAD_OFF: u32 = 4; const L1_STRIPES: u32 = 8;
581 const L1_KEY_BYTES: u32 = 64;
582 const L1_ITERS: u32 = 5;
583 const L1_MK_ITER: u32 = 7;
584
585 fn build_luks1(master: &[u8]) -> (Vec<u8>, Vec<u8>) {
588 let salt = [0x11u8; 32];
589 let mk_salt = [0x22u8; 32];
590 let slot_key = pbkdf2_sha256(PASS, &salt, L1_ITERS, L1_KEY_BYTES as usize);
591
592 let mut material =
593 af::af_split::<sha2::Sha256>(master, L1_KEY_BYTES as usize, L1_STRIPES as usize, 0x5a);
594 xts_encrypt(&slot_key, &mut material, 512, 0);
595
596 let mk_digest = pbkdf2_sha256(master, &mk_salt, L1_MK_ITER, 20);
597
598 let mut payload = known_plain(3 * 512);
599 let plain = payload.clone();
600 xts_encrypt(master, &mut payload, 512, 0);
601
602 let payload_byte = L1_PAYLOAD_OFF as usize * 512;
603 let mut img = vec![0u8; payload_byte + payload.len()];
604 img[0..6].copy_from_slice(&LUKS_MAGIC);
605 img[6..8].copy_from_slice(&1u16.to_be_bytes());
606 img[8..11].copy_from_slice(b"aes");
607 img[40..51].copy_from_slice(b"xts-plain64");
608 img[72..78].copy_from_slice(b"sha256");
609 img[104..108].copy_from_slice(&L1_PAYLOAD_OFF.to_be_bytes());
610 img[108..112].copy_from_slice(&L1_KEY_BYTES.to_be_bytes());
611 img[112..132].copy_from_slice(&mk_digest);
612 img[132..164].copy_from_slice(&mk_salt);
613 img[164..168].copy_from_slice(&L1_MK_ITER.to_be_bytes());
614 img[168..204].copy_from_slice(b"b22690e1-a392-4ecc-83b1-c1cf21200116");
615
616 for i in 0..LUKS_NUM_KEYS {
617 let base = 208 + i * KEYSLOT_LEN;
618 if i == 0 {
619 img[base..base + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
620 img[base + 4..base + 8].copy_from_slice(&L1_ITERS.to_be_bytes());
621 img[base + 8..base + 40].copy_from_slice(&salt);
622 img[base + 40..base + 44].copy_from_slice(&L1_KM_OFF.to_be_bytes());
623 img[base + 44..base + 48].copy_from_slice(&L1_STRIPES.to_be_bytes());
624 } else {
625 img[base..base + 4].copy_from_slice(&KEY_DISABLED.to_be_bytes());
626 }
627 }
628
629 let km_byte = L1_KM_OFF as usize * 512;
630 img[km_byte..km_byte + material.len()].copy_from_slice(&material);
631 img[payload_byte..payload_byte + payload.len()].copy_from_slice(&payload);
632 assert!(
633 LUKS1_PHDR_LEN <= km_byte,
634 "phdr must not overlap key material"
635 );
636 (img, plain)
637 }
638
639 #[test]
640 fn luks1_roundtrip_via_autodetect() {
641 let master: Vec<u8> = (0..64u8)
642 .map(|x| x.wrapping_mul(7).wrapping_add(3))
643 .collect();
644 let (img, plain) = build_luks1(&master);
645 let mut vol = LuksVolume::unlock_with_passphrase(Cursor::new(img), PASS).unwrap();
646
647 assert_eq!(vol.info().version, 1);
648 assert_eq!(vol.info().cipher_name, "aes");
649 assert_eq!(vol.info().cipher_mode, "xts-plain64");
650 assert_eq!(vol.info().key_bytes, 64);
651 assert_eq!(vol.info().sector_size, 512);
652 assert_eq!(vol.master_key(), &master[..]);
653 assert_eq!(vol.payload_size(), 3 * 512);
654
655 for lba in 0..3u64 {
656 let mut buf = [0u8; 512];
657 vol.read_at(lba * 512, &mut buf).unwrap();
658 assert_eq!(
659 &buf[..],
660 &plain[lba as usize * 512..lba as usize * 512 + 512]
661 );
662 }
663 }
664
665 #[test]
666 fn luks1_read_seek_traits() {
667 let master = vec![0xABu8; 64];
668 let (img, plain) = build_luks1(&master);
669 let mut vol = LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS).unwrap();
670
671 assert_eq!(vol.seek(SeekFrom::Start(512)).unwrap(), 512);
673 assert_eq!(vol.seek(SeekFrom::Current(-256)).unwrap(), 256);
674 let end = vol.seek(SeekFrom::End(0)).unwrap();
675 assert_eq!(end, 3 * 512);
676 assert!(vol.seek(SeekFrom::End(-1)).is_ok());
677 assert!(vol.seek(SeekFrom::Start(0)).is_ok());
678
679 let mut got = Vec::new();
681 std::io::Read::read_to_end(&mut vol, &mut got).unwrap();
682 assert_eq!(got, plain);
683 let mut z = [0u8; 16];
685 assert_eq!(std::io::Read::read(&mut vol, &mut z).unwrap(), 0);
686 }
687
688 #[test]
689 fn luks1_seek_before_start_errors() {
690 let master = vec![0x01u8; 64];
691 let (img, _) = build_luks1(&master);
692 let mut vol = LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS).unwrap();
693 assert!(vol.seek(SeekFrom::Start(0)).is_ok());
694 assert!(vol.seek(SeekFrom::Current(-1)).is_err());
695 }
696
697 #[test]
707 fn unlock_is_bounded_in_aggregate_not_just_per_derivation() {
708 let master = [0x33u8; L1_KEY_BYTES as usize];
709 let (img, _plain) = build_luks1(&master);
710
711 match LuksVolume::unlock1_with_passphrase_within(Cursor::new(img), PASS, Duration::ZERO) {
714 Err(LuksError::UnlockBudgetExceeded { .. }) => {}
715 Err(other) => panic!("expected UnlockBudgetExceeded, got {other:?}"), Ok(_) => panic!("a zero total budget must refuse before doing derivation work"), }
718 }
719
720 #[test]
721 fn luks1_wrong_passphrase() {
722 let master = vec![0x5Cu8; 64];
723 let (img, _) = build_luks1(&master);
724 assert!(matches!(
725 LuksVolume::unlock1_with_passphrase(Cursor::new(img), b"wrong"),
726 Err(LuksError::AuthenticationFailed)
727 ));
728 }
729
730 #[test]
731 fn luks1_no_active_keyslot() {
732 let master = vec![0x5Cu8; 64];
733 let (mut img, _) = build_luks1(&master);
734 img[208..212].copy_from_slice(&KEY_DISABLED.to_be_bytes());
736 assert!(matches!(
737 LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS),
738 Err(LuksError::NoActiveKeyslot)
739 ));
740 }
741
742 #[test]
743 fn luks1_short_key_material_is_skipped() {
744 let mut img = vec![0u8; LUKS1_PHDR_LEN];
747 img[0..6].copy_from_slice(&LUKS_MAGIC);
748 img[6..8].copy_from_slice(&1u16.to_be_bytes());
749 img[8..11].copy_from_slice(b"aes");
750 img[40..51].copy_from_slice(b"xts-plain64");
751 img[72..78].copy_from_slice(b"sha256");
752 img[104..108].copy_from_slice(&4u32.to_be_bytes());
753 img[108..112].copy_from_slice(&64u32.to_be_bytes());
754 let base = 208;
755 img[base..base + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
756 img[base + 4..base + 8].copy_from_slice(&5u32.to_be_bytes());
757 img[base + 40..base + 44].copy_from_slice(&1000u32.to_be_bytes()); img[base + 44..base + 48].copy_from_slice(&8u32.to_be_bytes());
759 assert!(matches!(
760 LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS),
761 Err(LuksError::AuthenticationFailed)
762 ));
763 }
764
765 #[test]
768 fn not_a_luks_container() {
769 assert!(matches!(
770 LuksVolume::unlock_with_passphrase(Cursor::new(vec![0u8; 8]), PASS),
771 Err(LuksError::NotLuks { .. })
772 ));
773 }
774
775 #[test]
776 fn unsupported_version() {
777 let mut img = vec![0u8; 8];
778 img[0..6].copy_from_slice(&LUKS_MAGIC);
779 img[6..8].copy_from_slice(&3u16.to_be_bytes());
780 assert!(matches!(
781 LuksVolume::unlock_with_passphrase(Cursor::new(img), PASS),
782 Err(LuksError::UnsupportedVersion { version: 3 })
783 ));
784 }
785
786 #[test]
787 fn split_encryption_without_dash() {
788 assert_eq!(split_encryption("aes"), ("aes".to_string(), String::new()));
789 assert_eq!(
790 split_encryption("aes-xts-plain64"),
791 ("aes".to_string(), "xts-plain64".to_string())
792 );
793 }
794
795 const L2_AREA_OFF: u64 = 8192;
798 const L2_SEG_OFF: u64 = 12288;
799 const L2_STRIPES: usize = 8;
800 const L2_DIG_ITER: u32 = 5;
801
802 fn build_luks2(
805 master: &[u8],
806 slot_key: &[u8],
807 kdf_json: &str,
808 sector_size: usize,
809 ) -> (Vec<u8>, Vec<u8>) {
810 let b64 = base64::engine::general_purpose::STANDARD;
811 let dig_salt = [0x22u8; 16];
812 let digest = pbkdf2_sha256(master, &dig_salt, L2_DIG_ITER, 32);
813
814 let mut material = af::af_split::<sha2::Sha256>(master, 64, L2_STRIPES, 0x5a);
815 xts_encrypt(slot_key, &mut material, 512, 0);
816
817 let mut payload = known_plain(sector_size); let plain = payload.clone();
819 xts_encrypt(master, &mut payload, sector_size, 0);
820
821 let json = format!(
822 concat!(
823 "{{\"keyslots\":{{\"0\":{{\"key_size\":64,",
824 "\"af\":{{\"type\":\"luks1\",\"stripes\":{stripes},\"hash\":\"sha256\"}},",
825 "\"area\":{{\"type\":\"raw\",\"offset\":\"{area}\",\"size\":\"512\",\"encryption\":\"aes-xts-plain64\",\"key_size\":64}},",
826 "\"kdf\":{kdf}}}}},",
827 "\"segments\":{{\"0\":{{\"type\":\"crypt\",\"offset\":\"{seg}\",\"size\":\"dynamic\",\"iv_tweak\":\"0\",\"encryption\":\"aes-xts-plain64\",\"sector_size\":{ss}}}}},",
828 "\"digests\":{{\"0\":{{\"type\":\"pbkdf2\",\"keyslots\":[\"0\"],\"segments\":[\"0\"],\"hash\":\"sha256\",\"iterations\":{diter},\"salt\":\"{dsalt}\",\"digest\":\"{dig}\"}}}}}}"
829 ),
830 stripes = L2_STRIPES,
831 area = L2_AREA_OFF,
832 kdf = kdf_json,
833 seg = L2_SEG_OFF,
834 ss = sector_size,
835 diter = L2_DIG_ITER,
836 dsalt = b64.encode(dig_salt),
837 dig = b64.encode(&digest),
838 );
839
840 let hdr_size = LUKS2_BIN_HDR_LEN + 4096; assert!(json.len() < 4096);
842 let total = L2_SEG_OFF as usize + payload.len();
843 let mut img = vec![0u8; total];
844 img[0..6].copy_from_slice(&LUKS_MAGIC);
845 img[6..8].copy_from_slice(&2u16.to_be_bytes());
846 img[8..16].copy_from_slice(&(hdr_size as u64).to_be_bytes());
847 img[LUKS2_BIN_HDR_LEN..LUKS2_BIN_HDR_LEN + json.len()].copy_from_slice(json.as_bytes());
848 let a = L2_AREA_OFF as usize;
849 img[a..a + material.len()].copy_from_slice(&material);
850 let s = L2_SEG_OFF as usize;
851 img[s..s + payload.len()].copy_from_slice(&payload);
852 (img, plain)
853 }
854
855 #[test]
856 fn luks2_argon2id_roundtrip() {
857 let master = vec![0xC3u8; 64];
858 let salt = [0x77u8; 16];
859 let slot_key = derive_key_argon2_within(
860 &Argon2Params {
861 kind: "argon2id",
862 time: 1,
863 memory: 32,
864 cpus: 1,
865 salt: &salt,
866 },
867 PASS,
868 64,
869 DERIVATION_BUDGET,
870 )
871 .unwrap();
872 let b64 = base64::engine::general_purpose::STANDARD;
873 let kdf = format!(
874 "{{\"type\":\"argon2id\",\"time\":1,\"memory\":32,\"cpus\":1,\"salt\":\"{}\"}}",
875 b64.encode(salt)
876 );
877 let (img, plain) = build_luks2(&master, &slot_key, &kdf, 4096);
878
879 let mut vol = LuksVolume::unlock_with_passphrase(Cursor::new(img), PASS).unwrap();
880 assert_eq!(vol.info().version, 2);
881 assert_eq!(vol.info().cipher_name, "aes");
882 assert_eq!(vol.info().cipher_mode, "xts-plain64");
883 assert_eq!(vol.info().sector_size, 4096);
884 assert_eq!(vol.info().key_bytes, 64);
885 assert_eq!(vol.master_key(), &master[..]);
886
887 let mut buf = [0u8; 512];
888 vol.read_at(0, &mut buf).unwrap();
889 assert_eq!(&buf[..], &plain[..512]);
890 }
891
892 #[test]
893 fn luks2_pbkdf2_roundtrip() {
894 let master = vec![0x1Eu8; 64];
895 let salt = [0x88u8; 16];
896 let slot_key = derive_key_within("sha256", PASS, &salt, 5, 64, DERIVATION_BUDGET).unwrap();
897 let b64 = base64::engine::general_purpose::STANDARD;
898 let kdf = format!(
899 "{{\"type\":\"pbkdf2\",\"hash\":\"sha256\",\"iterations\":5,\"salt\":\"{}\"}}",
900 b64.encode(salt)
901 );
902 let (img, plain) = build_luks2(&master, &slot_key, &kdf, 512);
903
904 let mut vol = LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS).unwrap();
905 assert_eq!(vol.info().sector_size, 512);
906 let mut buf = [0u8; 512];
907 vol.read_at(0, &mut buf).unwrap();
908 assert_eq!(&buf[..], &plain[..512]);
909 }
910
911 #[test]
922 fn luks2_pbkdf2_unlock_is_bounded_in_aggregate_not_just_per_derivation() {
923 let master = vec![0x44u8; 64];
924 let salt = [0x88u8; 16];
925 let slot_key = derive_key_within("sha256", PASS, &salt, 5, 64, DERIVATION_BUDGET).unwrap();
926 let b64 = base64::engine::general_purpose::STANDARD;
927 let kdf = format!(
928 "{{\"type\":\"pbkdf2\",\"hash\":\"sha256\",\"iterations\":5,\"salt\":\"{}\"}}",
929 b64.encode(salt)
930 );
931 let (img, _plain) = build_luks2(&master, &slot_key, &kdf, 512);
932
933 match LuksVolume::unlock2_with_passphrase_within(Cursor::new(img), PASS, Duration::ZERO) {
936 Err(LuksError::UnlockBudgetExceeded { slots_tried, .. }) => assert_eq!(slots_tried, 0),
937 Err(other) => panic!("expected UnlockBudgetExceeded, got {other:?}"), Ok(_) => panic!("a zero total budget must refuse before doing derivation work"), }
940 }
941
942 #[test]
947 fn luks2_argon2_unlock_is_bounded_in_aggregate_too() {
948 let master = vec![0xC3u8; 64];
949 let salt = [0x77u8; 16];
950 let slot_key = derive_key_argon2_within(
951 &Argon2Params {
952 kind: "argon2id",
953 time: 1,
954 memory: 32,
955 cpus: 1,
956 salt: &salt,
957 },
958 PASS,
959 64,
960 DERIVATION_BUDGET,
961 )
962 .unwrap();
963 let b64 = base64::engine::general_purpose::STANDARD;
964 let kdf = format!(
965 "{{\"type\":\"argon2id\",\"time\":1,\"memory\":32,\"cpus\":1,\"salt\":\"{}\"}}",
966 b64.encode(salt)
967 );
968 let (img, _plain) = build_luks2(&master, &slot_key, &kdf, 512);
969
970 match LuksVolume::unlock2_with_passphrase_within(Cursor::new(img), PASS, Duration::ZERO) {
971 Err(LuksError::UnlockBudgetExceeded { slots_tried, .. }) => assert_eq!(slots_tried, 0),
972 Err(other) => panic!("expected UnlockBudgetExceeded, got {other:?}"), Ok(_) => panic!("a zero total budget must refuse before running Argon2"), }
975 }
976
977 #[test]
983 fn autodetect_unlock_within_bounds_both_versions() {
984 let (img1, _) = build_luks1(&[0x33u8; L1_KEY_BYTES as usize]);
985 match LuksVolume::unlock_with_passphrase_within(Cursor::new(img1), PASS, Duration::ZERO) {
986 Err(LuksError::UnlockBudgetExceeded { .. }) => {}
987 Err(other) => panic!("LUKS1: expected UnlockBudgetExceeded, got {other:?}"), Ok(_) => panic!("LUKS1: a zero total budget must refuse"), }
990
991 let salt = [0x88u8; 16];
992 let slot_key = derive_key_within("sha256", PASS, &salt, 5, 64, DERIVATION_BUDGET).unwrap();
993 let b64 = base64::engine::general_purpose::STANDARD;
994 let kdf = format!(
995 "{{\"type\":\"pbkdf2\",\"hash\":\"sha256\",\"iterations\":5,\"salt\":\"{}\"}}",
996 b64.encode(salt)
997 );
998 let (img2, _) = build_luks2(&[0x44u8; 64], &slot_key, &kdf, 512);
999 match LuksVolume::unlock_with_passphrase_within(Cursor::new(img2), PASS, Duration::ZERO) {
1000 Err(LuksError::UnlockBudgetExceeded { .. }) => {}
1001 Err(other) => panic!("LUKS2: expected UnlockBudgetExceeded, got {other:?}"), Ok(_) => panic!("LUKS2: a zero total budget must refuse"), }
1004 }
1005
1006 #[test]
1007 fn luks2_no_crypt_segment() {
1008 let json = r#"{"keyslots":{"0":{"key_size":64,
1009 "af":{"stripes":8,"hash":"sha256"},
1010 "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
1011 "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
1012 "segments":{},"digests":{}}"#;
1013 let img = build_luks2_json(json);
1014 assert!(matches!(
1015 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1016 Err(LuksError::NoActiveKeyslot)
1017 ));
1018 }
1019
1020 #[test]
1021 fn luks2_no_keyslots() {
1022 let json = r#"{"keyslots":{},
1023 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1024 "digests":{}}"#;
1025 let img = build_luks2_json(json);
1026 assert!(matches!(
1027 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1028 Err(LuksError::NoActiveKeyslot)
1029 ));
1030 }
1031
1032 #[test]
1033 fn luks2_short_key_material_is_skipped() {
1034 let json = r#"{"keyslots":{"0":{"key_size":64,
1036 "af":{"stripes":8,"hash":"sha256"},
1037 "area":{"offset":"9999999","size":"512","encryption":"aes-xts-plain64"},
1038 "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
1039 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1040 "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"hash":"sha256","iterations":5,"salt":"AAAA","digest":"AAAA"}}}"#;
1041 let img = build_luks2_json(json);
1042 assert!(matches!(
1043 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1044 Err(LuksError::AuthenticationFailed)
1045 ));
1046 }
1047
1048 fn build_luks2_json(json: &str) -> Vec<u8> {
1051 let hdr_size = LUKS2_BIN_HDR_LEN + 4096;
1052 let total = 16384usize;
1053 let mut img = vec![0u8; total];
1054 img[0..6].copy_from_slice(&LUKS_MAGIC);
1055 img[6..8].copy_from_slice(&2u16.to_be_bytes());
1056 img[8..16].copy_from_slice(&(hdr_size as u64).to_be_bytes());
1057 img[LUKS2_BIN_HDR_LEN..LUKS2_BIN_HDR_LEN + json.len()].copy_from_slice(json.as_bytes());
1058 img
1059 }
1060
1061 #[test]
1064 fn luks1_unsupported_hash_errors() {
1065 let mut img = vec![0u8; 4096];
1068 img[0..6].copy_from_slice(&LUKS_MAGIC);
1069 img[6..8].copy_from_slice(&1u16.to_be_bytes());
1070 img[8..11].copy_from_slice(b"aes");
1071 img[40..51].copy_from_slice(b"xts-plain64");
1072 img[72..75].copy_from_slice(b"md5");
1073 img[104..108].copy_from_slice(&4u32.to_be_bytes());
1074 img[108..112].copy_from_slice(&64u32.to_be_bytes());
1075 let base = 208;
1076 img[base..base + 4].copy_from_slice(&KEY_ENABLED.to_be_bytes());
1077 img[base + 4..base + 8].copy_from_slice(&5u32.to_be_bytes());
1078 img[base + 40..base + 44].copy_from_slice(&1u32.to_be_bytes());
1079 img[base + 44..base + 48].copy_from_slice(&8u32.to_be_bytes());
1080 assert!(matches!(
1081 LuksVolume::unlock1_with_passphrase(Cursor::new(img), PASS),
1082 Err(LuksError::Unsupported { what: "hash", .. })
1083 ));
1084 }
1085
1086 #[test]
1087 fn luks2_bad_argon2_params_errors() {
1088 let json = r#"{"keyslots":{"0":{"key_size":64,
1090 "af":{"stripes":8,"hash":"sha256"},
1091 "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
1092 "kdf":{"type":"argon2id","time":1,"memory":0,"cpus":1,"salt":"AAAAAAAAAAA="}}},
1093 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1094 "digests":{}}"#;
1095 let img = build_luks2_json(json);
1096 assert!(matches!(
1097 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1098 Err(LuksError::Unsupported { .. })
1099 ));
1100 }
1101
1102 #[test]
1108 fn luks2_unsupported_keyslot_kdf_hash_errors() {
1109 let json = r#"{"keyslots":{"0":{"key_size":64,
1110 "af":{"stripes":8,"hash":"sha256"},
1111 "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
1112 "kdf":{"type":"pbkdf2","hash":"md5","iterations":5,"salt":"AAAA"}}},
1113 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1114 "digests":{}}"#;
1115 let img = build_luks2_json(json);
1116 assert!(matches!(
1117 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1118 Err(LuksError::Unsupported { what: "hash", .. })
1119 ));
1120 }
1121
1122 #[test]
1123 fn luks2_unsupported_af_hash_errors() {
1124 let json = r#"{"keyslots":{"0":{"key_size":64,
1127 "af":{"stripes":8,"hash":"md5"},
1128 "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
1129 "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
1130 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1131 "digests":{}}"#;
1132 let img = build_luks2_json(json);
1133 assert!(matches!(
1134 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1135 Err(LuksError::Unsupported { what: "hash", .. })
1136 ));
1137 }
1138
1139 #[test]
1140 fn luks2_unsupported_digest_hash_errors() {
1141 let json = r#"{"keyslots":{"0":{"key_size":64,
1143 "af":{"stripes":8,"hash":"sha256"},
1144 "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
1145 "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
1146 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1147 "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"hash":"md5","iterations":5,"salt":"AAAA","digest":"AAAA"}}}"#;
1148 let img = build_luks2_json(json);
1149 assert!(matches!(
1150 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1151 Err(LuksError::Unsupported { what: "hash", .. })
1152 ));
1153 }
1154
1155 #[test]
1156 fn luks2_digest_mismatch_falls_through() {
1157 let json = r#"{"keyslots":{"0":{"key_size":64,
1162 "af":{"stripes":8,"hash":"sha256"},
1163 "area":{"offset":"8192","size":"512","encryption":"aes-xts-plain64"},
1164 "kdf":{"type":"pbkdf2","hash":"sha256","iterations":5,"salt":"AAAA"}}},
1165 "segments":{"0":{"type":"crypt","offset":"12288","encryption":"aes-xts-plain64","sector_size":512}},
1166 "digests":{"0":{"type":"pbkdf2","keyslots":["0"],"hash":"sha256","iterations":5,"salt":"AAAA","digest":"AAAA"},
1167 "1":{"type":"pbkdf2","keyslots":["9"],"hash":"sha256","iterations":5,"salt":"AAAA","digest":"AAAA"}}}"#;
1168 let img = build_luks2_json(json);
1169 assert!(matches!(
1170 LuksVolume::unlock2_with_passphrase(Cursor::new(img), PASS),
1171 Err(LuksError::AuthenticationFailed)
1172 ));
1173 }
1174
1175 struct InterruptOnce(bool);
1178 impl Read for InterruptOnce {
1179 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1180 if self.0 {
1181 Ok(0)
1182 } else {
1183 self.0 = true;
1184 Err(std::io::Error::from(std::io::ErrorKind::Interrupted))
1185 }
1186 }
1187 }
1188
1189 struct AlwaysError;
1190 impl Read for AlwaysError {
1191 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1192 Err(std::io::Error::other("boom"))
1193 }
1194 }
1195
1196 #[test]
1197 fn read_available_retries_on_interrupted() {
1198 let mut r = InterruptOnce(false);
1199 let mut buf = [0xFFu8; 4];
1200 assert_eq!(read_available(&mut r, &mut buf).unwrap(), 0);
1201 assert_eq!(buf, [0u8; 4]); }
1203
1204 #[test]
1205 fn read_available_propagates_hard_error() {
1206 let mut r = AlwaysError;
1207 let mut buf = [0u8; 4];
1208 assert!(read_available(&mut r, &mut buf).is_err());
1209 }
1210}