diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/commands/mount.rs | 113 | ||||
-rw-r--r-- | src/key.rs | 111 |
2 files changed, 124 insertions, 100 deletions
diff --git a/src/commands/mount.rs b/src/commands/mount.rs index 5782b3ab..18b4aae5 100644 --- a/src/commands/mount.rs +++ b/src/commands/mount.rs @@ -215,28 +215,20 @@ fn get_uuid_for_dev_node( #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct Cli { - /// Path to passphrase/key file + /// Path to passphrase file /// - /// Precedes key_location/unlock_policy: if the filesystem can be decrypted - /// by the specified passphrase file; it is decrypted. (i.e. Regardless - /// if "fail" is specified for key_location/unlock_policy.) + /// This can be used to optionally specify a file to read the passphrase + /// from. An explictly specified key_location/unlock_policy overrides this + /// argument. #[arg(short = 'f', long)] passphrase_file: Option<PathBuf>, - /// Password policy to use in case of encrypted filesystem. - /// - /// Possible values are: - /// "fail" - don't ask for password, fail if filesystem is encrypted; - /// "wait" - wait for password to become available before mounting; - /// "ask" - prompt the user for password; - #[arg( - short = 'k', - long = "key_location", - value_enum, - default_value_t, - verbatim_doc_comment - )] - unlock_policy: UnlockPolicy, + /// Passphrase policy to use in case of an encrypted filesystem. If not + /// specified, the password will be searched for in the keyring. If not + /// found, the password will be prompted or read from stdin, depending on + /// whether the stdin is connected to a terminal or not. + #[arg(short = 'k', long = "key_location", value_enum)] + unlock_policy: Option<UnlockPolicy>, /// Device, or UUID=\<UUID\> dev: String, @@ -305,68 +297,67 @@ fn devs_str_sbs_from_device( } } -fn cmd_mount_inner(opt: 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 let udev_info = udev_bcachefs_info()?; - let (devices, sbs) = if let Some(uuid) = opt.dev.strip_prefix("UUID=") { - devs_str_sbs_from_uuid(&udev_info, uuid)? - } else if let Some(uuid) = opt.dev.strip_prefix("OLD_BLKID_UUID=") { + let (devices, sbs) = if let Some(("UUID" | "OLD_BLKID_UUID", uuid)) = cli.dev.split_once('=') { devs_str_sbs_from_uuid(&udev_info, uuid)? + } else if cli.dev.contains(':') { + // If the device string contains ":" we will assume the user knows the + // entire list. If they supply a single device it could be either the FS + // only has 1 device or it's only 1 of a number of devices which are + // part of the FS. This appears to be the case when we get called during + // fstab mount processing and the fstab specifies a UUID. + + let sbs = cli + .dev + .split(':') + .map(read_super_silent) + .collect::<Result<Vec<_>>>()?; + + (cli.dev.clone(), sbs) } else { - // If the device string contains ":" we will assume the user knows the entire list. - // If they supply a single device it could be either the FS only has 1 device or it's - // only 1 of a number of devices which are part of the FS. This appears to be the case - // when we get called during fstab mount processing and the fstab specifies a UUID. - if opt.dev.contains(':') { - let sbs = opt - .dev - .split(':') - .map(read_super_silent) - .collect::<Result<Vec<_>>>()?; - - (opt.dev, sbs) - } else { - devs_str_sbs_from_device(&udev_info, Path::new(&opt.dev))? - } + devs_str_sbs_from_device(&udev_info, Path::new(&cli.dev))? }; ensure!(!sbs.is_empty(), "No device(s) to mount specified"); let first_sb = sbs[0]; - let uuid = first_sb.sb().uuid(); - if unsafe { bcachefs::bch2_sb_is_encrypted(first_sb.sb) } { - let _key_handle: KeyHandle = KeyHandle::new_from_search(&uuid).or_else(|_| { - opt.passphrase_file - .and_then(|path| match Passphrase::new_from_file(&first_sb, 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(|| opt.unlock_policy.apply(&first_sb)) - })?; + handle_unlock(cli, &first_sb)?; } - if let Some(mountpoint) = opt.mountpoint { + if let Some(mountpoint) = cli.mountpoint.as_deref() { info!( "mounting with params: device: {}, target: {}, options: {}", devices, mountpoint.to_string_lossy(), - &opt.options + &cli.options ); - let (data, mountflags) = parse_mount_options(&opt.options); + let (data, mountflags) = parse_mount_options(&cli.options); mount_inner(devices, mountpoint, "bcachefs", mountflags, data) } else { info!( "would mount with params: device: {}, options: {}", - devices, &opt.options + devices, &cli.options ); Ok(()) @@ -381,17 +372,17 @@ pub fn mount(mut argv: Vec<String>, symlink_cmd: Option<&str>) -> i32 { argv.remove(0); } - let opt = Cli::parse_from(argv); + let cli = Cli::parse_from(argv); // @TODO : more granular log levels via mount option - log::set_max_level(match opt.verbose { + log::set_max_level(match cli.verbose { 0 => LevelFilter::Warn, 1 => LevelFilter::Trace, 2_u8..=u8::MAX => todo!(), }); - colored::control::set_override(opt.colorize); - if let Err(e) = cmd_mount_inner(opt) { + colored::control::set_override(cli.colorize); + if let Err(e) = cmd_mount_inner(&cli) { error!("Fatal error: {}", e); 1 } else { @@ -11,7 +11,7 @@ use std::{ use anyhow::{anyhow, ensure, Result}; use bch_bindgen::{ bcachefs::{self, bch_key, bch_sb_handle}, - c::{bch2_chacha_encrypt_key, bch_sb_field_crypt}, + c::{bch2_chacha_encrypt_key, bch_encrypted_key, bch_sb_field_crypt}, keyutils::{self, keyctl_search}, }; use byteorder::{LittleEndian, ReadBytesExt}; @@ -25,9 +25,15 @@ const BCH_KEY_MAGIC: &str = "bch**key"; #[derive(Clone, Debug, clap::ValueEnum, strum::Display)] pub enum UnlockPolicy { + /// Don't ask for passphrase, if the key cannot be found in the keyring just + /// fail Fail, + /// Wait for passphrase to become available before mounting Wait, + /// Interactively prompt the user for a passphrase Ask, + /// Try to read the passphrase from `stdin` without prompting + Stdin, } impl UnlockPolicy { @@ -37,9 +43,10 @@ impl UnlockPolicy { info!("Using filesystem unlock policy '{self}' on {uuid}"); match self { - Self::Fail => Err(anyhow!("no passphrase available")), + Self::Fail => KeyHandle::new_from_search(&uuid), Self::Wait => Ok(KeyHandle::wait_for_unlock(&uuid)?), 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)), } } } @@ -63,44 +70,24 @@ impl KeyHandle { } pub fn new(sb: &bch_sb_handle, passphrase: &Passphrase) -> Result<Self> { - let bch_key_magic = BCH_KEY_MAGIC.as_bytes().read_u64::<LittleEndian>().unwrap(); - - let crypt = sb.sb().crypt().unwrap(); - let crypt_ptr = (crypt as *const bch_sb_field_crypt).cast_mut(); - - let mut output: bch_key = - unsafe { bcachefs::derive_passphrase(crypt_ptr, passphrase.get().as_ptr()) }; - - let mut key = *crypt.key(); - - let ret = unsafe { - bch2_chacha_encrypt_key( - ptr::addr_of_mut!(output), - sb.sb().nonce(), - ptr::addr_of_mut!(key).cast(), - mem::size_of_val(&key), - ) - }; - - ensure!(ret == 0, "chacha decryption failure"); - ensure!(key.magic == bch_key_magic, "failed to verify passphrase"); - let key_name = Self::format_key_name(&sb.sb().uuid()); let key_name = CStr::as_ptr(&key_name); let key_type = c_str!("user"); + let (passphrase_key, _sb_key) = passphrase.check(sb)?; + let key_id = unsafe { keyutils::add_key( key_type, key_name, - ptr::addr_of!(output).cast(), - mem::size_of_val(&output), + ptr::addr_of!(passphrase_key).cast(), + mem::size_of_val(&passphrase_key), keyutils::KEY_SPEC_USER_KEYRING, ) }; if key_id > 0 { - info!("Found key in keyring"); + info!("Added key to keyring"); Ok(KeyHandle { _uuid: sb.sb().uuid(), _id: c_long::from(key_id), @@ -154,26 +141,36 @@ impl Passphrase { &self.0 } - // blocks indefinitely if no input is available on stdin - fn new_from_prompt() -> Result<Self> { - let passphrase = if stdin().is_terminal() { - Zeroizing::new(rpassword::prompt_password("Enter passphrase: ")?) + pub fn new() -> Result<Self> { + if stdin().is_terminal() { + Self::new_from_prompt() } else { - info!("Trying to read passphrase from stdin..."); - let mut line = Zeroizing::new(String::new()); - stdin().read_line(&mut line)?; - line - }; + Self::new_from_stdin() + } + } + + // 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'))?)) } - pub fn new_from_file(sb: &bch_sb_handle, passphrase_file: impl AsRef<Path>) -> Result<Self> { + // 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> { let passphrase_file = passphrase_file.as_ref(); info!( - "Attempting to unlock key for filesystem {} with passphrase from file {}", - sb.sb().uuid(), + "Attempting to unlock key with passphrase from file {}", passphrase_file.display() ); @@ -181,4 +178,40 @@ impl Passphrase { Ok(Self(CString::new(passphrase.trim_end_matches('\n'))?)) } + + fn derive(&self, crypt: &bch_sb_field_crypt) -> bch_key { + let crypt_ptr = (crypt as *const bch_sb_field_crypt).cast_mut(); + + unsafe { bcachefs::derive_passphrase(crypt_ptr, self.get().as_ptr()) } + } + + pub fn check(&self, sb: &bch_sb_handle) -> Result<(bch_key, bch_encrypted_key)> { + let bch_key_magic = BCH_KEY_MAGIC.as_bytes().read_u64::<LittleEndian>().unwrap(); + + let crypt = sb + .sb() + .crypt() + .ok_or_else(|| anyhow!("filesystem is not encrypted"))?; + let mut sb_key = *crypt.key(); + + ensure!( + sb_key.magic != bch_key_magic, + "filesystem does not have encryption key" + ); + + let mut passphrase_key: bch_key = self.derive(crypt); + + let ret = unsafe { + bch2_chacha_encrypt_key( + ptr::addr_of_mut!(passphrase_key), + sb.sb().nonce(), + ptr::addr_of_mut!(sb_key).cast(), + mem::size_of_val(&sb_key), + ) + }; + ensure!(ret == 0, "error encrypting key"); + ensure!(sb_key.magic == bch_key_magic, "incorrect passphrase"); + + Ok((passphrase_key, sb_key)) + } } |