diff options
Diffstat (limited to 'rust-src/mount/src')
-rw-r--r-- | rust-src/mount/src/filesystem.rs | 174 | ||||
-rw-r--r-- | rust-src/mount/src/key.rs | 96 | ||||
-rw-r--r-- | rust-src/mount/src/keyutils_wrapper.h | 1 | ||||
-rw-r--r-- | rust-src/mount/src/lib.rs | 190 | ||||
-rw-r--r-- | rust-src/mount/src/libbcachefs_wrapper.h | 4 |
5 files changed, 465 insertions, 0 deletions
diff --git a/rust-src/mount/src/filesystem.rs b/rust-src/mount/src/filesystem.rs new file mode 100644 index 00000000..36af8c05 --- /dev/null +++ b/rust-src/mount/src/filesystem.rs @@ -0,0 +1,174 @@ +extern "C" { + pub static stdout: *mut libc::FILE; +} + +use getset::{CopyGetters, Getters}; +use std::path::PathBuf; +#[derive(Getters, CopyGetters)] +pub struct FileSystem { + /// External UUID of the bcachefs + #[getset(get = "pub")] + uuid: uuid::Uuid, + /// Whether filesystem is encrypted + #[getset(get_copy = "pub")] + encrypted: bool, + /// Super block + #[getset(get = "pub")] + sb: bcachefs::bch_sb_handle, + /// Member devices for this filesystem + #[getset(get = "pub")] + devices: Vec<PathBuf>, +} + +/// Parse a comma-separated mount options and split out mountflags and filesystem +/// specific options. +fn parse_mount_options(options: impl AsRef<str>) -> (Option<String>, u64) { + use either::Either::*; + let (opts, flags) = options + .as_ref() + .split(",") + .map(|o| match o { + "dirsync" => Left(libc::MS_DIRSYNC), + "lazytime" => Left(1 << 25), // MS_LAZYTIME + "mand" => Left(libc::MS_MANDLOCK), + "noatime" => Left(libc::MS_NOATIME), + "nodev" => Left(libc::MS_NODEV), + "nodiratime" => Left(libc::MS_NODIRATIME), + "noexec" => Left(libc::MS_NOEXEC), + "nosuid" => Left(libc::MS_NOSUID), + "ro" => Left(libc::MS_RDONLY), + "rw" => Left(0), + "relatime" => Left(libc::MS_RELATIME), + "strictatime" => Left(libc::MS_STRICTATIME), + "sync" => Left(libc::MS_SYNCHRONOUS), + "" => Left(0), + o @ _ => Right(o), + }) + .fold((Vec::new(), 0), |(mut opts, flags), next| match next { + Left(f) => (opts, flags | f), + Right(o) => { + opts.push(o); + (opts, flags) + } + }); + + use itertools::Itertools; + ( + if opts.len() == 0 { + None + } else { + Some(opts.iter().join(",")) + }, + flags, + ) +} + +impl FileSystem { + pub(crate) fn new(sb: bcachefs::bch_sb_handle) -> Self { + Self { + uuid: sb.sb().uuid(), + encrypted: sb.sb().crypt().is_some(), + sb: sb, + devices: vec![], + } + } + + pub fn mount( + &self, + target: impl AsRef<std::path::Path>, + options: impl AsRef<str>, + ) -> anyhow::Result<()> { + use itertools::Itertools; + use std::ffi::c_void; + use std::os::raw::c_char; + use std::os::unix::ffi::OsStrExt; + let src = self.devices.iter().map(|d| d.display()).join(":"); + let (data, mountflags) = parse_mount_options(options); + let fstype = c_str!("bcachefs"); + + let src = std::ffi::CString::new(src)?; // bind the CString to keep it alive + let target = std::ffi::CString::new(target.as_ref().as_os_str().as_bytes())?; // ditto + let data = data.map(|data| std::ffi::CString::new(data)).transpose()?; // ditto + + let src = src.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; + let target = target.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; + let data = data.as_ref().map_or(std::ptr::null(), |data| { + data.as_c_str().to_bytes_with_nul().as_ptr() as *const c_void + }); + + let ret = unsafe { libc::mount(src, target, fstype, mountflags, data) }; + if ret == 0 { + Ok(()) + } else { + Err(crate::ErrnoError(errno::errno()).into()) + } + } +} + +use crate::bcachefs; +use std::collections::HashMap; +use uuid::Uuid; +pub fn probe_filesystems() -> anyhow::Result<HashMap<Uuid, FileSystem>> { + use std::os::unix::ffi::OsStrExt; + let mut udev = udev::Enumerator::new()?; + let mut fss = HashMap::new(); + udev.match_subsystem("block")?; + + { + // Stop libbcachefs from spamming the output + let _gag = gag::Gag::stdout().unwrap(); + for dev in udev.scan_devices()? { + if let Some(p) = dev.devnode() { + let path = + std::ffi::CString::new(p.as_os_str().as_bytes()).unwrap(); + let result = unsafe { + let mut opts = std::mem::MaybeUninit::zeroed(); + let mut sb = std::mem::MaybeUninit::zeroed(); + let ret = bcachefs::bch2_read_super( + path.as_ptr(), + opts.as_mut_ptr(), + sb.as_mut_ptr(), + ); + if ret == -libc::EACCES { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "no permission", + )) + } else if ret != 0 { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "failed to read super", + )) + } else { + Ok((opts.assume_init(), sb.assume_init())) + } + }; + match result { + Ok((_, sb)) => match fss.get_mut(&sb.sb().uuid()) { + None => { + let mut fs = FileSystem::new(sb); + fs.devices.push(p.to_owned()); + fss.insert(fs.uuid, fs); + } + Some(fs) => { + fs.devices.push(p.to_owned()); + } + }, + Err(e) if e.kind() + != std::io::ErrorKind::PermissionDenied => + { + () + } + e @ Err(_) => { + e?; + } + } + } + } + // Flush stdout so buffered output don't get printed after we remove the gag + unsafe { + libc::fflush(stdout); + } + } + Ok(fss) +} diff --git a/rust-src/mount/src/key.rs b/rust-src/mount/src/key.rs new file mode 100644 index 00000000..6769f52c --- /dev/null +++ b/rust-src/mount/src/key.rs @@ -0,0 +1,96 @@ +use log::info; + +fn check_for_key(key_name: &std::ffi::CStr) -> anyhow::Result<bool> { + use crate::keyutils::{self, keyctl_search}; + let key_name = key_name.to_bytes_with_nul().as_ptr() as *const _; + let key_type = c_str!("logon"); + + let key_id = + unsafe { keyctl_search(keyutils::KEY_SPEC_USER_KEYRING, key_type, key_name, 0) }; + if key_id > 0 { + info!("Key has became avaiable"); + Ok(true) + } else if errno::errno().0 != libc::ENOKEY { + Err(crate::ErrnoError(errno::errno()).into()) + } else { + Ok(false) + } +} + +fn wait_for_key(uuid: &uuid::Uuid) -> anyhow::Result<()> { + let key_name = std::ffi::CString::new(format!("bcachefs:{}", uuid)).unwrap(); + loop { + if check_for_key(&key_name)? { + break Ok(()); + } + + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} + +const BCH_KEY_MAGIC: &str = "bch**key"; +use crate::filesystem::FileSystem; +fn ask_for_key(fs: &FileSystem) -> anyhow::Result<()> { + use crate::bcachefs::{self, bch2_chacha_encrypt_key, bch_encrypted_key, bch_key}; + use anyhow::anyhow; + use byteorder::{LittleEndian, ReadBytesExt}; + use std::os::raw::c_char; + + let key_name = std::ffi::CString::new(format!("bcachefs:{}", fs.uuid())).unwrap(); + if check_for_key(&key_name)? { + return Ok(()); + } + + let bch_key_magic = BCH_KEY_MAGIC.as_bytes().read_u64::<LittleEndian>().unwrap(); + let crypt = fs.sb().sb().crypt().unwrap(); + let pass = rpassword::read_password_from_tty(Some("Enter passphrase: "))?; + let pass = std::ffi::CString::new(pass.trim_end())?; // bind to keep the CString alive + let mut output: bch_key = unsafe { + bcachefs::derive_passphrase( + crypt as *const _ as *mut _, + pass.as_c_str().to_bytes_with_nul().as_ptr() as *const _, + ) + }; + + let mut key = crypt.key().clone(); + let ret = unsafe { + bch2_chacha_encrypt_key( + &mut output as *mut _, + fs.sb().sb().nonce(), + &mut key as *mut _ as *mut _, + std::mem::size_of::<bch_encrypted_key>() as u64, + ) + }; + if ret != 0 { + Err(anyhow!("chache decryption failure")) + } else if key.magic != bch_key_magic { + Err(anyhow!("failed to verify the password")) + } else { + let key_type = c_str!("logon"); + let ret = unsafe { + crate::keyutils::add_key( + key_type, + key_name.as_c_str().to_bytes_with_nul() as *const _ + as *const c_char, + &output as *const _ as *const _, + std::mem::size_of::<bch_key>() as u64, + crate::keyutils::KEY_SPEC_USER_KEYRING, + ) + }; + if ret == -1 { + Err(anyhow!("failed to add key to keyring: {}", errno::errno())) + } else { + Ok(()) + } + } +} + +pub(crate) fn prepare_key(fs: &FileSystem, password: crate::KeyLocation) -> anyhow::Result<()> { + use crate::KeyLocation::*; + use anyhow::anyhow; + match password { + Fail => Err(anyhow!("no key available")), + Wait => Ok(wait_for_key(fs.uuid())?), + Ask => ask_for_key(fs), + } +} diff --git a/rust-src/mount/src/keyutils_wrapper.h b/rust-src/mount/src/keyutils_wrapper.h new file mode 100644 index 00000000..857cee2e --- /dev/null +++ b/rust-src/mount/src/keyutils_wrapper.h @@ -0,0 +1 @@ +#include <keyutils.h> diff --git a/rust-src/mount/src/lib.rs b/rust-src/mount/src/lib.rs new file mode 100644 index 00000000..751eab38 --- /dev/null +++ b/rust-src/mount/src/lib.rs @@ -0,0 +1,190 @@ +use structopt::StructOpt; +use anyhow::anyhow; + +#[macro_export] +macro_rules! c_str { + ($lit:expr) => { + unsafe { std::ffi::CStr::from_ptr(concat!($lit, "\0").as_ptr() as *const std::os::raw::c_char) + .to_bytes_with_nul() + .as_ptr() as *const std::os::raw::c_char } + }; +} + +#[derive(Debug)] +struct ErrnoError(errno::Errno); +impl std::fmt::Display for ErrnoError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + self.0.fmt(f) + } +} +impl std::error::Error for ErrnoError {} + +#[derive(Debug)] +pub(crate) enum KeyLocation { + Fail, + Wait, + Ask, +} + +impl std::str::FromStr for KeyLocation { + type Err = anyhow::Error; + fn from_str(s: &str) -> anyhow::Result<Self> { + use anyhow::anyhow; + match s { + "fail" => Ok(Self::Fail), + "wait" => Ok(Self::Wait), + "ask" => Ok(Self::Ask), + _ => Err(anyhow!("invalid password option")) + } + } +} + +#[derive(StructOpt, Debug)] +/// Mount a bcachefs filesystem by its UUID. +struct Options { + /// Where the password would be loaded from. + /// + /// 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; + #[structopt(short, long, default_value = "fail")] + key_location: KeyLocation, + + /// External UUID of the bcachefs filesystem + uuid: uuid::Uuid, + + /// Where the filesystem should be mounted. If not set, then the filesystem + /// won't actually be mounted. But all steps preceeding mounting the + /// filesystem (e.g. asking for passphrase) will still be performed. + mountpoint: Option<std::path::PathBuf>, + + /// Mount options + #[structopt(short, default_value = "")] + options: String, +} + +mod filesystem; +mod key; +mod keyutils { + #![allow(non_upper_case_globals)] + #![allow(non_camel_case_types)] + #![allow(non_snake_case)] + #![allow(unused)] + + include!(concat!(env!("OUT_DIR"), "/keyutils.rs")); +} + +mod bcachefs { + #![allow(non_upper_case_globals)] + #![allow(non_camel_case_types)] + #![allow(non_snake_case)] + #![allow(unused)] + + include!(concat!(env!("OUT_DIR"), "/bcachefs.rs")); + + use bitfield::bitfield; + bitfield! { + pub struct bch_scrypt_flags(u64); + pub N, _: 15, 0; + pub R, _: 31, 16; + pub P, _: 47, 32; + } + bitfield! { + pub struct bch_crypt_flags(u64); + TYPE, _: 4, 0; + } + use memoffset::offset_of; + impl bch_sb_field_crypt { + pub fn scrypt_flags(&self) -> Option<bch_scrypt_flags> { + let t = bch_crypt_flags(self.flags); + if t.TYPE() != bch_kdf_types::BCH_KDF_SCRYPT as u64 { + None + } else { + Some(bch_scrypt_flags(self.kdf_flags)) + } + } + pub fn key(&self) -> &bch_encrypted_key { + &self.key + } + } + impl bch_sb { + pub fn crypt(&self) -> Option<&bch_sb_field_crypt> { + unsafe { + let ptr = bch2_sb_field_get( + self as *const _ as *mut _, + bch_sb_field_type::BCH_SB_FIELD_crypt, + ) as *const u8; + if ptr.is_null() { + None + } else { + let offset = offset_of!(bch_sb_field_crypt, field); + Some(&*((ptr.sub(offset)) as *const _)) + } + } + } + pub fn uuid(&self) -> uuid::Uuid { + uuid::Uuid::from_bytes(self.user_uuid.b) + } + + /// Get the nonce used to encrypt the superblock + pub fn nonce(&self) -> nonce { + use byteorder::{ReadBytesExt, LittleEndian}; + let mut internal_uuid = &self.uuid.b[..]; + let dword1 = internal_uuid.read_u32::<LittleEndian>().unwrap(); + let dword2 = internal_uuid.read_u32::<LittleEndian>().unwrap(); + nonce { d: [0, 0, dword1, dword2] } + } + } + impl bch_sb_handle { + pub fn sb(&self) -> &bch_sb { + unsafe { &*self.sb } + } + } +} + +fn main_inner() -> anyhow::Result<()> { + use itertools::Itertools; + use log::{info, trace}; + + env_logger::init(); + let opt = Options::from_args(); + trace!("{:?}", opt); + + let fss = filesystem::probe_filesystems()?; + info!("Found {} bcachefs filesystems: ", fss.len()); + for fs in fss.values() { + info!( + "{} ({}): {}", + fs.uuid(), + if fs.encrypted() { + "encrypted" + } else { + "unencrypted" + }, + fs.devices().iter().map(|d| d.display()).join(" ") + ); + } + + if let Some(fs) = fss.get(&opt.uuid) { + if fs.encrypted() { + info!("Making sure key is loaded for this filesystem"); + key::prepare_key(&fs, opt.key_location)?; + } + + if let Some(p) = opt.mountpoint { + fs.mount(&p, &opt.options) + } else { + Ok(()) + } + } else { + Err(anyhow!("Filesystem {} is not found", opt.uuid)) + } +} + +#[no_mangle] +pub extern "C" fn main() { + if let Err(e) = main_inner() { + println!("Error: {:?}", e); + } +} diff --git a/rust-src/mount/src/libbcachefs_wrapper.h b/rust-src/mount/src/libbcachefs_wrapper.h new file mode 100644 index 00000000..9d9754c1 --- /dev/null +++ b/rust-src/mount/src/libbcachefs_wrapper.h @@ -0,0 +1,4 @@ +#include "../libbcachefs/super-io.h" +#include "../libbcachefs/checksum.h" +#include "../libbcachefs/bcachefs_format.h" +#include "../crypto.h" |