use eyre::{format_err, Result}; use log::{error, info, warn}; use std::collections::BTreeSet as Set; use std::process::Stdio; use tokio::io::AsyncWriteExt; use tokio::process::Command; use tokio::sync::Mutex; use super::{retry_or_ignore, USED_DEVS}; use crate::blockdev::{is_uninitialized, uninitialize}; use crate::fs::walk_dir; use crate::input; use dkl::bootstrap::{CryptDev, DevFilter}; pub async fn setup(devs: &[CryptDev]) { if devs.is_empty() { return; } let mut used_devs = USED_DEVS.lock().await; // CryptDev.name that have a least one assignment done let mut done = Set::new(); // dmcrypt devices opened here let mut done_crypt = Set::new(); retry_or_ignore(async || { let all_devs = walk_dir("/dev").await; for dev in devs { let mut mappings = find_dev(dev, &all_devs).await?; mappings.retain(|(_, dev_path)| !used_devs.contains(dev_path)); if mappings.is_empty() && !dev.optional() && !done.contains(&dev.name) { return Err(format_err!("no device found for crypt dev {}", dev.name)); } for (crypt_dev, dev_path) in mappings { if done_crypt.contains(&crypt_dev) { continue; } info!("crypt dev {crypt_dev}: using {dev_path}"); crypt_open(&crypt_dev, &dev_path).await?; done_crypt.insert(crypt_dev); used_devs.insert(dev_path); done.insert(&dev.name); } } Ok(()) }) .await; } struct PrevPw { pw: String, reuse: bool, } impl PrevPw { fn is_set(&self) -> bool { !self.pw.is_empty() } fn can_reuse(&self) -> bool { self.reuse && self.is_set() } fn invalidate(&mut self) { self.pw = String::new(); self.reuse = false; } async fn input(&mut self, prompt: impl std::fmt::Display) -> String { if self.can_reuse() { info!("reusing password"); self.pw.clone() } else if self.is_set() { let pw = input::read_password(format!("{prompt} (\"\" reuse, \"*\" auto-reuse)? ")).await; match pw.as_str() { "" => self.pw.clone(), "*" => { self.reuse = true; self.pw.clone() } _ => { self.pw = pw.clone(); pw } } } else { let pw = loop { let pw = input::read_password(format!("{prompt}? ")).await; if pw.is_empty() { error!("empty password provided!"); continue; } break pw; }; self.pw = pw.clone(); pw } } } static PREV_PW: Mutex = Mutex::const_new(PrevPw { pw: String::new(), reuse: false, }); async fn crypt_open(crypt_dev: &str, dev_path: &str) -> Result<()> { 'open_loop: loop { let mut prev_pw = PREV_PW.lock().await; let pw = prev_pw .input(format!("crypt password for {crypt_dev}")) .await; if cryptsetup(&pw, ["open", dev_path, crypt_dev]).await? { return Ok(()); } error!("crypt open {crypt_dev} from {dev_path} failed"); if is_uninitialized(dev_path).await? { // we can format the device info!("{dev_path} looks uninitialized, it may be formatted"); match input::read_choice(["[f]ormat", "[r]etry", "[i]gnore"]).await { 'r' => continue 'open_loop, 'i' => return Ok(()), 'f' => { if !cryptsetup(&pw, ["luksFormat", dev_path]).await? { return Err(format_err!("cryptsetup luksFormat failed")); } if !cryptsetup(&pw, ["open", dev_path, crypt_dev]).await? { return Err(format_err!("open after format failed")); } if let Err(e) = uninitialize(&format!("/dev/mapper/{crypt_dev}")).await { warn!("uninitialize failed (ignored): {e}"); } return Ok(()); } _ => unreachable!(), } } // device looks initialized, don't allow format warn!("{dev_path} looks initialized, formatting not allowed from init"); prev_pw.invalidate(); match input::read_choice(["[r]etry", "[i]gnore"]).await { 'r' => continue 'open_loop, 'i' => return Ok(()), _ => unreachable!(), } } } async fn cryptsetup(pw: &str, args: [&str; N]) -> Result { let mut child = Command::new("cryptsetup") .args(args) .arg("--key-file=-") .stdin(Stdio::piped()) .spawn()?; (child.stdin.as_mut().unwrap()) .write_all(pw.as_bytes()) .await?; Ok(child.wait().await?.success()) } async fn find_dev(dev: &CryptDev, all_devs: &[String]) -> Result> { let dev_name = &dev.name; Ok(match dev.filter() { DevFilter::None => vec![], DevFilter::Dev(path) => (all_devs.iter()) .filter(|dev_path| dev_path == &path) .map(|dev_path| (dev.name.clone(), dev_path.clone())) .collect(), DevFilter::Prefix(prefix) => (all_devs.iter()) .filter_map(|path| { let suffix = path.strip_prefix(prefix)?; Some((format!("{dev_name}{suffix}"), path.clone())) }) .collect(), DevFilter::Udev(filter) => { use crate::udev; let devs = udev::all().await?; let filter: udev::Filter = filter.clone().into(); (devs.iter()) .filter(|dev| dev.subsystem() == Some("block") && filter.matches(dev)) .filter_map(|dev| { let path = dev.property("DEVNAME")?.to_string(); let mut name = dev_name.replace("${name}", dev.name()?); for (p, v) in dev.properties() { name = name.replace(&format!("${{{p}}}"), v); } Some((name, path)) }) .collect() } }) }