feat: rewrite key.rs

- Introduce `KeyHandle` and `Passphrase` types

- Refactor the functions into associated functions

- Add `zeroizing` crate to handle passphrase memory safely

Signed-off-by: Thomas Mühlbacher <tmuehlbacher@posteo.net>
This commit is contained in:
Thomas Mühlbacher 2024-05-30 21:42:38 +02:00
parent 25bce91b4b
commit 22495e0d31
4 changed files with 196 additions and 159 deletions

21
Cargo.lock generated
View File

@ -90,6 +90,7 @@ dependencies = [
"strum_macros", "strum_macros",
"udev", "udev",
"uuid", "uuid",
"zeroize",
] ]
[[package]] [[package]]
@ -773,3 +774,23 @@ name = "windows_x86_64_msvc"
version = "0.52.0" version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -25,3 +25,4 @@ bch_bindgen = { path = "bch_bindgen" }
byteorder = "1.3" byteorder = "1.3"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
strum_macros = "0.26" strum_macros = "0.26"
zeroize = { version = "1", features = ["std", "zeroize_derive"] }

View File

@ -1,15 +1,19 @@
use crate::key; use std::{
use crate::key::UnlockPolicy; collections::HashMap,
ffi::{c_char, c_void, CString},
io::{stdout, IsTerminal},
path::{Path, PathBuf},
{env, fs, str},
};
use anyhow::{ensure, Result};
use bch_bindgen::{bcachefs, bcachefs::bch_sb_handle, opt_set, path_to_cstr}; use bch_bindgen::{bcachefs, bcachefs::bch_sb_handle, opt_set, path_to_cstr};
use clap::Parser; use clap::Parser;
use log::{debug, error, info, LevelFilter}; use log::{debug, error, info, LevelFilter};
use std::collections::HashMap;
use std::ffi::{c_char, c_void, CString};
use std::io::{stdout, IsTerminal};
use std::path::{Path, PathBuf};
use std::{env, fs, str};
use uuid::Uuid; use uuid::Uuid;
use crate::key::{KeyHandle, Passphrase, UnlockPolicy};
fn mount_inner( fn mount_inner(
src: String, src: String,
target: impl AsRef<std::path::Path>, target: impl AsRef<std::path::Path>,
@ -304,11 +308,11 @@ fn devs_str_sbs_from_device(
} }
} }
fn cmd_mount_inner(opt: Cli) -> anyhow::Result<()> { fn cmd_mount_inner(opt: Cli) -> Result<()> {
// Grab the udev information once // Grab the udev information once
let udev_info = udev_bcachefs_info()?; let udev_info = udev_bcachefs_info()?;
let (devices, block_devices_to_mount) = if opt.dev.starts_with("UUID=") { let (devices, sbs) = if opt.dev.starts_with("UUID=") {
let uuid = opt.dev.replacen("UUID=", "", 1); let uuid = opt.dev.replacen("UUID=", "", 1);
devs_str_sbs_from_uuid(&udev_info, uuid)? devs_str_sbs_from_uuid(&udev_info, uuid)?
} else if opt.dev.starts_with("OLD_BLKID_UUID=") { } else if opt.dev.starts_with("OLD_BLKID_UUID=") {
@ -333,44 +337,26 @@ fn cmd_mount_inner(opt: Cli) -> anyhow::Result<()> {
} }
}; };
if block_devices_to_mount.is_empty() { ensure!(!sbs.is_empty(), "No device(s) to mount specified");
Err(anyhow::anyhow!("No device found from specified parameters"))?;
}
let key_name = CString::new(format!( let first_sb = sbs[0];
"bcachefs:{}", let uuid = first_sb.sb().uuid();
block_devices_to_mount[0].sb().uuid()
))
.unwrap();
// Check if the filesystem's master key is encrypted and we don't have a key if unsafe { bcachefs::bch2_sb_is_encrypted(first_sb.sb) } {
if unsafe { bcachefs::bch2_sb_is_encrypted(block_devices_to_mount[0].sb) } let _key_handle = KeyHandle::new_from_search(&uuid).or_else(|_| {
&& !key::check_for_key(&key_name)? opt.passphrase_file
{ .map(|path| {
// First by password_file, if available Passphrase::new_from_file(&first_sb, path)
let fallback_to_unlock_policy = if let Some(passphrase_file) = &opt.passphrase_file { .inspect_err(|e| {
match key::read_from_passphrase_file( error!(
&block_devices_to_mount[0], "Failed to read passphrase from file, falling back to prompt: {}",
passphrase_file.as_path(), e
) { )
Ok(()) => { })
// Decryption succeeded .and_then(|p| KeyHandle::new(&first_sb, &p))
false })
} .unwrap_or_else(|| opt.unlock_policy.apply(&first_sb))
Err(err) => { });
// Decryption failed, fall back to unlock_policy
error!("Failed to decrypt using passphrase_file: {}", err);
true
}
}
} else {
// No passphrase_file specified, fall back to unlock_policy
true
};
// If decryption by key_file was unsuccesful, prompt for passphrase (or follow key_policy)
if fallback_to_unlock_policy {
key::apply_key_unlocking_policy(&block_devices_to_mount[0], opt.unlock_policy)?;
};
} }
if let Some(mountpoint) = opt.mountpoint { if let Some(mountpoint) = opt.mountpoint {

View File

@ -1,14 +1,27 @@
use std::{ use std::{
fmt::Debug, ffi::{CStr, CString},
fs, fs,
io::{stdin, IsTerminal}, io::{stdin, IsTerminal},
mem,
path::Path,
thread,
time::Duration,
}; };
use anyhow::anyhow; use anyhow::{anyhow, ensure, Result};
use bch_bindgen::bcachefs::bch_sb_handle; use bch_bindgen::{
bcachefs::{self, bch_key, bch_sb_handle},
c::bch2_chacha_encrypt_key,
keyutils::{self, keyctl_search},
};
use byteorder::{LittleEndian, ReadBytesExt};
use log::info; use log::info;
use uuid::Uuid;
use zeroize::{ZeroizeOnDrop, Zeroizing};
use crate::c_str; use crate::{c_str, ErrnoError};
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 {
@ -17,135 +30,151 @@ pub enum UnlockPolicy {
Ask, Ask,
} }
impl UnlockPolicy {
pub fn apply(&self, sb: &bch_sb_handle) -> Result<KeyHandle> {
let uuid = sb.sb().uuid();
info!(
"Attempting to unlock filesystem {} with unlock policy '{}'",
uuid, self
);
match self {
Self::Fail => Err(anyhow!("no passphrase available")),
Self::Wait => Ok(KeyHandle::wait_for_unlock(&uuid)?),
Self::Ask => Passphrase::new_from_prompt().and_then(|p| KeyHandle::new(sb, &p)),
}
}
}
impl Default for UnlockPolicy { impl Default for UnlockPolicy {
fn default() -> Self { fn default() -> Self {
Self::Ask Self::Ask
} }
} }
pub fn check_for_key(key_name: &std::ffi::CStr) -> anyhow::Result<bool> { /// A handle to an existing bcachefs key in the kernel keyring
use bch_bindgen::keyutils::{self, keyctl_search}; pub struct KeyHandle {
let key_name = key_name.to_bytes_with_nul().as_ptr() as *const _; // FIXME: Either these come in useful for something or we remove them
let key_type = c_str!("user"); _uuid: Uuid,
_id: i64,
let key_id = unsafe { keyctl_search(keyutils::KEY_SPEC_USER_KEYRING, key_type, key_name, 0) };
if key_id > 0 {
info!("Key has become available");
Ok(true)
} else {
match errno::errno().0 {
libc::ENOKEY | libc::EKEYREVOKED => Ok(false),
_ => Err(crate::ErrnoError(errno::errno()).into()),
}
}
} }
fn wait_for_unlock(uuid: &uuid::Uuid) -> anyhow::Result<()> { impl KeyHandle {
let key_name = std::ffi::CString::new(format!("bcachefs:{}", uuid)).unwrap(); pub fn format_key_name(uuid: &Uuid) -> CString {
loop { CString::new(format!("bcachefs:{}", uuid)).unwrap()
if check_for_key(&key_name)? {
break Ok(());
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
// blocks indefinitely if no input is available on stdin
fn ask_for_passphrase(sb: &bch_sb_handle) -> anyhow::Result<()> {
let passphrase = if stdin().is_terminal() {
rpassword::prompt_password("Enter passphrase: ")?
} else {
info!("Trying to read passphrase from stdin...");
let mut line = String::new();
stdin().read_line(&mut line)?;
line
};
unlock_master_key(sb, &passphrase)
}
const BCH_KEY_MAGIC: &str = "bch**key";
fn unlock_master_key(sb: &bch_sb_handle, passphrase: &str) -> anyhow::Result<()> {
use bch_bindgen::bcachefs::{self, bch2_chacha_encrypt_key, bch_encrypted_key, bch_key};
use byteorder::{LittleEndian, ReadBytesExt};
use std::os::raw::c_char;
let key_name = std::ffi::CString::new(format!("bcachefs:{}", sb.sb().uuid())).unwrap();
if check_for_key(&key_name)? {
return Ok(());
} }
let bch_key_magic = BCH_KEY_MAGIC.as_bytes().read_u64::<LittleEndian>().unwrap(); pub fn new(sb: &bch_sb_handle, passphrase: &Passphrase) -> Result<Self> {
let crypt = sb.sb().crypt().unwrap(); let bch_key_magic = BCH_KEY_MAGIC.as_bytes().read_u64::<LittleEndian>().unwrap();
let passphrase = std::ffi::CString::new(passphrase.trim_end())?; // bind to keep the CString alive
let mut output: bch_key = unsafe { let crypt = sb.sb().crypt().unwrap();
bcachefs::derive_passphrase( let crypt_ptr = crypt as *const _ as *mut _;
crypt as *const _ as *mut _,
passphrase.as_c_str().to_bytes_with_nul().as_ptr() as *const _, let mut output: bch_key =
) unsafe { bcachefs::derive_passphrase(crypt_ptr, passphrase.get().as_ptr()) };
};
let mut key = *crypt.key();
let mut key = *crypt.key();
let ret = unsafe {
bch2_chacha_encrypt_key(
&mut output as *mut _,
sb.sb().nonce(),
&mut key as *mut _ as *mut _,
std::mem::size_of::<bch_encrypted_key>(),
)
};
if ret != 0 {
Err(anyhow!("chacha decryption failure"))
} else if key.magic != bch_key_magic {
Err(anyhow!("failed to verify the password"))
} else {
let key_type = c_str!("user");
let ret = unsafe { let ret = unsafe {
bch_bindgen::keyutils::add_key( bch2_chacha_encrypt_key(
key_type, &mut output as *mut _,
key_name.as_c_str().to_bytes_with_nul() as *const _ as *const c_char, sb.sb().nonce(),
&output as *const _ as *const _, &mut key as *mut _ as *mut _,
std::mem::size_of::<bch_key>(), mem::size_of_val(&key),
bch_bindgen::keyutils::KEY_SPEC_USER_KEYRING,
) )
}; };
if ret == -1 {
Err(anyhow!("failed to add key to keyring: {}", errno::errno())) 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 key_id = unsafe {
keyutils::add_key(
key_type,
key_name,
&output as *const _ as *const _,
mem::size_of_val(&output),
keyutils::KEY_SPEC_USER_KEYRING,
)
};
if key_id > 0 {
info!("Found key in keyring");
Ok(KeyHandle {
_uuid: sb.sb().uuid(),
_id: key_id as i64,
})
} else { } else {
Ok(()) Err(anyhow!("failed to add key to keyring: {}", errno::errno()))
}
}
pub fn new_from_search(uuid: &Uuid) -> Result<Self> {
let key_name = Self::format_key_name(uuid);
let key_name = CStr::as_ptr(&key_name);
let key_type = c_str!("user");
let key_id =
unsafe { keyctl_search(keyutils::KEY_SPEC_USER_KEYRING, key_type, key_name, 0) };
if key_id > 0 {
info!("Found key in keyring");
Ok(Self {
_uuid: *uuid,
_id: key_id,
})
} else {
Err(ErrnoError(errno::errno()).into())
}
}
fn wait_for_unlock(uuid: &Uuid) -> Result<Self> {
loop {
match Self::new_from_search(uuid) {
Err(_) => thread::sleep(Duration::from_secs(1)),
r => break r,
}
} }
} }
} }
pub fn read_from_passphrase_file( #[derive(ZeroizeOnDrop)]
block_device: &bch_sb_handle, pub struct Passphrase(CString);
passphrase_file: &std::path::Path,
) -> anyhow::Result<()> {
// Attempts to unlock the master key by password_file
// Return true if unlock was successful, false otherwise
info!(
"Attempting to unlock master key for filesystem {}, using password from file {}",
block_device.sb().uuid(),
passphrase_file.display()
);
// Read the contents of the password_file into a string
let passphrase = fs::read_to_string(passphrase_file)?;
// Call decrypt_master_key with the read string
unlock_master_key(block_device, &passphrase)
}
pub fn apply_key_unlocking_policy( impl Passphrase {
block_device: &bch_sb_handle, fn get(&self) -> &CStr {
unlock_policy: UnlockPolicy, &self.0
) -> anyhow::Result<()> { }
info!(
"Attempting to unlock master key for filesystem {}, using unlock policy {}", // blocks indefinitely if no input is available on stdin
block_device.sb().uuid(), fn new_from_prompt() -> Result<Self> {
unlock_policy let passphrase = if stdin().is_terminal() {
); Zeroizing::new(rpassword::prompt_password("Enter passphrase: ")?)
match unlock_policy { } else {
UnlockPolicy::Fail => Err(anyhow!("no passphrase available")), info!("Trying to read passphrase from stdin...");
UnlockPolicy::Wait => Ok(wait_for_unlock(&block_device.sb().uuid())?), let mut line = Zeroizing::new(String::new());
UnlockPolicy::Ask => ask_for_passphrase(block_device), stdin().read_line(&mut line)?;
line
};
Ok(Self(CString::new(passphrase.as_str())?))
}
pub fn new_from_file(sb: &bch_sb_handle, 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(),
passphrase_file.display()
);
let passphrase = Zeroizing::new(fs::read_to_string(passphrase_file)?);
Ok(Self(CString::new(passphrase.as_str())?))
} }
} }