feat(mount): make unlock policy optional/explict

This changes the semantics of some arguments related to unlocking and
slightly changes the unlocking logic. Also update help formatting/text.

Instead of defaulting to `UnlockPolicy::Ask`, the argument becomes
optional. That means if it is specified, the user really wants that
specific policy. Similar to how `passphrase_file` also works.

This also extends `UnlockPolicy` to override `isatty` detection.

Fixes: #292
Signed-off-by: Thomas Mühlbacher <tmuehlbacher@posteo.net>
This commit is contained in:
Thomas Mühlbacher 2024-06-26 18:00:48 +02:00
parent a411e7237f
commit 9bd3ada1d1
2 changed files with 57 additions and 47 deletions

View File

@ -215,28 +215,20 @@ fn get_uuid_for_dev_node(
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
pub struct Cli { pub struct Cli {
/// Path to passphrase/key file /// Path to passphrase file
/// ///
/// Precedes key_location/unlock_policy: if the filesystem can be decrypted /// This can be used to optionally specify a file to read the passphrase
/// by the specified passphrase file; it is decrypted. (i.e. Regardless /// from. An explictly specified key_location/unlock_policy overrides this
/// if "fail" is specified for key_location/unlock_policy.) /// argument.
#[arg(short = 'f', long)] #[arg(short = 'f', long)]
passphrase_file: Option<PathBuf>, passphrase_file: Option<PathBuf>,
/// Password policy to use in case of encrypted filesystem. /// Passphrase policy to use in case of an encrypted filesystem. If not
/// /// specified, the password will be searched for in the keyring. If not
/// Possible values are: /// found, the password will be prompted or read from stdin, depending on
/// "fail" - don't ask for password, fail if filesystem is encrypted; /// whether the stdin is connected to a terminal or not.
/// "wait" - wait for password to become available before mounting; #[arg(short = 'k', long = "key_location", value_enum)]
/// "ask" - prompt the user for password; unlock_policy: Option<UnlockPolicy>,
#[arg(
short = 'k',
long = "key_location",
value_enum,
default_value_t,
verbatim_doc_comment
)]
unlock_policy: UnlockPolicy,
/// Device, or UUID=\<UUID\> /// Device, or UUID=\<UUID\>
dev: String, dev: String,
@ -305,7 +297,23 @@ fn devs_str_sbs_from_device(
} }
} }
fn cmd_mount_inner(cli: Cli) -> Result<()> { /// If a user explicitly specifies `unlock_policy` or `passphrase_file` then use
/// that without falling back to other mechanisms. If these options are not
/// used, then search for the key or ask for it.
fn handle_unlock(cli: &Cli, sb: &bch_sb_handle) -> Result<KeyHandle> {
if let Some(policy) = cli.unlock_policy.as_ref() {
return policy.apply(sb);
}
if let Some(path) = cli.passphrase_file.as_deref() {
return Passphrase::new_from_file(path).and_then(|p| KeyHandle::new(sb, &p));
}
KeyHandle::new_from_search(&sb.sb().uuid())
.or_else(|_| Passphrase::new().and_then(|p| KeyHandle::new(sb, &p)))
}
fn cmd_mount_inner(cli: &Cli) -> Result<()> {
// Grab the udev information once // Grab the udev information once
let udev_info = udev_bcachefs_info()?; let udev_info = udev_bcachefs_info()?;
@ -325,7 +333,7 @@ fn cmd_mount_inner(cli: Cli) -> Result<()> {
.map(read_super_silent) .map(read_super_silent)
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
(cli.dev, sbs) (cli.dev.clone(), sbs)
} else { } else {
devs_str_sbs_from_device(&udev_info, Path::new(&cli.dev))? devs_str_sbs_from_device(&udev_info, Path::new(&cli.dev))?
} }
@ -334,26 +342,11 @@ fn cmd_mount_inner(cli: Cli) -> Result<()> {
ensure!(!sbs.is_empty(), "No device(s) to mount specified"); ensure!(!sbs.is_empty(), "No device(s) to mount specified");
let first_sb = sbs[0]; let first_sb = sbs[0];
let uuid = first_sb.sb().uuid();
if unsafe { bcachefs::bch2_sb_is_encrypted(first_sb.sb) } { if unsafe { bcachefs::bch2_sb_is_encrypted(first_sb.sb) } {
let _key_handle: KeyHandle = KeyHandle::new_from_search(&uuid).or_else(|_| { handle_unlock(cli, &first_sb)?;
cli.passphrase_file
.and_then(|path| match Passphrase::new_from_file(path) {
Ok(p) => Some(KeyHandle::new(&first_sb, &p)),
Err(e) => {
error!(
"Failed to read passphrase from file, falling back to prompt: {}",
e
);
None
}
})
.unwrap_or_else(|| cli.unlock_policy.apply(&first_sb))
})?;
} }
if let Some(mountpoint) = cli.mountpoint { if let Some(mountpoint) = cli.mountpoint.as_deref() {
info!( info!(
"mounting with params: device: {}, target: {}, options: {}", "mounting with params: device: {}, target: {}, options: {}",
devices, devices,
@ -391,7 +384,7 @@ pub fn mount(mut argv: Vec<String>, symlink_cmd: Option<&str>) -> i32 {
}); });
colored::control::set_override(cli.colorize); colored::control::set_override(cli.colorize);
if let Err(e) = cmd_mount_inner(cli) { if let Err(e) = cmd_mount_inner(&cli) {
error!("Fatal error: {}", e); error!("Fatal error: {}", e);
1 1
} else { } else {

View File

@ -25,9 +25,14 @@ const BCH_KEY_MAGIC: &str = "bch**key";
#[derive(Clone, Debug, clap::ValueEnum, strum::Display)] #[derive(Clone, Debug, clap::ValueEnum, strum::Display)]
pub enum UnlockPolicy { pub enum UnlockPolicy {
/// Don't ask for passphrase, fail if filesystem is encrypted
Fail, Fail,
/// Wait for passphrase to become available before mounting
Wait, Wait,
/// Interactively prompt the user for a passphrase
Ask, Ask,
/// Try to read the passphrase from `stdin` without prompting
Stdin,
} }
impl UnlockPolicy { impl UnlockPolicy {
@ -40,6 +45,7 @@ impl UnlockPolicy {
Self::Fail => Err(anyhow!("no passphrase available")), Self::Fail => Err(anyhow!("no passphrase available")),
Self::Wait => Ok(KeyHandle::wait_for_unlock(&uuid)?), Self::Wait => Ok(KeyHandle::wait_for_unlock(&uuid)?),
Self::Ask => Passphrase::new_from_prompt().and_then(|p| KeyHandle::new(sb, &p)), Self::Ask => Passphrase::new_from_prompt().and_then(|p| KeyHandle::new(sb, &p)),
Self::Stdin => Passphrase::new_from_stdin().and_then(|p| KeyHandle::new(sb, &p)),
} }
} }
} }
@ -154,20 +160,31 @@ impl Passphrase {
&self.0 &self.0
} }
// blocks indefinitely if no input is available on stdin pub fn new() -> Result<Self> {
fn new_from_prompt() -> Result<Self> { if stdin().is_terminal() {
let passphrase = if stdin().is_terminal() { Self::new_from_prompt()
Zeroizing::new(rpassword::prompt_password("Enter passphrase: ")?)
} else { } else {
info!("Trying to read passphrase from stdin..."); Self::new_from_stdin()
let mut line = Zeroizing::new(String::new()); }
stdin().read_line(&mut line)?; }
line
}; // blocks indefinitely if no input is available on stdin
pub fn new_from_prompt() -> Result<Self> {
let passphrase = Zeroizing::new(rpassword::prompt_password("Enter passphrase: ")?);
Ok(Self(CString::new(passphrase.trim_end_matches('\n'))?)) Ok(Self(CString::new(passphrase.trim_end_matches('\n'))?))
} }
// blocks indefinitely if no input is available on stdin
pub fn new_from_stdin() -> Result<Self> {
info!("Trying to read passphrase from stdin...");
let mut line = Zeroizing::new(String::new());
stdin().read_line(&mut line)?;
Ok(Self(CString::new(line.trim_end_matches('\n'))?))
}
pub fn new_from_file(passphrase_file: impl AsRef<Path>) -> Result<Self> { pub fn new_from_file(passphrase_file: impl AsRef<Path>) -> Result<Self> {
let passphrase_file = passphrase_file.as_ref(); let passphrase_file = passphrase_file.as_ref();