From 8180d36b59a0501d1e4ee027aabbae74c68d69e3 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Tue, 16 Jul 2024 14:21:11 -0400 Subject: [PATCH] Prototype of online encrypt for Stratis pools --- Cargo.lock | 6 +- Cargo.toml | 4 + src/engine/strat_engine/crypt/handle/v1.rs | 28 +-- src/engine/strat_engine/crypt/handle/v2.rs | 192 +++++++++++++++--- src/engine/strat_engine/crypt/shared.rs | 76 ++++++- .../strat_engine/thinpool/filesystem.rs | 8 + src/engine/strat_engine/thinpool/thinpool.rs | 132 ++++++------ 7 files changed, 322 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cec174be3a..e0fa574737 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,8 +785,7 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libcryptsetup-rs" version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a61d3782d841dca88244f582cfd95d96da9d175fb06616d50a480058647e39" +source = "git+https://github.com/jbaublitz/libcryptsetup-rs?branch=reencrypt-fixes#7b1e96c9f95d85ee4fc693d3ab58b7e8e2d5a5c2" dependencies = [ "bitflags 2.4.0", "either", @@ -803,8 +802,7 @@ dependencies = [ [[package]] name = "libcryptsetup-rs-sys" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c78b397341cb9aa5ddc8d11118754ed0eab4aeb9cee96ee7cbe83a7d2867b8d2" +source = "git+https://github.com/jbaublitz/libcryptsetup-rs?branch=reencrypt-fixes#7b1e96c9f95d85ee4fc693d3ab58b7e8e2d5a5c2" dependencies = [ "bindgen", "cc", diff --git a/Cargo.toml b/Cargo.toml index 973e494e3d..d7e3aa20cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,10 +145,14 @@ optional = true version = "0.9.3" features = ["mutex"] optional = true +git = "https://github.com/jbaublitz/libcryptsetup-rs" +branch = "reencrypt-fixes" [dependencies.libcryptsetup-rs-sys] version = "0.4.0" optional = true +git = "https://github.com/jbaublitz/libcryptsetup-rs" +branch = "reencrypt-fixes" [dependencies.libmount] version = "0.1.9" diff --git a/src/engine/strat_engine/crypt/handle/v1.rs b/src/engine/strat_engine/crypt/handle/v1.rs index b31a716223..a667d1d0ff 100644 --- a/src/engine/strat_engine/crypt/handle/v1.rs +++ b/src/engine/strat_engine/crypt/handle/v1.rs @@ -31,7 +31,7 @@ use crate::{ engine::MAX_STRATIS_PASS_SIZE, strat_engine::{ backstore::get_devno_from_path, - cmd::{clevis_decrypt, clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind}, + cmd::{clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind}, crypt::{ consts::{ CLEVIS_LUKS_TOKEN_ID, DEFAULT_CRYPT_KEYSLOTS_SIZE, DEFAULT_CRYPT_METADATA_SIZE, @@ -42,8 +42,8 @@ use crate::{ }, shared::{ acquire_crypt_device, activate, add_keyring_keyslot, check_luks2_token, - clevis_info_from_metadata, device_from_physical_path, ensure_inactive, - ensure_wiped, get_keyslot_number, interpret_clevis_config, + clevis_decrypt, clevis_info_from_metadata, device_from_physical_path, + ensure_inactive, ensure_wiped, get_keyslot_number, interpret_clevis_config, key_desc_from_metadata, luks2_token_type_is_valid, read_key, wipe_fallback, }, }, @@ -956,7 +956,7 @@ impl CryptHandle { /// Add a keyring binding to the underlying LUKS2 volume. pub fn bind_keyring(&mut self, key_desc: &KeyDescription) -> StratisResult<()> { let mut device = self.acquire_crypt_device()?; - let key = Self::clevis_decrypt(&mut device)?.ok_or_else(|| { + let key = clevis_decrypt(&mut device)?.ok_or_else(|| { StratisError::Msg( "The Clevis token appears to have been wiped outside of \ Stratis; cannot add a keyring key binding without an existing \ @@ -1030,24 +1030,6 @@ impl CryptHandle { replace_pool_name(&mut device, pool_name) } - /// Decrypt a Clevis passphrase and return it securely. - fn clevis_decrypt(device: &mut CryptDevice) -> StratisResult> { - let mut token = match device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok() { - Some(t) => t, - None => return Ok(None), - }; - let jwe = token - .as_object_mut() - .and_then(|map| map.remove("jwe")) - .ok_or_else(|| { - StratisError::Msg(format!( - "Token slot {CLEVIS_LUKS_TOKEN_ID} is occupied but does not appear to be a Clevis \ - token; aborting" - )) - })?; - clevis_decrypt(&jwe).map(Some) - } - /// Deactivate the device referenced by the current device handle. pub fn deactivate(&self) -> StratisResult<()> { ensure_inactive(&mut self.acquire_crypt_device()?, self.activation_name()) @@ -1097,7 +1079,7 @@ impl CryptHandle { StratisError::Msg("Failed to find key with key description".to_string()) })? } else if self.encryption_info().clevis_info().is_some() { - Self::clevis_decrypt(&mut crypt)?.expect("Already checked token exists") + clevis_decrypt(&mut crypt)?.expect("Already checked token exists") } else { unreachable!("Must be encrypted") }; diff --git a/src/engine/strat_engine/crypt/handle/v2.rs b/src/engine/strat_engine/crypt/handle/v2.rs index e3b5a885ac..4a91510727 100644 --- a/src/engine/strat_engine/crypt/handle/v2.rs +++ b/src/engine/strat_engine/crypt/handle/v2.rs @@ -4,7 +4,8 @@ use std::{ fmt::Debug, - fs::File, + fs::{File, OpenOptions}, + io::Write, iter::once, path::{Path, PathBuf}, }; @@ -13,14 +14,19 @@ use either::Either; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde_json::Value; -use devicemapper::{Device, DmName, DmNameBuf, Sectors}; +use devicemapper::{Bytes, Device, DmName, DmNameBuf, Sectors, IEC}; +use libblkid_rs::BlkidProbe; use libcryptsetup_rs::{ c_uint, consts::{ - flags::{CryptActivate, CryptVolumeKey}, - vals::{EncryptionFormat, KeyslotsSize, MetadataSize}, + flags::{CryptActivate, CryptReencrypt, CryptVolumeKey}, + vals::{ + CryptReencryptDirectionInfo, CryptReencryptModeInfo, EncryptionFormat, KeyslotsSize, + MetadataSize, + }, }, - CryptDevice, CryptInit, CryptParamsLuks2, CryptParamsLuks2Ref, SafeMemHandle, TokenInput, + CryptDevice, CryptInit, CryptParamsLuks2, CryptParamsLuks2Ref, CryptParamsReencrypt, + SafeMemHandle, TokenInput, }; #[cfg(test)] @@ -29,23 +35,24 @@ use crate::{ engine::{ engine::MAX_STRATIS_PASS_SIZE, strat_engine::{ - backstore::get_devno_from_path, - cmd::{clevis_decrypt, clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind}, + backstore::{backstore::v2, get_devno_from_path}, + cmd::{clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind}, crypt::{ consts::{ CLEVIS_LUKS_TOKEN_ID, DEFAULT_CRYPT_KEYSLOTS_SIZE, DEFAULT_CRYPT_METADATA_SIZE, LUKS2_TOKEN_ID, STRATIS_MEK_SIZE, }, shared::{ - acquire_crypt_device, activate, add_keyring_keyslot, clevis_info_from_metadata, - device_from_physical_path, ensure_wiped, get_keyslot_number, - interpret_clevis_config, key_desc_from_metadata, luks2_token_type_is_valid, - wipe_fallback, + acquire_crypt_device, activate, add_keyring_keyslot, clevis_decrypt, + clevis_info_from_metadata, device_from_physical_path, ensure_wiped, + get_keyslot_number, get_passphrase, interpret_clevis_config, + key_desc_from_metadata, luks2_token_type_is_valid, wipe_fallback, }, }, device::blkdev_size, dm::DEVICEMAPPER_PATH, names::format_crypt_backstore_name, + thinpool::{StratFilesystem, ThinPool}, }, types::{ DevicePath, EncryptionInfo, KeyDescription, PoolUuid, SizedKeyMemory, UnlockMethod, @@ -404,10 +411,10 @@ impl CryptHandle { Ok(()) } - fn initialize_with_err( + /// Format the device and initialize the unlock methods. + fn initialize_unlock_methods( device: &mut CryptDevice, physical_path: &Path, - pool_uuid: PoolUuid, encryption_info: &EncryptionInfo, luks2_params: Option<&CryptParamsLuks2>, ) -> StratisResult<()> { @@ -440,6 +447,20 @@ impl CryptHandle { } }; + Ok(()) + } + + /// Format the device and initialize the unlock methods, activating the device once it is + /// successfully set up. + fn initialize_with_err( + device: &mut CryptDevice, + physical_path: &Path, + pool_uuid: PoolUuid, + encryption_info: &EncryptionInfo, + luks2_params: Option<&CryptParamsLuks2>, + ) -> StratisResult<()> { + Self::initialize_unlock_methods(device, physical_path, encryption_info, luks2_params)?; + let activation_name = format_crypt_backstore_name(&pool_uuid); activate( device, @@ -639,7 +660,7 @@ impl CryptHandle { /// Add a keyring binding to the underlying LUKS2 volume. pub fn bind_keyring(&mut self, key_desc: &KeyDescription) -> StratisResult<()> { let mut device = self.acquire_crypt_device()?; - let key = Self::clevis_decrypt(&mut device)?.ok_or_else(|| { + let key = clevis_decrypt(&mut device)?.ok_or_else(|| { StratisError::Msg( "The Clevis token appears to have been wiped outside of \ Stratis; cannot add a keyring key binding without an existing \ @@ -707,22 +728,135 @@ impl CryptHandle { Ok(()) } - /// Decrypt a Clevis passphrase and return it securely. - fn clevis_decrypt(device: &mut CryptDevice) -> StratisResult> { - let mut token = match device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok() { - Some(t) => t, - None => return Ok(None), + /// Encrypt an unencrypted pool. + #[allow(dead_code)] + pub fn encrypt( + thinpool: &mut ThinPool, + pool_uuid: PoolUuid, + unencrypted_path: &Path, + encryption_info: &EncryptionInfo, + fs: &[StratFilesystem], + ) -> StratisResult { + let tmp_header = format!("/tmp/temp-header-{pool_uuid}"); + { + let mut file = OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&tmp_header)?; + file.write_all(&[0; 4096])?; + } + + let mut device = CryptInit::init(Path::new(&tmp_header))?; + let data_offset = Bytes::from(16 * IEC::Mi).sectors(); + device.set_data_offset(*data_offset)?; + + let sectors = fs + .iter() + .map(|fs| fs.block_size()) + .collect::>>()?; + let min_sector = sectors.iter().min(); + let sector_size = match min_sector { + Some(min) => convert_int!(*min, u64, u32)?, + None => { + let mut probe = BlkidProbe::new_from_filename(unencrypted_path)?; + let top = probe.get_topology()?; + convert_int!(top.get_logical_sector_size(), u64, u32)? + } }; - let jwe = token - .as_object_mut() - .and_then(|map| map.remove("jwe")) - .ok_or_else(|| { - StratisError::Msg(format!( - "Token slot {CLEVIS_LUKS_TOKEN_ID} is occupied but does not appear to be a Clevis \ - token; aborting" - )) - })?; - clevis_decrypt(&jwe).map(Some) + let params = CryptParamsLuks2 { + data_alignment: 0, + data_device: None, + integrity: None, + integrity_params: None, + pbkdf: None, + label: None, + sector_size, + subsystem: None, + }; + + Self::initialize_unlock_methods( + &mut device, + unencrypted_path, + encryption_info, + Some(¶ms), + )?; + let (keyslot, key) = get_passphrase(&mut device, encryption_info)?; + device.reencrypt_handle().reencrypt_init_by_passphrase( + None, + key.as_ref(), + None, + keyslot, + ("aes", "xts-plain"), + CryptParamsReencrypt { + mode: CryptReencryptModeInfo::Encrypt, + direction: CryptReencryptDirectionInfo::Forward, + resilience: "checksum".to_string(), + hash: "sha256".to_string(), + data_shift: 0, + max_hotzone_size: 0, + device_size: 0, + luks2: CryptParamsLuks2 { + data_alignment: 0, + data_device: None, + integrity: None, + integrity_params: None, + pbkdf: None, + label: None, + sector_size, + subsystem: None, + }, + flags: CryptReencrypt::INITIALIZE_ONLY, + }, + )?; + + let mut device = CryptInit::init(unencrypted_path)?; + device + .backup_handle() + .header_restore(Some(EncryptionFormat::Luks2), Path::new(&tmp_header))?; + + let activation_name = &format_crypt_backstore_name(&pool_uuid).to_string(); + device.activate_handle().activate_by_passphrase( + Some(activation_name), + None, + key.as_ref(), + CryptActivate::SHARED, + )?; + + let devno = get_devno_from_path(Path::new(&format!("/dev/mapper/{activation_name}")))?; + thinpool.set_device(devno)?; + + device.reencrypt_handle().reencrypt_init_by_passphrase( + Some(activation_name), + key.as_ref(), + None, + keyslot, + ("aes", "xts-plain"), + CryptParamsReencrypt { + mode: CryptReencryptModeInfo::Encrypt, + direction: CryptReencryptDirectionInfo::Forward, + resilience: "checksum".to_string(), + hash: "sha256".to_string(), + data_shift: 0, + max_hotzone_size: 0, + device_size: 0, + luks2: CryptParamsLuks2 { + data_alignment: 0, + data_device: None, + integrity: None, + integrity_params: None, + pbkdf: None, + label: None, + sector_size, + subsystem: None, + }, + flags: CryptReencrypt::RESUME_ONLY, + }, + )?; + device.reencrypt_handle().reencrypt2::<()>(None, None)?; + + CryptHandle::setup(unencrypted_path, pool_uuid, UnlockMethod::Any, None) + .map(|h| h.expect("should have crypt device after online encrypt")) } /// Deactivate the device referenced by the current device handle. diff --git a/src/engine/strat_engine/crypt/shared.rs b/src/engine/strat_engine/crypt/shared.rs index 31dd2d5126..5655247deb 100644 --- a/src/engine/strat_engine/crypt/shared.rs +++ b/src/engine/strat_engine/crypt/shared.rs @@ -31,7 +31,7 @@ use libcryptsetup_rs::{ use crate::{ engine::{ strat_engine::{ - cmd::clevis_decrypt, + cmd, crypt::consts::{ CLEVIS_LUKS_TOKEN_ID, CLEVIS_RECURSION_LIMIT, CLEVIS_TANG_TRUST_URL, CLEVIS_TOKEN_NAME, DEFAULT_CRYPT_KEYSLOTS_SIZE, DEFAULT_CRYPT_METADATA_SIZE, @@ -42,6 +42,7 @@ use crate::{ keys, }, types::{KeyDescription, SizedKeyMemory, UnlockMethod}, + EncryptionInfo, }, stratis::{StratisError, StratisResult}, }; @@ -800,7 +801,7 @@ fn open_safe(device: &mut CryptDevice, token: libc::c_int) -> StratisResult StratisResult<()> { )?; Ok(()) } + +/// Decrypt a Clevis passphrase and return it securely. +pub fn clevis_decrypt(device: &mut CryptDevice) -> StratisResult> { + let mut token = match device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok() { + Some(t) => t, + None => return Ok(None), + }; + let jwe = token + .as_object_mut() + .and_then(|map| map.remove("jwe")) + .ok_or_else(|| { + StratisError::Msg(format!( + "Token slot {CLEVIS_LUKS_TOKEN_ID} is occupied but does not appear to be a Clevis \ + token; aborting" + )) + })?; + cmd::clevis_decrypt(&jwe).map(Some) +} + +/// Get one of the passphrases for the encrypted device. +pub fn get_passphrase( + device: &mut CryptDevice, + encryption_info: &EncryptionInfo, +) -> StratisResult<(c_uint, SizedKeyMemory)> { + match encryption_info { + EncryptionInfo::KeyDesc(kd) => Ok(( + get_keyslot_number(device, LUKS2_TOKEN_ID)? + .expect("encryption info specified that there is a keyring binding"), + read_key(kd)?.ok_or_else(|| { + StratisError::Msg("Key description {kd} was not found in the keyring".to_string()) + })?, + )), + EncryptionInfo::ClevisInfo(_) => Ok(( + get_keyslot_number(device, CLEVIS_LUKS_TOKEN_ID)? + .expect("encryption info specified that there is a Clevis binding"), + clevis_decrypt(device)? + .expect("encryption info specified that there is a Clevis binding"), + )), + EncryptionInfo::Both(kd, _) => match read_key(kd) { + Ok(Some(key)) => Ok(( + get_keyslot_number(device, LUKS2_TOKEN_ID)? + .expect("encryption info specified that there is a keyring binding"), + key, + )), + Ok(None) => { + warn!( + "Key description {} not found in keyring; falling back on Clevis", + kd.as_application_str() + ); + Ok(( + get_keyslot_number(device, CLEVIS_LUKS_TOKEN_ID)? + .expect("encryption info specified that there is a Clevis binding"), + clevis_decrypt(device)? + .expect("encryption info specified that there is a Clevis binding"), + )) + } + Err(_) => { + warn!( + "Fetching key description {} failed; falling back on Clevis", + kd.as_application_str() + ); + Ok(( + get_keyslot_number(device, CLEVIS_LUKS_TOKEN_ID)? + .expect("encryption info specified that there is a Clevis binding"), + clevis_decrypt(device)? + .expect("encryption info specified that there is a Clevis binding"), + )) + } + }, + } +} diff --git a/src/engine/strat_engine/thinpool/filesystem.rs b/src/engine/strat_engine/thinpool/filesystem.rs index ba3748a507..6e0f6f6a73 100644 --- a/src/engine/strat_engine/thinpool/filesystem.rs +++ b/src/engine/strat_engine/thinpool/filesystem.rs @@ -18,6 +18,7 @@ use devicemapper::{ Bytes, DevId, DmDevice, DmName, DmOptions, DmUuid, Sectors, ThinDev, ThinDevId, ThinPoolDev, ThinStatus, }; +use libblkid_rs::BlkidProbe; use nix::{ mount::{mount, umount, MsFlags}, @@ -450,6 +451,13 @@ impl StratFilesystem { self.origin = None; changed } + + /// Get the sector size reported by libblkid for this filesystem. + pub fn block_size(&self) -> StratisResult { + let mut probe = BlkidProbe::new_from_filename(&self.devnode())?; + let top = probe.get_topology()?; + Ok(top.get_logical_sector_size()) + } } impl Filesystem for StratFilesystem { diff --git a/src/engine/strat_engine/thinpool/thinpool.rs b/src/engine/strat_engine/thinpool/thinpool.rs index 1dcfb5e389..e84da20e56 100644 --- a/src/engine/strat_engine/thinpool/thinpool.rs +++ b/src/engine/strat_engine/thinpool/thinpool.rs @@ -850,72 +850,6 @@ impl ThinPool { backstore: PhantomData, }) } - - /// Set the device on all DM devices - pub fn set_device(&mut self, backstore_device: Device) -> StratisResult { - if backstore_device == self.backstore_device { - return Ok(false); - } - - let xform_target_line = - |line: &TargetLine| -> TargetLine { - let new_params = match line.params { - LinearDevTargetParams::Linear(ref params) => LinearDevTargetParams::Linear( - LinearTargetParams::new(backstore_device, params.start_offset), - ), - LinearDevTargetParams::Flakey(ref params) => { - let feature_args = params.feature_args.iter().cloned().collect::>(); - LinearDevTargetParams::Flakey(FlakeyTargetParams::new( - backstore_device, - params.start_offset, - params.up_interval, - params.down_interval, - feature_args, - )) - } - }; - - TargetLine::new(line.start, line.length, new_params) - }; - - let meta_table = self - .thin_pool - .meta_dev() - .table() - .table - .clone() - .iter() - .map(&xform_target_line) - .collect::>(); - - let data_table = self - .thin_pool - .data_dev() - .table() - .table - .clone() - .iter() - .map(&xform_target_line) - .collect::>(); - - let mdv_table = self - .mdv - .device() - .table() - .table - .clone() - .iter() - .map(&xform_target_line) - .collect::>(); - - self.thin_pool.set_meta_table(get_dm(), meta_table)?; - self.thin_pool.set_data_table(get_dm(), data_table)?; - self.mdv.set_table(mdv_table)?; - - self.backstore_device = backstore_device; - - Ok(true) - } } impl ThinPool { @@ -1715,6 +1649,72 @@ where } Ok(changed) } + + /// Set the device on all DM devices + pub fn set_device(&mut self, backstore_device: Device) -> StratisResult { + if backstore_device == self.backstore_device { + return Ok(false); + } + + let xform_target_line = + |line: &TargetLine| -> TargetLine { + let new_params = match line.params { + LinearDevTargetParams::Linear(ref params) => LinearDevTargetParams::Linear( + LinearTargetParams::new(backstore_device, params.start_offset), + ), + LinearDevTargetParams::Flakey(ref params) => { + let feature_args = params.feature_args.iter().cloned().collect::>(); + LinearDevTargetParams::Flakey(FlakeyTargetParams::new( + backstore_device, + params.start_offset, + params.up_interval, + params.down_interval, + feature_args, + )) + } + }; + + TargetLine::new(line.start, line.length, new_params) + }; + + let meta_table = self + .thin_pool + .meta_dev() + .table() + .table + .clone() + .iter() + .map(&xform_target_line) + .collect::>(); + + let data_table = self + .thin_pool + .data_dev() + .table() + .table + .clone() + .iter() + .map(&xform_target_line) + .collect::>(); + + let mdv_table = self + .mdv + .device() + .table() + .table + .clone() + .iter() + .map(&xform_target_line) + .collect::>(); + + self.thin_pool.set_meta_table(get_dm(), meta_table)?; + self.thin_pool.set_data_table(get_dm(), data_table)?; + self.mdv.set_table(mdv_table)?; + + self.backstore_device = backstore_device; + + Ok(true) + } } impl<'a, B> Into for &'a ThinPool {