diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1549df0734..4dd06bb922 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -149,6 +149,7 @@ jobs: dnf install -y asciidoc clang + cryptsetup cryptsetup-devel dbus-daemon dbus-tools diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 80499f8bfb..d42501ce45 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -130,6 +130,7 @@ jobs: apt-get install -y asciidoc clang + cryptsetup curl git libblkid-dev diff --git a/.github/workflows/valgrind.yml b/.github/workflows/valgrind.yml index f58302108a..ac77074416 100644 --- a/.github/workflows/valgrind.yml +++ b/.github/workflows/valgrind.yml @@ -73,6 +73,7 @@ jobs: dnf install -y asciidoc clang + cryptsetup cryptsetup-devel curl device-mapper-persistent-data diff --git a/Cargo.lock b/Cargo.lock index 25f9834dca..0f774cb54f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,14 +410,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "winapi", + "windows-targets 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 71feaccc9d..5a43d8c215 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ version = "1.2.3" optional = true [dependencies.chrono] -version = "0.4.20" +version = "0.4.31" optional = true default-features = false features = ["clock", "std"] diff --git a/src/dbus/pool/pool_3_9/methods.rs b/src/dbus/pool/pool_3_9/methods.rs new file mode 100644 index 0000000000..9582eb06b2 --- /dev/null +++ b/src/dbus/pool/pool_3_9/methods.rs @@ -0,0 +1,249 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use std::sync::Arc; + +use futures::executor::block_on; +use serde_json::from_str; +use tokio::sync::RwLock; +use zbus::Connection; + +use crate::{ + dbus::{ + consts::OK_STRING, + manager::Manager, + types::DbusErrorEnum, + util::{ + engine_to_dbus_err_tuple, send_clevis_info_signal, send_encrypted_signal, + send_keyring_signal, send_last_reencrypted_signal, tuple_to_option, + }, + }, + engine::{ + CreateAction, DeleteAction, EncryptedDevice, Engine, InputEncryptionInfo, KeyDescription, + Lockable, PoolIdentifier, PoolUuid, + }, + stratis::StratisError, +}; + +pub async fn encrypt_pool_method( + engine: &Arc, + connection: &Arc, + manager: &Lockable>>, + pool_uuid: PoolUuid, + key_descs: Vec<((bool, u32), KeyDescription)>, + clevis_infos: Vec<((bool, u32), &str, &str)>, +) -> (bool, u16, String) { + let default_return = false; + + let key_descs_parsed = + match key_descs + .into_iter() + .try_fold(Vec::new(), |mut vec, (ts_opt, kd)| { + let token_slot = tuple_to_option(ts_opt); + vec.push((token_slot, kd)); + Ok(vec) + }) { + Ok(kds) => kds, + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return (default_return, rc, rs); + } + }; + + let clevis_infos_parsed = + match clevis_infos + .into_iter() + .try_fold(Vec::new(), |mut vec, (ts_opt, pin, json_str)| { + let token_slot = tuple_to_option(ts_opt); + let json = from_str(json_str)?; + vec.push((token_slot, (pin.to_owned(), json))); + Ok(vec) + }) { + Ok(cis) => cis, + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return (default_return, rc, rs); + } + }; + + let iei = match InputEncryptionInfo::new(key_descs_parsed, clevis_infos_parsed) { + Ok(Some(info)) => info, + Ok(None) => { + return ( + default_return, + DbusErrorEnum::ERROR as u16, + "No unlock methods provided".to_string(), + ); + } + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return (default_return, rc, rs); + } + }; + + let guard_res = engine + .get_mut_pool(PoolIdentifier::Uuid(pool_uuid)) + .await + .ok_or_else(|| StratisError::Msg(format!("No pool associated with uuid {pool_uuid}"))); + let cloned_engine = Arc::clone(engine); + match tokio::task::spawn_blocking(move || { + let mut guard = guard_res?; + + handle_action!(guard + .start_encrypt_pool(pool_uuid, &iei) + .and_then(|action| match action { + CreateAction::Identity => Ok(CreateAction::Identity), + CreateAction::Created((sector_size, key_info)) => { + let guard = guard.downgrade(); + guard + .do_encrypt_pool(pool_uuid, sector_size, key_info) + .map(|_| guard) + .and_then(|guard| { + let mut guard = block_on(cloned_engine.upgrade_pool(guard)); + let (name, _, _) = guard.as_mut_tuple(); + guard.finish_encrypt_pool(&name, pool_uuid) + }) + .map(|_| CreateAction::Created(EncryptedDevice(pool_uuid))) + } + })) + }) + .await + { + Ok(Ok(CreateAction::Created(_))) => { + match manager.read().await.pool_get_path(&pool_uuid) { + Some(p) => { + send_keyring_signal(connection, &p.as_ref(), true).await; + send_clevis_info_signal(connection, &p.as_ref(), true).await; + send_encrypted_signal(connection, &p.as_ref()).await; + } + None => { + warn!("No pool path associated with UUID {pool_uuid}; failed to send encryption related signals"); + } + } + (true, DbusErrorEnum::OK as u16, OK_STRING.to_string()) + } + Ok(Ok(CreateAction::Identity)) => (false, DbusErrorEnum::OK as u16, OK_STRING.to_string()), + Ok(Err(e)) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + (default_return, rc, rs) + } + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&StratisError::from(e)); + (default_return, rc, rs) + } + } +} + +pub async fn reencrypt_pool_method( + engine: &Arc, + connection: &Arc, + manager: &Lockable>>, + pool_uuid: PoolUuid, +) -> (bool, u16, String) { + let default_return = false; + + let guard_res = engine + .get_mut_pool(PoolIdentifier::Uuid(pool_uuid)) + .await + .ok_or_else(|| StratisError::Msg(format!("No pool associated with uuid {pool_uuid}"))); + let cloned_engine = Arc::clone(engine); + match tokio::task::spawn_blocking(move || { + let mut guard = guard_res?; + + let (name, _, _) = guard.as_mut_tuple(); + + let result = guard.start_reencrypt_pool(); + let result = result.and_then(|key_info| { + let guard = guard.downgrade(); + let result = guard.do_reencrypt_pool(pool_uuid, key_info); + result.map(|inner| (guard, inner)) + }); + let result = result.and_then(|(guard, _)| { + let mut guard = block_on(cloned_engine.upgrade_pool(guard)); + guard.finish_reencrypt_pool(&name, pool_uuid) + }); + handle_action!(result) + }) + .await + { + Ok(Ok(_)) => { + match manager.read().await.pool_get_path(&pool_uuid) { + Some(p) => { + send_last_reencrypted_signal(connection, &p.as_ref()).await; + } + None => { + warn!("No pool path associated with UUID {pool_uuid}; failed to send encryption related signals"); + } + } + (true, DbusErrorEnum::OK as u16, OK_STRING.to_string()) + } + Ok(Err(e)) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + (default_return, rc, rs) + } + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&StratisError::from(e)); + (default_return, rc, rs) + } + } +} + +pub async fn decrypt_pool_method( + engine: &Arc, + connection: &Arc, + manager: &Lockable>>, + pool_uuid: PoolUuid, +) -> (bool, u16, String) { + let default_return = false; + + let guard_res = engine + .get_mut_pool(PoolIdentifier::Uuid(pool_uuid)) + .await + .ok_or_else(|| StratisError::Msg(format!("No pool associated with uuid {pool_uuid}"))); + let cloned_engine = Arc::clone(engine); + match tokio::task::spawn_blocking(move || { + let mut guard = guard_res?; + + handle_action!(match guard.decrypt_pool_idem_check(pool_uuid) { + Ok(DeleteAction::Identity) => Ok(DeleteAction::Identity), + Ok(DeleteAction::Deleted(d)) => { + let guard = guard.downgrade(); + guard + .do_decrypt_pool(pool_uuid) + .and_then(|_| { + let mut guard = block_on(cloned_engine.upgrade_pool(guard)); + let (name, _, _) = guard.as_mut_tuple(); + guard.finish_decrypt_pool(pool_uuid, &name) + }) + .map(|_| DeleteAction::Deleted(d)) + } + Err(e) => Err(e), + }) + }) + .await + { + Ok(Ok(DeleteAction::Deleted(_))) => { + match manager.read().await.pool_get_path(&pool_uuid) { + Some(p) => { + send_keyring_signal(connection, &p.as_ref(), true).await; + send_clevis_info_signal(connection, &p.as_ref(), true).await; + send_encrypted_signal(connection, &p.as_ref()).await; + } + None => { + warn!("No pool path associated with UUID {pool_uuid}; failed to send encryption related signals"); + } + } + (true, DbusErrorEnum::OK as u16, OK_STRING.to_string()) + } + Ok(Ok(DeleteAction::Identity)) => (false, DbusErrorEnum::OK as u16, OK_STRING.to_string()), + Ok(Err(e)) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + (default_return, rc, rs) + } + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&StratisError::from(e)); + (default_return, rc, rs) + } + } +} diff --git a/src/dbus/pool/pool_3_9/mod.rs b/src/dbus/pool/pool_3_9/mod.rs index dbecea81f8..d159c5126e 100644 --- a/src/dbus/pool/pool_3_9/mod.rs +++ b/src/dbus/pool/pool_3_9/mod.rs @@ -47,6 +47,12 @@ use crate::{ stratis::StratisResult, }; +mod methods; +mod props; + +pub use methods::{decrypt_pool_method, encrypt_pool_method, reencrypt_pool_method}; +pub use props::last_reencrypted_timestamp_prop; + pub struct PoolR9 { connection: Arc, engine: Arc, @@ -327,6 +333,30 @@ impl PoolR9 { filesystem_metadata_method(&self.engine, self.uuid, fs_name, current).await } + async fn encrypt_pool( + &self, + key_descs: Vec<((bool, u32), KeyDescription)>, + clevis_infos: Vec<((bool, u32), &str, &str)>, + ) -> (bool, u16, String) { + encrypt_pool_method( + &self.engine, + &self.connection, + &self.manager, + self.uuid, + key_descs, + clevis_infos, + ) + .await + } + + async fn reencrypt_pool(&self) -> (bool, u16, String) { + reencrypt_pool_method(&self.engine, &self.connection, &self.manager, self.uuid).await + } + + async fn decrypt_pool(&self) -> (bool, u16, String) { + decrypt_pool_method(&self.engine, &self.connection, &self.manager, self.uuid).await + } + #[zbus(property(emits_changed_signal = "const"))] fn uuid(&self) -> String { self.uuid.simple().to_string() @@ -337,7 +367,7 @@ impl PoolR9 { pool_prop(&self.engine, self.uuid, name_prop).await } - #[zbus(property(emits_changed_signal = "const"))] + #[zbus(property(emits_changed_signal = "true"))] async fn encrypted(&self) -> Result { pool_prop(&self.engine, self.uuid, encrypted_prop).await } @@ -434,4 +464,9 @@ impl PoolR9 { async fn metadata_version(&self) -> Result { pool_prop(&self.engine, self.uuid, metadata_version_prop).await } + + #[zbus(property(emits_changed_signal = "true"))] + async fn last_reencrypted_timestamp(&self) -> Result<(bool, String), Error> { + pool_prop(&self.engine, self.uuid, last_reencrypted_timestamp_prop).await + } } diff --git a/src/dbus/pool/pool_3_9/props.rs b/src/dbus/pool/pool_3_9/props.rs new file mode 100644 index 0000000000..90092d2f4d --- /dev/null +++ b/src/dbus/pool/pool_3_9/props.rs @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use chrono::SecondsFormat; + +use crate::{ + dbus::util::option_to_tuple, + engine::{Pool, PoolUuid, SomeLockReadGuard}, +}; + +pub fn last_reencrypted_timestamp_prop( + guard: SomeLockReadGuard, +) -> (bool, String) { + option_to_tuple( + guard + .last_reencrypt() + .map(|t| t.to_rfc3339_opts(SecondsFormat::Secs, true)), + String::new(), + ) +} diff --git a/src/dbus/util.rs b/src/dbus/util.rs index efd3a178ab..f892f5e733 100644 --- a/src/dbus/util.rs +++ b/src/dbus/util.rs @@ -1978,3 +1978,25 @@ pub async fn send_user_info_signal(connection: &Arc, path: &ObjectPa "blockdev.r9" ); } + +pub async fn send_encrypted_signal(connection: &Arc, path: &ObjectPath<'_>) { + send_signal!( + connection, + PoolR9, + path, + encrypted_changed, + "encryption status", + "pool.r9" + ); +} + +pub async fn send_last_reencrypted_signal(connection: &Arc, path: &ObjectPath<'_>) { + send_signal!( + connection, + PoolR9, + path, + last_reencrypted_timestamp_changed, + "last reencryption timestamp", + "pool.r9" + ); +} diff --git a/src/engine/engine.rs b/src/engine/engine.rs index 3cb52c9061..47e33048ca 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -25,13 +25,14 @@ use crate::{ }, types::{ ActionAvailability, BlockDevTier, Clevis, CreateAction, DeleteAction, DevUuid, - EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, IntegritySpec, Key, - KeyDescription, LockedPoolsInfo, MappingCreateAction, MappingDeleteAction, Name, - OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, PoolIdentifier, PoolUuid, - PropChangeAction, RegenAction, RenameAction, ReportType, SetCreateAction, - SetDeleteAction, SetUnlockAction, StartAction, StopAction, StoppedPoolsInfo, - StratBlockDevDiff, StratFilesystemDiff, StratSigblockVersion, TokenUnlockMethod, - UdevEngineEvent, UnlockMethod, + EncryptedDevice, EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, + IntegritySpec, Key, KeyDescription, LockedPoolsInfo, MappingCreateAction, + MappingDeleteAction, Name, OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, + PoolIdentifier, PoolUuid, PropChangeAction, ReencryptedDevice, RegenAction, + RenameAction, ReportType, SetCreateAction, SetDeleteAction, SetUnlockAction, + SizedKeyMemory, StartAction, StopAction, StoppedPoolsInfo, StratBlockDevDiff, + StratFilesystemDiff, StratSigblockVersion, TokenUnlockMethod, UdevEngineEvent, + UnlockMethod, }, }, stratis::StratisResult, @@ -401,6 +402,60 @@ pub trait Pool: Debug + Send + Sync { limit: Option, ) -> StratisResult>>; + /// Setup pool encryption operation and check for idempotence. + fn start_encrypt_pool( + &mut self, + pool_uuid: PoolUuid, + encryption_info: &InputEncryptionInfo, + ) -> StratisResult>; + + /// Encrypt an unencrypted pool. + fn do_encrypt_pool( + &self, + pool_uuid: PoolUuid, + sector_size: u32, + key_info: (u32, SizedKeyMemory), + ) -> StratisResult<()>; + + /// Update internal data structures with the result of the encryption operation. + fn finish_encrypt_pool(&mut self, name: &Name, pool_uuid: PoolUuid) -> StratisResult<()>; + + /// Start reencryption of an encrypted pool. + /// + /// Sets up the reencryption process. + fn start_reencrypt_pool(&mut self) -> StratisResult>; + + /// Perform reencryption of an encrypted pool. + /// + /// Acquires a read lock during the duration of the reencryption. + fn do_reencrypt_pool( + &self, + pool_uuid: PoolUuid, + key_info: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()>; + + /// Finish reencryption of an encrypted pool. + fn finish_reencrypt_pool( + &mut self, + name: &Name, + pool_uuid: PoolUuid, + ) -> StratisResult; + + /// Check idempotence of a pool decrypt command. + /// + /// This method does not require interior mutability, but a write lock is required + /// to ensure that no other decryption operations have already started. + fn decrypt_pool_idem_check( + &mut self, + pool_uuid: PoolUuid, + ) -> StratisResult>; + + /// Decrypt an encrypted pool. + fn do_decrypt_pool(&self, pool_uuid: PoolUuid) -> StratisResult<()>; + + /// Finish pool decryption operation. + fn finish_decrypt_pool(&mut self, pool_uuid: PoolUuid, name: &Name) -> StratisResult<()>; + /// Return the metadata that would be written if metadata were written. fn current_metadata(&self, pool_name: &Name) -> StratisResult; @@ -433,6 +488,9 @@ pub trait Pool: Debug + Send + Sync { /// /// Returns true if the key was newly loaded and false if the key was already loaded. fn load_volume_key(&mut self, uuid: PoolUuid) -> StratisResult; + + /// Get the timestamp of the last online reencryption operation. + fn last_reencrypt(&self) -> Option>; } pub type HandleEvents

= ( @@ -486,6 +544,15 @@ pub trait Engine: Debug + Report + Send + Sync { token_slot: UnlockMethod, ) -> StratisResult>; + /// Upgrade the read lock on a given pool to a write lock. + /// + /// This method will prioritize the given lock over all other queued operations to preserve + /// atomic operations that need to switch between read and write locks. + async fn upgrade_pool( + &self, + lock: SomeLockReadGuard, + ) -> SomeLockWriteGuard; + /// Find the pool designated by name or UUID. async fn get_pool( &self, diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 40479570be..f8b00a3cf6 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -20,12 +20,13 @@ pub use self::{ }, types::{ ActionAvailability, BlockDevTier, ClevisInfo, CreateAction, DeleteAction, DevUuid, Diff, - EncryptionInfo, EngineAction, Features, FilesystemUuid, GrowAction, InputEncryptionInfo, - IntegritySpec, IntegrityTagSpec, KeyDescription, Lockable, LockedPoolInfo, LockedPoolsInfo, - MappingCreateAction, MappingDeleteAction, MaybeInconsistent, Name, OptionalTokenSlotInput, - PoolDiff, PoolEncryptionInfo, PoolIdentifier, PoolUuid, PropChangeAction, RenameAction, - ReportType, SetCreateAction, SetDeleteAction, SetUnlockAction, StartAction, StopAction, - StoppedPoolInfo, StoppedPoolsInfo, StratBlockDevDiff, StratFilesystemDiff, StratPoolDiff, + EncryptedDevice, EncryptionInfo, EngineAction, Features, FilesystemUuid, GrowAction, + InputEncryptionInfo, IntegritySpec, IntegrityTagSpec, KeyDescription, Lockable, + LockedPoolInfo, LockedPoolsInfo, MappingCreateAction, MappingDeleteAction, + MaybeInconsistent, Name, OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, + PoolIdentifier, PoolUuid, PropChangeAction, RenameAction, ReportType, SetCreateAction, + SetDeleteAction, SetUnlockAction, StartAction, StopAction, StoppedPoolInfo, + StoppedPoolsInfo, StratBlockDevDiff, StratFilesystemDiff, StratPoolDiff, StratSigblockVersion, StratisUuid, ThinPoolDiff, ToDisplay, TokenUnlockMethod, UdevEngineEvent, UnlockMethod, ValidatedIntegritySpec, DEFAULT_INTEGRITY_JOURNAL_SIZE, DEFAULT_INTEGRITY_TAG_SPEC, diff --git a/src/engine/sim_engine/engine.rs b/src/engine/sim_engine/engine.rs index 6534120564..0a97dff93d 100644 --- a/src/engine/sim_engine/engine.rs +++ b/src/engine/sim_engine/engine.rs @@ -18,18 +18,18 @@ use crate::{ engine::{ engine::{Engine, HandleEvents, KeyActions, Pool, Report}, shared::{create_pool_idempotent_or_err, validate_name, validate_paths}, - sim_engine::{keys::SimKeyActions, pool::SimPool}, + sim_engine::{keys::SimKeyActions, pool::SimPool, shared::convert_encryption_info}, structures::{ AllLockReadAvailableGuard, AllLockReadGuard, AllLockWriteAvailableGuard, AllLockWriteGuard, AllOrSomeLock, Lockable, SomeLockReadGuard, SomeLockWriteGuard, Table, }, types::{ - CreateAction, DeleteAction, DevUuid, EncryptionInfo, Features, FilesystemUuid, - InputEncryptionInfo, IntegritySpec, LockedPoolsInfo, Name, PoolDevice, PoolDiff, - PoolIdentifier, PoolUuid, RenameAction, ReportType, SetUnlockAction, StartAction, - StopAction, StoppedPoolInfo, StoppedPoolsInfo, StratFilesystemDiff, TokenUnlockMethod, - UdevEngineEvent, UnlockMechanism, UnlockMethod, ValidatedIntegritySpec, + CreateAction, DeleteAction, DevUuid, Features, FilesystemUuid, InputEncryptionInfo, + IntegritySpec, LockedPoolsInfo, Name, PoolDevice, PoolDiff, PoolIdentifier, PoolUuid, + RenameAction, ReportType, SetUnlockAction, StartAction, StopAction, StoppedPoolInfo, + StoppedPoolsInfo, StratFilesystemDiff, TokenUnlockMethod, UdevEngineEvent, + UnlockMethod, ValidatedIntegritySpec, }, StratSigblockVersion, }, @@ -140,30 +140,7 @@ impl Engine for SimEngine { let integrity_spec = ValidatedIntegritySpec::try_from(integrity_spec)?; - let converted_ei = encryption_info - .cloned() - .map(|ei| { - ei.into_iter().try_fold( - EncryptionInfo::new(), - |mut info, (token_slot, unlock_mechanism)| { - let ts = match token_slot { - Some(t) => t, - None => info.free_token_slot(), - }; - if let UnlockMechanism::KeyDesc(ref kd) = unlock_mechanism { - if !self.key_handler.contains_key(kd) { - return Err(StratisError::Msg(format!( - "Key {} was not found in the keyring", - kd.as_application_str() - ))); - } - } - info.add_info(ts, unlock_mechanism)?; - Ok(info) - }, - ) - }) - .transpose()?; + let converted_ei = convert_encryption_info(encryption_info, Some(&self.key_handler))?; let guard = self.pools.read(PoolIdentifier::Name(name.clone())).await; match guard.as_ref().map(|g| g.as_tuple()) { @@ -241,6 +218,13 @@ impl Engine for SimEngine { Ok(SetUnlockAction::empty()) } + async fn upgrade_pool( + &self, + lock: SomeLockReadGuard, + ) -> SomeLockWriteGuard { + self.pools.upgrade(lock).await.into_dyn() + } + async fn get_pool( &self, key: PoolIdentifier, diff --git a/src/engine/sim_engine/mod.rs b/src/engine/sim_engine/mod.rs index 93aa975bc1..4b4c973df3 100644 --- a/src/engine/sim_engine/mod.rs +++ b/src/engine/sim_engine/mod.rs @@ -9,3 +9,4 @@ mod engine; mod filesystem; mod keys; mod pool; +mod shared; diff --git a/src/engine/sim_engine/pool.rs b/src/engine/sim_engine/pool.rs index 69d7164fab..88633d0c16 100644 --- a/src/engine/sim_engine/pool.rs +++ b/src/engine/sim_engine/pool.rs @@ -8,11 +8,13 @@ use std::{ vec::Vec, }; +use chrono::{DateTime, Utc}; use either::Either; use itertools::Itertools; use serde_json::{Map, Value}; use devicemapper::{Bytes, Sectors, IEC}; +use libcryptsetup_rs::SafeMemHandle; use crate::{ engine::{ @@ -21,16 +23,18 @@ use crate::{ init_cache_idempotent_or_err, validate_filesystem_size, validate_filesystem_size_specs, validate_name, validate_paths, }, - sim_engine::{blockdev::SimDev, filesystem::SimFilesystem}, + sim_engine::{ + blockdev::SimDev, filesystem::SimFilesystem, shared::convert_encryption_info, + }, structures::Table, types::{ ActionAvailability, BlockDevTier, Clevis, CreateAction, DeleteAction, DevUuid, - EncryptionInfo, FilesystemUuid, GrowAction, Key, KeyDescription, Name, - OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, PoolUuid, RegenAction, - RenameAction, SetCreateAction, SetDeleteAction, StratSigblockVersion, UnlockMechanism, + EncryptedDevice, EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, Key, + KeyDescription, Name, OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, PoolUuid, + PropChangeAction, ReencryptedDevice, RegenAction, RenameAction, SetCreateAction, + SetDeleteAction, SizedKeyMemory, StratSigblockVersion, UnlockMechanism, ValidatedIntegritySpec, }, - PropChangeAction, }, stratis::{StratisError, StratisResult}, }; @@ -44,6 +48,7 @@ pub struct SimPool { enable_overprov: bool, encryption_info: Option, integrity_spec: ValidatedIntegritySpec, + last_reencrypt: Option>, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -72,6 +77,7 @@ impl SimPool { enable_overprov: true, encryption_info: enc_info.cloned(), integrity_spec, + last_reencrypt: None, }, ) } @@ -961,6 +967,78 @@ impl Pool for SimPool { } } + fn start_encrypt_pool( + &mut self, + _: PoolUuid, + enc: &InputEncryptionInfo, + ) -> StratisResult> { + if self.encryption_info.is_some() { + Ok(CreateAction::Identity) + } else { + self.encryption_info = convert_encryption_info(Some(enc), None)?; + Ok(CreateAction::Created(( + 0, + (0, SizedKeyMemory::new(SafeMemHandle::alloc(1)?, 0)), + ))) + } + } + + fn do_encrypt_pool(&self, _: PoolUuid, _: u32, _: (u32, SizedKeyMemory)) -> StratisResult<()> { + Ok(()) + } + + fn finish_encrypt_pool(&mut self, _: &Name, _: PoolUuid) -> StratisResult<()> { + Ok(()) + } + + fn start_reencrypt_pool(&mut self) -> StratisResult> { + if self.encryption_info.is_none() { + Err(StratisError::Msg( + "Cannot reencrypt unencrypted pool".to_string(), + )) + } else { + Ok(vec![]) + } + } + + fn do_reencrypt_pool( + &self, + _: PoolUuid, + _: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()> { + Ok(()) + } + + fn finish_reencrypt_pool( + &mut self, + _: &Name, + pool_uuid: PoolUuid, + ) -> StratisResult { + self.last_reencrypt = Some(Utc::now()); + Ok(ReencryptedDevice(pool_uuid)) + } + + fn decrypt_pool_idem_check( + &mut self, + pool_uuid: PoolUuid, + ) -> StratisResult> { + if self.encryption_info.is_none() { + Ok(DeleteAction::Identity) + } else { + Ok(DeleteAction::Deleted(EncryptedDevice(pool_uuid))) + } + } + + fn do_decrypt_pool(&self, _: PoolUuid) -> StratisResult<()> { + Ok(()) + } + + fn finish_decrypt_pool(&mut self, _: PoolUuid, _: &Name) -> StratisResult<()> { + self.encryption_info = None; + self.last_reencrypt = None; + Ok(()) + } + fn current_metadata(&self, pool_name: &Name) -> StratisResult { serde_json::to_string(&self.record(pool_name)).map_err(|e| e.into()) } @@ -1081,6 +1159,10 @@ impl Pool for SimPool { fn load_volume_key(&mut self, _: PoolUuid) -> StratisResult { Ok(false) } + + fn last_reencrypt(&self) -> Option> { + self.last_reencrypt + } } #[cfg(test)] diff --git a/src/engine/sim_engine/shared.rs b/src/engine/sim_engine/shared.rs new file mode 100644 index 0000000000..7dc7edc9dc --- /dev/null +++ b/src/engine/sim_engine/shared.rs @@ -0,0 +1,42 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::{ + engine::types::{EncryptionInfo, InputEncryptionInfo, UnlockMechanism}, + stratis::{StratisError, StratisResult}, +}; + +use super::keys::SimKeyActions; + +pub fn convert_encryption_info( + encryption_info: Option<&InputEncryptionInfo>, + key_handler: Option<&SimKeyActions>, +) -> StratisResult> { + encryption_info + .cloned() + .map(|ei| { + ei.into_iter().try_fold( + EncryptionInfo::new(), + |mut info, (token_slot, unlock_mechanism)| { + let ts = match token_slot { + Some(t) => t, + None => info.free_token_slot(), + }; + if let UnlockMechanism::KeyDesc(ref kd) = unlock_mechanism { + if let Some(kh) = key_handler { + if !kh.contains_key(kd) { + return Err(StratisError::Msg(format!( + "Key {} was not found in the keyring", + kd.as_application_str() + ))); + } + } + } + info.add_info(ts, unlock_mechanism)?; + Ok(info) + }, + ) + }) + .transpose() +} diff --git a/src/engine/strat_engine/backstore/backstore/v1.rs b/src/engine/strat_engine/backstore/backstore/v1.rs index 26e39a6869..4fadadc8e4 100644 --- a/src/engine/strat_engine/backstore/backstore/v1.rs +++ b/src/engine/strat_engine/backstore/backstore/v1.rs @@ -34,7 +34,7 @@ use crate::{ }, types::{ ActionAvailability, BlockDevTier, DevUuid, EncryptionInfo, InputEncryptionInfo, - KeyDescription, Name, PoolEncryptionInfo, PoolUuid, + KeyDescription, Name, PoolEncryptionInfo, PoolUuid, SizedKeyMemory, }, }, stratis::{StratisError, StratisResult}, @@ -800,6 +800,51 @@ impl Backstore { } } + /// Setup reencryption for all encrypted devices in the pool. + /// + /// Returns: + /// * Ok(()) if successful + /// * Err(_) if an operation fails while setting up reencryption on the devices. + /// + /// Precondition: blockdevs() must always return the block devices in the + /// same order. + pub fn prepare_reencrypt(&mut self) -> StratisResult> { + if self.encryption_info().is_none() { + return Err(StratisError::Msg( + "Requested pool does not appear to be encrypted".to_string(), + )); + }; + + operation_loop( + self.blockdevs_mut().into_iter().map(|(_, _, bd)| bd), + |blockdev| blockdev.setup_reencrypt(), + ) + } + + /// Reencrypt all encrypted devices in the pool. + /// + /// Returns: + /// * Ok(()) if successful + /// * Err(_) if an operation fails while reencrypting the devices. + /// + /// Precondition: blockdevs() must always return the block devices in the + /// same order. + pub fn reencrypt( + &self, + pool_uuid: PoolUuid, + key_info: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()> { + let blockdevs = self.blockdevs(); + + assert!(blockdevs.len() == key_info.len()); + + for (bd, (slot, key, new_slot)) in blockdevs.into_iter().map(|(_, _, bd)| bd).zip(key_info) + { + bd.do_reencrypt(pool_uuid, slot, key, new_slot)?; + } + Ok(()) + } + /// Regenerate the Clevis bindings with the block devices in this pool using /// the same configuration. /// @@ -917,10 +962,10 @@ impl Recordable for Backstore { } } -fn operation_loop<'a, I, A>(blockdevs: I, action: A) -> StratisResult<()> +fn operation_loop<'a, I, A, R>(blockdevs: I, action: A) -> StratisResult> where I: IntoIterator, - A: Fn(&mut StratBlockDev) -> StratisResult<()>, + A: Fn(&mut StratBlockDev) -> StratisResult, { fn rollback_loop( rollback_record: Vec<&mut StratBlockDev>, @@ -961,13 +1006,18 @@ where causal_error } - fn perform_operation<'a, I, A>(tmp_dir: &TempDir, blockdevs: I, action: A) -> StratisResult<()> + fn perform_operation<'a, I, A, R>( + tmp_dir: &TempDir, + blockdevs: I, + action: A, + ) -> StratisResult> where I: IntoIterator, - A: Fn(&mut StratBlockDev) -> StratisResult<()>, + A: Fn(&mut StratBlockDev) -> StratisResult, { let mut original_headers = Vec::new(); let mut rollback_record = Vec::new(); + let mut results = Vec::new(); for blockdev in blockdevs { match back_up_luks_header(blockdev.physical_path(), tmp_dir) { Ok(h) => original_headers.push(h), @@ -975,12 +1025,15 @@ where }; let res = action(blockdev); rollback_record.push(blockdev); - if let Err(error) = res { - return Err(rollback_loop(rollback_record, original_headers, error)); + match res { + Ok(r) => results.push(r), + Err(e) => { + return Err(rollback_loop(rollback_record, original_headers, e)); + } } } - Ok(()) + Ok(results) } let tmp_dir = TempDir::new()?; diff --git a/src/engine/strat_engine/backstore/backstore/v2.rs b/src/engine/strat_engine/backstore/backstore/v2.rs index 383392043f..7d4bfa965a 100644 --- a/src/engine/strat_engine/backstore/backstore/v2.rs +++ b/src/engine/strat_engine/backstore/backstore/v2.rs @@ -9,6 +9,7 @@ use std::{cmp, iter::once, path::PathBuf}; use chrono::{DateTime, Utc}; use either::Either; use serde_json::Value; +use tempfile::TempDir; use devicemapper::{ CacheDev, CacheDevTargetTable, CacheTargetParams, DevId, Device, DmDevice, DmFlags, DmOptions, @@ -19,22 +20,30 @@ use crate::{ engine::{ strat_engine::{ backstore::{ - backstore::InternalBackstore, blockdev::v2::StratBlockDev, - blockdevmgr::BlockDevMgr, cache_tier::CacheTier, data_tier::DataTier, - devices::UnownedDevices, shared::BlockSizeSummary, + backstore::InternalBackstore, + blockdev::v2::StratBlockDev, + blockdevmgr::BlockDevMgr, + cache_tier::CacheTier, + data_tier::DataTier, + devices::{get_devno_from_path, UnownedDevices}, + shared::BlockSizeSummary, + }, + crypt::{ + back_up_luks_header, handle::v2::CryptHandle, manual_wipe, restore_luks_header, + DEFAULT_CRYPT_DATA_OFFSET_V2, }, - crypt::{handle::v2::CryptHandle, manual_wipe, DEFAULT_CRYPT_DATA_OFFSET_V2}, dm::{get_dm, list_of_backstore_devices, remove_optional_devices, DEVICEMAPPER_PATH}, keys::{search_key_process, unset_key_process}, metadata::MDADataSize, - names::{format_backstore_ids, CacheRole}, + names::{format_backstore_ids, format_crypt_backstore_name, CacheRole}, serde_structs::{BackstoreSave, CapSave, PoolFeatures, PoolSave, Recordable}, + thinpool::ThinPool, writing::wipe_sectors, }, types::{ ActionAvailability, BlockDevTier, DevUuid, EncryptionInfo, InputEncryptionInfo, - KeyDescription, OptionalTokenSlotInput, PoolUuid, SizedKeyMemory, TokenUnlockMethod, - UnlockMechanism, ValidatedIntegritySpec, VolumeKeyKeyDescription, + KeyDescription, OffsetDirection, OptionalTokenSlotInput, PoolUuid, SizedKeyMemory, + TokenUnlockMethod, UnlockMechanism, ValidatedIntegritySpec, VolumeKeyKeyDescription, }, }, stratis::{StratisError, StratisResult}, @@ -131,32 +140,26 @@ fn make_placeholder_dev( LinearDev::setup(get_dm(), &dm_name, Some(&dm_uuid), target).map_err(StratisError::from) } -/// This structure can allocate additional space to the upper layer, but it -/// cannot accept returned space. When it is extended to be able to accept -/// returned space the allocation algorithm will have to be revised. +/// Contains all devices that could potentially be the cap device and the allocations recorded on +/// the cap device. #[derive(Debug)] -pub struct Backstore { - /// Coordinate handling of blockdevs that back the cache. Optional, since - /// this structure can operate without a cache. - cache_tier: Option>, - /// Coordinates handling of the blockdevs that form the base. - data_tier: DataTier, +struct CapDevice { /// A linear DM device. origin: Option, /// A placeholder device to be converted to cache or a cache device. cache: Option, - /// A placeholder device to be converted to cache; necessary for reencryption support. - placeholder: Option, /// Either encryption information for a handle to be created at a later time or /// handle for encryption layer in backstore. enc: Option>, + /// A placeholder device to be converted to cache; necessary for reencryption support. + placeholder: Option, /// Data allocations on the cap device, allocs: Vec<(Sectors, Sectors)>, /// Metadata allocations on the cache or placeholder device. crypt_meta_allocs: Vec<(Sectors, Sectors)>, } -impl InternalBackstore for Backstore { +impl CapDevice { fn device(&self) -> Option { self.enc .as_ref() @@ -165,30 +168,24 @@ impl InternalBackstore for Backstore { .or_else(|| self.placeholder.as_ref().map(|lin| lin.device())) } - fn datatier_allocated_size(&self) -> Sectors { - self.allocs.iter().map(|(_, length)| *length).sum() - } - - fn datatier_usable_size(&self) -> Sectors { - self.datatier_size() - self.datatier_metadata_size() - } - - fn available_in_backstore(&self) -> Sectors { - self.datatier_usable_size() - self.datatier_allocated_size() + pub fn is_encrypted(&self) -> bool { + self.enc.is_some() } fn alloc( &mut self, + data_tier: &mut DataTier, + available: Sectors, pool_uuid: PoolUuid, sizes: &[Sectors], ) -> StratisResult>> { let total_required = sizes.iter().cloned().sum(); - if self.available_in_backstore() < total_required { + if available < total_required { return Ok(None); } - if self.data_tier.alloc(sizes) { - self.extend_cap_device(pool_uuid)?; + if data_tier.alloc(sizes) { + self.extend_cap_device(data_tier, pool_uuid)?; } else { return Ok(None); } @@ -213,9 +210,43 @@ impl InternalBackstore for Backstore { Ok(Some(chunks)) } -} -impl Backstore { + fn meta_alloc_cache( + &mut self, + data_tier: &mut DataTier, + available: Sectors, + sizes: &[Sectors], + ) -> StratisResult { + let total_required = sizes.iter().cloned().sum(); + if available < total_required { + return Ok(false); + } + + if !data_tier.alloc(sizes) { + return Ok(false); + } + + let mut chunks = Vec::new(); + for size in sizes { + let next = self.calc_next_cache()?; + let seg = (next, *size); + chunks.push(seg); + self.crypt_meta_allocs.push(seg); + } + + // Assert that the postcondition holds. + assert_eq!( + sizes, + chunks + .iter() + .map(|x| x.1) + .collect::>() + .as_slice() + ); + + Ok(true) + } + /// Calculate next from all of the metadata and data allocations present in the backstore. fn calc_next_cache(&self) -> StratisResult { let mut all_allocs = if self.allocs.is_empty() { @@ -270,6 +301,171 @@ impl Backstore { .unwrap_or(Sectors(0)) } + /// Extend the cap device whether it is a cache or not. Create the DM + /// device if it does not already exist. Return an error if DM + /// operations fail. Use all segments currently allocated in the data tier. + fn extend_cap_device( + &mut self, + data_tier: &DataTier, + pool_uuid: PoolUuid, + ) -> StratisResult<()> { + let create = match ( + self.cache.as_mut(), + self.placeholder + .as_mut() + .and_then(|p| self.origin.as_mut().map(|o| (p, o))), + self.enc.as_mut(), + ) { + (None, None, None) => true, + (_, _, Some(Either::Left(_))) => true, + (Some(cache), None, Some(Either::Right(handle))) => { + let table = data_tier.segments.map_to_dm(); + cache.set_origin_table(get_dm(), table)?; + cache.resume(get_dm())?; + handle.resize(pool_uuid, None)?; + false + } + (Some(cache), None, None) => { + let table = data_tier.segments.map_to_dm(); + cache.set_origin_table(get_dm(), table)?; + cache.resume(get_dm())?; + false + } + (None, Some((placeholder, origin)), Some(Either::Right(handle))) => { + let table = data_tier.segments.map_to_dm(); + origin.set_table(get_dm(), table)?; + origin.resume(get_dm())?; + let table = vec![TargetLine::new( + Sectors(0), + origin.size(), + LinearDevTargetParams::Linear(LinearTargetParams::new( + origin.device(), + Sectors(0), + )), + )]; + placeholder.set_table(get_dm(), table)?; + placeholder.resume(get_dm())?; + handle.resize(pool_uuid, None)?; + false + } + (None, Some((cap, linear)), None) => { + let table = data_tier.segments.map_to_dm(); + linear.set_table(get_dm(), table)?; + linear.resume(get_dm())?; + let table = vec![TargetLine::new( + Sectors(0), + linear.size(), + LinearDevTargetParams::Linear(LinearTargetParams::new( + linear.device(), + Sectors(0), + )), + )]; + cap.set_table(get_dm(), table)?; + cap.resume(get_dm())?; + false + } + _ => panic!("NOT (self.cache().is_some() AND self.origin.is_some())"), + }; + + if create { + let table = data_tier.segments.map_to_dm(); + let (dm_name, dm_uuid) = format_backstore_ids(pool_uuid, CacheRole::OriginSub); + let origin = LinearDev::setup(get_dm(), &dm_name, Some(&dm_uuid), table)?; + let placeholder = make_placeholder_dev(pool_uuid, &origin)?; + let handle = match self.enc { + Some(Either::Left(ref einfo)) => Some(CryptHandle::initialize( + &once(DEVICEMAPPER_PATH) + .chain(once( + format_backstore_ids(pool_uuid, CacheRole::Cache) + .0 + .to_string() + .as_str(), + )) + .collect::(), + pool_uuid, + einfo, + None, + )?), + Some(Either::Right(_)) => unreachable!("Checked above"), + None => { + manual_wipe( + &placeholder.devnode(), + Sectors(0), + DEFAULT_CRYPT_DATA_OFFSET_V2, + )?; + None + } + }; + self.origin = Some(origin); + self.placeholder = Some(placeholder); + self.enc = handle.map(Either::Right); + } + + Ok(()) + } + + /// Shift alloc offset when the cap device has changed. + fn shift_alloc_offset(&mut self, offset: Sectors, direction: OffsetDirection) { + for (start, _) in self.allocs.iter_mut() { + match direction { + OffsetDirection::Forwards => { + *start += offset; + } + OffsetDirection::Backwards => { + *start -= offset; + } + } + } + } +} + +/// This structure can allocate additional space to the upper layer, but it +/// cannot accept returned space. When it is extended to be able to accept +/// returned space the allocation algorithm will have to be revised. +#[derive(Debug)] +pub struct Backstore { + /// Coordinate handling of blockdevs that back the cache. Optional, since + /// this structure can operate without a cache. + cache_tier: Option>, + /// Coordinates handling of the blockdevs that form the base. + data_tier: DataTier, + /// Represents a cap device for the backstore. + cap_device: CapDevice, +} + +impl InternalBackstore for Backstore { + fn device(&self) -> Option { + self.cap_device.device() + } + + fn datatier_allocated_size(&self) -> Sectors { + self.cap_device + .allocs + .iter() + .map(|(_, length)| *length) + .sum() + } + + fn datatier_usable_size(&self) -> Sectors { + self.datatier_size() - self.datatier_metadata_size() + } + + fn available_in_backstore(&self) -> Sectors { + self.datatier_usable_size() - self.datatier_allocated_size() + } + + fn alloc( + &mut self, + pool_uuid: PoolUuid, + sizes: &[Sectors], + ) -> StratisResult>> { + let available = self.available_in_backstore(); + self.cap_device + .alloc(&mut self.data_tier, available, pool_uuid, sizes) + } +} + +impl Backstore { /// Make a Backstore object from blockdevs that already belong to Stratis. /// Precondition: every device in datadevs and cachedevs has already been /// determined to belong to the pool with the specified pool_uuid. @@ -399,12 +595,14 @@ impl Backstore { Ok(Backstore { data_tier, cache_tier, - origin, - cache, - placeholder, - enc, - allocs: pool_save.backstore.cap.allocs.clone(), - crypt_meta_allocs: pool_save.backstore.cap.crypt_meta_allocs.clone(), + cap_device: CapDevice { + origin, + cache, + placeholder, + enc, + allocs: pool_save.backstore.cap.allocs.clone(), + crypt_meta_allocs: pool_save.backstore.cap.crypt_meta_allocs.clone(), + }, }) } @@ -431,13 +629,15 @@ impl Backstore { let mut backstore = Backstore { data_tier, - placeholder: None, cache_tier: None, - cache: None, - origin: None, - enc: encryption_info.cloned().map(Either::Left), - allocs: Vec::new(), - crypt_meta_allocs: Vec::new(), + cap_device: CapDevice { + placeholder: None, + cache: None, + origin: None, + enc: encryption_info.cloned().map(Either::Left), + allocs: Vec::new(), + crypt_meta_allocs: Vec::new(), + }, }; let size = DEFAULT_CRYPT_DATA_OFFSET_V2; @@ -451,35 +651,9 @@ impl Backstore { } fn meta_alloc_cache(&mut self, sizes: &[Sectors]) -> StratisResult { - let total_required = sizes.iter().cloned().sum(); let available = self.available_in_backstore(); - if available < total_required { - return Ok(false); - } - - if !self.data_tier.alloc(sizes) { - return Ok(false); - } - - let mut chunks = Vec::new(); - for size in sizes { - let next = self.calc_next_cache()?; - let seg = (next, *size); - chunks.push(seg); - self.crypt_meta_allocs.push(seg); - } - - // Assert that the postcondition holds. - assert_eq!( - sizes, - chunks - .iter() - .map(|x| x.1) - .collect::>() - .as_slice() - ); - - Ok(true) + self.cap_device + .meta_alloc_cache(&mut self.data_tier, available, sizes) } /// Initialize the cache tier and add cachedevs to the backstore. @@ -511,16 +685,16 @@ impl Backstore { let cache_tier = CacheTier::new(bdm)?; - let origin = self.origin - .take() - .expect("some space has already been allocated from the backstore => (cache_tier.is_none() <=> self.origin.is_some())"); - let placeholder = self.placeholder - .take() - .expect("some space has already been allocated from the backstore => (cache_tier.is_none() <=> self.placeholder.is_some())"); + let origin = self.cap_device.origin + .take() + .expect("some space has already been allocated from the backstore => (cache_tier.is_none() <=> self.origin.is_some())"); + let placeholder = self.cap_device.placeholder + .take() + .expect("some space has already been allocated from the backstore => (cache_tier.is_none() <=> self.placeholder.is_some())"); let cache = make_cache(pool_uuid, &cache_tier, origin, Some(placeholder), true)?; - self.cache = Some(cache); + self.cap_device.cache = Some(cache); let uuids = cache_tier .block_mgr @@ -556,6 +730,7 @@ impl Backstore { match self.cache_tier { Some(ref mut cache_tier) => { let cache_device = self + .cap_device .cache .as_mut() .expect("cache_tier.is_some() <=> self.cache.is_some()"); @@ -592,105 +767,6 @@ impl Backstore { self.data_tier.add(pool_uuid, devices) } - /// Extend the cap device whether it is a cache or not. Create the DM - /// device if it does not already exist. Return an error if DM - /// operations fail. Use all segments currently allocated in the data tier. - fn extend_cap_device(&mut self, pool_uuid: PoolUuid) -> StratisResult<()> { - let create = match ( - self.cache.as_mut(), - self.placeholder - .as_mut() - .and_then(|p| self.origin.as_mut().map(|o| (p, o))), - self.enc.as_mut(), - ) { - (None, None, None) => true, - (_, _, Some(Either::Left(_))) => true, - (Some(cache), None, Some(Either::Right(handle))) => { - let table = self.data_tier.segments.map_to_dm(); - cache.set_origin_table(get_dm(), table)?; - cache.resume(get_dm())?; - handle.resize(pool_uuid, None)?; - false - } - (Some(cache), None, None) => { - let table = self.data_tier.segments.map_to_dm(); - cache.set_origin_table(get_dm(), table)?; - cache.resume(get_dm())?; - false - } - (None, Some((placeholder, origin)), Some(Either::Right(handle))) => { - let table = self.data_tier.segments.map_to_dm(); - origin.set_table(get_dm(), table)?; - origin.resume(get_dm())?; - let table = vec![TargetLine::new( - Sectors(0), - origin.size(), - LinearDevTargetParams::Linear(LinearTargetParams::new( - origin.device(), - Sectors(0), - )), - )]; - placeholder.set_table(get_dm(), table)?; - placeholder.resume(get_dm())?; - handle.resize(pool_uuid, None)?; - false - } - (None, Some((cap, linear)), None) => { - let table = self.data_tier.segments.map_to_dm(); - linear.set_table(get_dm(), table)?; - linear.resume(get_dm())?; - let table = vec![TargetLine::new( - Sectors(0), - linear.size(), - LinearDevTargetParams::Linear(LinearTargetParams::new( - linear.device(), - Sectors(0), - )), - )]; - cap.set_table(get_dm(), table)?; - cap.resume(get_dm())?; - false - } - _ => panic!("NOT (self.cache().is_some() AND self.origin.is_some())"), - }; - - if create { - let table = self.data_tier.segments.map_to_dm(); - let (dm_name, dm_uuid) = format_backstore_ids(pool_uuid, CacheRole::OriginSub); - let origin = LinearDev::setup(get_dm(), &dm_name, Some(&dm_uuid), table)?; - let placeholder = make_placeholder_dev(pool_uuid, &origin)?; - let handle = match self.enc { - Some(Either::Left(ref einfo)) => Some(CryptHandle::initialize( - &once(DEVICEMAPPER_PATH) - .chain(once( - format_backstore_ids(pool_uuid, CacheRole::Cache) - .0 - .to_string() - .as_str(), - )) - .collect::(), - pool_uuid, - einfo, - None, - )?), - Some(Either::Right(_)) => unreachable!("Checked above"), - None => { - manual_wipe( - &placeholder.devnode(), - Sectors(0), - DEFAULT_CRYPT_DATA_OFFSET_V2, - )?; - None - } - }; - self.origin = Some(origin); - self.placeholder = Some(placeholder); - self.enc = handle.map(Either::Right); - } - - Ok(()) - } - /// Get only the datadevs in the pool. pub fn datadevs(&self) -> Vec<(DevUuid, &StratBlockDev)> { self.data_tier.blockdevs() @@ -756,17 +832,23 @@ impl Backstore { /// no ioctl is required. #[cfg(test)] fn size(&self) -> Sectors { - self.enc + self.cap_device + .enc .as_ref() .and_then(|either| either.as_ref().right().map(|handle| handle.size())) - .or_else(|| self.placeholder.as_ref().map(|d| d.size())) - .or_else(|| self.cache.as_ref().map(|d| d.size())) + .or_else(|| self.cap_device.placeholder.as_ref().map(|d| d.size())) + .or_else(|| self.cap_device.cache.as_ref().map(|d| d.size())) .unwrap_or(Sectors(0)) } /// Destroy the entire store. pub fn destroy(&mut self, pool_uuid: PoolUuid) -> StratisResult<()> { - if let Some(h) = self.enc.as_mut().and_then(|either| either.as_ref().right()) { + if let Some(h) = self + .cap_device + .enc + .as_mut() + .and_then(|either| either.as_ref().right()) + { h.wipe()?; } let devs = list_of_backstore_devices(pool_uuid); @@ -821,7 +903,11 @@ impl Backstore { /// The space is included in the data_tier.allocated() result, since it is /// allocated from the assembled devices of the data tier. fn datatier_crypt_meta_size(&self) -> Sectors { - self.crypt_meta_allocs.iter().map(|(_, len)| *len).sum() + self.cap_device + .crypt_meta_allocs + .iter() + .map(|(_, len)| *len) + .sum() } /// Metadata size on the data tier, including crypt metadata space. @@ -868,7 +954,7 @@ impl Backstore { } pub fn is_encrypted(&self) -> bool { - self.enc.is_some() + self.cap_device.is_encrypted() } pub fn has_cache(&self) -> bool { @@ -877,7 +963,8 @@ impl Backstore { /// Get the encryption information for the backstore. pub fn encryption_info(&self) -> Option<&EncryptionInfo> { - self.enc + self.cap_device + .enc .as_ref() .and_then(|either| either.as_ref().right().map(|h| h.encryption_info())) } @@ -896,6 +983,7 @@ impl Backstore { clevis_info: &Value, ) -> StratisResult> { let handle = self + .cap_device .enc .as_mut() .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? @@ -955,6 +1043,7 @@ impl Backstore { /// * Returns Err(_) if unbinding failed. pub fn unbind_keyring(&mut self, token_slot: Option) -> StratisResult { let handle = self + .cap_device .enc .as_mut() .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? @@ -996,6 +1085,7 @@ impl Backstore { /// * Returns Err(_) if unbinding failed. pub fn unbind_clevis(&mut self, token_slot: Option) -> StratisResult { let handle = self + .cap_device .enc .as_mut() .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? @@ -1044,6 +1134,7 @@ impl Backstore { key_desc: &KeyDescription, ) -> StratisResult> { let handle = self + .cap_device .enc .as_mut() .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? @@ -1119,6 +1210,7 @@ impl Backstore { key_desc: &KeyDescription, ) -> StratisResult> { let handle = self + .cap_device .enc .as_mut() .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? @@ -1166,6 +1258,7 @@ impl Backstore { /// result in a metadata change. pub fn rebind_clevis(&mut self, token_slot: Option) -> StratisResult<()> { let handle = self + .cap_device .enc .as_mut() .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? @@ -1198,6 +1291,133 @@ impl Backstore { self.data_tier.grow(dev) } + pub fn prepare_encrypt( + &mut self, + pool_uuid: PoolUuid, + thinpool: &mut ThinPool, + offset: Sectors, + offset_direction: OffsetDirection, + encryption_info: &InputEncryptionInfo, + ) -> StratisResult<(u32, (u32, SizedKeyMemory))> { + let (dm_name, _) = format_backstore_ids(pool_uuid, CacheRole::Cache); + let unencrypted_path = PathBuf::from(DEVICEMAPPER_PATH).join(dm_name.to_string()); + let (sector_size, key_info) = + CryptHandle::setup_encrypt(pool_uuid, thinpool, &unencrypted_path, encryption_info)?; + + thinpool.suspend()?; + let set_device_res = get_devno_from_path( + &PathBuf::from(DEVICEMAPPER_PATH) + .join(format_crypt_backstore_name(&pool_uuid).to_string()), + ) + .and_then(|devno| thinpool.set_device(devno, offset, offset_direction)); + thinpool.resume()?; + self.shift_alloc_offset(offset, offset_direction); + set_device_res?; + + Ok((sector_size, key_info)) + } + + pub fn do_encrypt( + pool_uuid: PoolUuid, + sector_size: u32, + key_info: (u32, SizedKeyMemory), + ) -> StratisResult<()> { + let (dm_name, _) = format_backstore_ids(pool_uuid, CacheRole::Cache); + let unencrypted_path = PathBuf::from(DEVICEMAPPER_PATH).join(dm_name.to_string()); + CryptHandle::do_encrypt(&unencrypted_path, pool_uuid, sector_size, key_info) + } + + pub fn finish_encrypt(&mut self, pool_uuid: PoolUuid) -> StratisResult<()> { + let (dm_name, _) = format_backstore_ids(pool_uuid, CacheRole::Cache); + let unencrypted_path = PathBuf::from(DEVICEMAPPER_PATH).join(dm_name.to_string()); + let handle = CryptHandle::finish_encrypt(&unencrypted_path, pool_uuid)?; + self.cap_device.enc = Some(Either::Right(handle)); + Ok(()) + } + + pub fn prepare_reencrypt(&self) -> StratisResult> { + match self.cap_device.enc { + Some(Either::Left(_)) => { + Err(StratisError::Msg("Encrypted pool where the encrypted device has not yet been created cannot be reencrypted".to_string())) + } + Some(Either::Right(ref handle)) => { + let tmp_dir = TempDir::new()?; + let h = back_up_luks_header(handle.luks2_device_path(), &tmp_dir)?; + match handle.setup_reencrypt() { + Ok(tup) => Ok(vec![tup]), + Err(causal_e) => { + if let Err(rollback_e) = restore_luks_header(handle.luks2_device_path(), &h) { + Err(StratisError::RollbackError { + causal_error: Box::new(causal_e), + rollback_error: Box::new(rollback_e), + level: ActionAvailability::Full, + }) + } else { + Err(causal_e) + } + } + } + } + None => { + Err(StratisError::Msg("Unencrypted device cannot be reencrypted".to_string())) + } + } + } + + /// Precondition: key_info contains exactly one entry. + pub fn reencrypt( + &self, + pool_uuid: PoolUuid, + mut key_info: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()> { + assert!(key_info.len() == 1); + let (slot, key, new_slot) = key_info.remove(0); + match self.cap_device.enc { + Some(Either::Right(ref handle)) => { + handle.do_reencrypt(pool_uuid, slot, key, new_slot)?; + } + _ => unreachable!("Should have called prepare_reencrypt"), + } + Ok(()) + } + + pub fn do_decrypt(&self, pool_uuid: PoolUuid) -> StratisResult<()> { + let handle = self + .cap_device + .enc + .as_ref() + .ok_or_else(|| StratisError::Msg("Pool is not encrypted".to_string()))? + .as_ref() + .right() + .ok_or_else(|| { + StratisError::Msg("No space has been allocated from the backstore".to_string()) + })?; + let luks2_path = handle.luks2_device_path().to_owned(); + + handle.decrypt(pool_uuid)?; + manual_wipe(&luks2_path, Sectors(0), DEFAULT_CRYPT_DATA_OFFSET_V2)?; + Ok(()) + } + + pub fn finish_decrypt( + &mut self, + pool_uuid: PoolUuid, + thinpool: &mut ThinPool, + offset: Sectors, + direction: OffsetDirection, + ) -> StratisResult<()> { + thinpool.suspend()?; + let (dm_name, _) = format_backstore_ids(pool_uuid, CacheRole::Cache); + let set_device_res = + get_devno_from_path(&PathBuf::from(DEVICEMAPPER_PATH).join(dm_name.to_string())) + .and_then(|devno| thinpool.set_device(devno, offset, direction)); + thinpool.resume()?; + self.shift_alloc_offset(offset, direction); + set_device_res?; + self.cap_device.enc = None; + Ok(()) + } + /// A summary of block sizes pub fn block_size_summary(&self, tier: BlockDevTier) -> Option { match tier { @@ -1255,6 +1475,11 @@ impl Backstore { Ok(true) } } + + /// Shift the allocations in the backstore by the requested offset in the requested direction. + pub fn shift_alloc_offset(&mut self, offset: Sectors, direction: OffsetDirection) { + self.cap_device.shift_alloc_offset(offset, direction) + } } impl Into for &Backstore { @@ -1281,8 +1506,8 @@ impl Recordable for Backstore { BackstoreSave { cache_tier: self.cache_tier.as_ref().map(|c| c.record()), cap: CapSave { - allocs: self.allocs.clone(), - crypt_meta_allocs: self.crypt_meta_allocs.clone(), + allocs: self.cap_device.allocs.clone(), + crypt_meta_allocs: self.cap_device.crypt_meta_allocs.clone(), }, data_tier: self.data_tier.record(), } @@ -1318,14 +1543,14 @@ mod tests { /// device fn invariant(backstore: &Backstore) { assert!( - (backstore.cache_tier.is_none() && backstore.cache.is_none()) + (backstore.cache_tier.is_none() && backstore.cap_device.cache.is_none()) || (backstore.cache_tier.is_some() - && backstore.cache.is_some() - && backstore.origin.is_none()) + && backstore.cap_device.cache.is_some() + && backstore.cap_device.origin.is_none()) ); assert_eq!( backstore.data_tier.allocated(), - match (&backstore.origin, &backstore.cache) { + match (&backstore.cap_device.origin, &backstore.cap_device.cache) { (None, None) => DEFAULT_CRYPT_DATA_OFFSET_V2, (&None, Some(cache)) => cache.size(), (Some(linear), &None) => linear.size(), @@ -1397,9 +1622,10 @@ mod tests { invariant(&backstore); assert_eq!(cache_uuids.len(), initcachepaths.len()); - assert_matches!(backstore.origin, None); + assert_matches!(backstore.cap_device.origin, None); let cache_status = backstore + .cap_device .cache .as_ref() .map(|c| c.status(get_dm(), DmOptions::default()).unwrap()) @@ -1425,6 +1651,7 @@ mod tests { assert_eq!(cache_uuids.len(), cachedevpaths.len()); let cache_status = backstore + .cap_device .cache .as_ref() .map(|c| c.status(get_dm(), DmOptions::default()).unwrap()) diff --git a/src/engine/strat_engine/backstore/blockdev/v1.rs b/src/engine/strat_engine/backstore/blockdev/v1.rs index 4bec722702..3c4bf9523f 100644 --- a/src/engine/strat_engine/backstore/blockdev/v1.rs +++ b/src/engine/strat_engine/backstore/blockdev/v1.rs @@ -35,7 +35,7 @@ use crate::{ }, types::{ Compare, DevUuid, DevicePath, Diff, EncryptionInfo, KeyDescription, Name, PoolUuid, - StateDiff, StratBlockDevDiff, StratSigblockVersion, + SizedKeyMemory, StateDiff, StratBlockDevDiff, StratSigblockVersion, }, }, stratis::{StratisError, StratisResult}, @@ -403,6 +403,35 @@ impl StratBlockDev { } } } + + /// Prepare the crypt header for reencryption. + /// + /// Can be rolled back. + pub fn setup_reencrypt(&self) -> StratisResult<(u32, SizedKeyMemory, u32)> { + let crypt_handle = self + .underlying_device + .crypt_handle() + .expect("Checked that pool is encrypted"); + crypt_handle.setup_reencrypt() + } + + /// Perform the reencryption. + /// + /// Cannot be rolled back. + pub fn do_reencrypt( + &self, + pool_uuid: PoolUuid, + keyslot: u32, + key: SizedKeyMemory, + new_keyslot: u32, + ) -> StratisResult<()> { + let crypt_handle = self + .underlying_device + .crypt_handle() + .expect("Checked that pool is encrypted"); + crypt_handle.do_reencrypt(pool_uuid, keyslot, key, new_keyslot) + } + #[cfg(test)] pub fn invariant(&self) { assert!(self.total_size() == self.used.size()); diff --git a/src/engine/strat_engine/backstore/devices.rs b/src/engine/strat_engine/backstore/devices.rs index cd71a57325..cb9cacbb51 100644 --- a/src/engine/strat_engine/backstore/devices.rs +++ b/src/engine/strat_engine/backstore/devices.rs @@ -978,6 +978,13 @@ where } } +/// Use libblkid to probe for logical sector size of the device specified by path. +pub fn get_logical_sector_size(path: &Path) -> StratisResult { + let mut probe = BlkidProbe::new_from_filename(path)?; + let top = probe.get_topology()?; + Ok(Bytes::from(top.get_logical_sector_size())) +} + #[cfg(test)] mod tests { use std::fs::OpenOptions; diff --git a/src/engine/strat_engine/backstore/mod.rs b/src/engine/strat_engine/backstore/mod.rs index 58add8867b..6d196bb3b7 100644 --- a/src/engine/strat_engine/backstore/mod.rs +++ b/src/engine/strat_engine/backstore/mod.rs @@ -14,7 +14,10 @@ mod shared; pub use self::{ blockdev::v2::integrity_meta_space, - devices::{find_stratis_devs_by_uuid, get_devno_from_path, ProcessedPathInfos, UnownedDevices}, + devices::{ + find_stratis_devs_by_uuid, get_devno_from_path, get_logical_sector_size, + ProcessedPathInfos, UnownedDevices, + }, }; #[cfg(test)] diff --git a/src/engine/strat_engine/cmd.rs b/src/engine/strat_engine/cmd.rs index e9777db2b5..46776a13a9 100644 --- a/src/engine/strat_engine/cmd.rs +++ b/src/engine/strat_engine/cmd.rs @@ -111,7 +111,6 @@ const CLEVIS_EXEC_NAMES: &[&str] = &[ CLEVIS_REGEN, JOSE, JQ, - CRYPTSETUP, CURL, TPM2_CREATEPRIMARY, TPM2_UNSEAL, @@ -132,6 +131,7 @@ static EXECUTABLES: LazyLock>> = LazyLock::new(| THIN_METADATA_SIZE.to_string(), find_executable(THIN_METADATA_SIZE), ), + (CRYPTSETUP.to_string(), find_executable(CRYPTSETUP)), ] .iter() .cloned() @@ -579,3 +579,38 @@ pub fn thin_metadata_size( ))) } } + +pub fn run_encrypt(path: &Path) -> StratisResult<()> { + get_persistent_keyring()?; + let mut cmd = Command::new(get_executable(CRYPTSETUP)); + cmd.arg("reencrypt") + .arg("--encrypt") + .arg("--resume-only") + .arg("--token-only") + .arg(path); + + execute_cmd(&mut cmd) +} + +pub fn run_reencrypt(path: &Path) -> StratisResult<()> { + get_persistent_keyring()?; + let mut cmd = Command::new(get_executable(CRYPTSETUP)); + cmd.arg("reencrypt") + .arg("--resume-only") + .arg("--token-only") + .arg(path); + + execute_cmd(&mut cmd) +} + +pub fn run_decrypt(path: &Path) -> StratisResult<()> { + get_persistent_keyring()?; + let mut cmd = Command::new(get_executable(CRYPTSETUP)); + cmd.arg("reencrypt") + .arg("--decrypt") + .arg("--resume-only") + .arg("--token-only") + .arg(path); + + execute_cmd(&mut cmd) +} diff --git a/src/engine/strat_engine/crypt/handle/v1.rs b/src/engine/strat_engine/crypt/handle/v1.rs index 40bb734aaa..ec5161e065 100644 --- a/src/engine/strat_engine/crypt/handle/v1.rs +++ b/src/engine/strat_engine/crypt/handle/v1.rs @@ -44,8 +44,8 @@ use crate::{ acquire_crypt_device, activate, activate_by_token, add_keyring_keyslot, check_luks2_token, clevis_decrypt, device_from_physical_path, encryption_info_from_metadata, ensure_inactive, ensure_wiped, - get_keyslot_number, interpret_clevis_config, luks2_token_type_is_valid, - read_key, wipe_fallback, + get_keyslot_number, handle_do_reencrypt, handle_setup_reencrypt, + interpret_clevis_config, luks2_token_type_is_valid, read_key, wipe_fallback, }, }, dm::DEVICEMAPPER_PATH, @@ -1003,6 +1003,34 @@ impl CryptHandle { Ok(()) } + /// Set up a reencryption operation on the given crypt device. + /// + /// This operation can be rolled back. + pub fn setup_reencrypt(&self) -> StratisResult<(u32, SizedKeyMemory, u32)> { + handle_setup_reencrypt(self.luks2_device_path(), self.encryption_info()) + } + + /// Perform the reencryption operation on the encrypted pool to convert to switch to another + /// volume key. + /// + /// This operation cannot be rolled back. + pub fn do_reencrypt( + &self, + pool_uuid: PoolUuid, + single_keyslot: u32, + single_key: SizedKeyMemory, + single_new_keyslot: u32, + ) -> StratisResult<()> { + handle_do_reencrypt( + self.metadata.activation_name.to_string().as_str(), + pool_uuid, + self.luks2_device_path(), + single_keyslot, + single_key, + single_new_keyslot, + ) + } + /// Rename the pool in the LUKS2 token. pub fn rename_pool_in_metadata(&mut self, pool_name: Name) -> StratisResult<()> { let mut device = self.acquire_crypt_device()?; diff --git a/src/engine/strat_engine/crypt/handle/v2.rs b/src/engine/strat_engine/crypt/handle/v2.rs index 4f150d770d..1219fc261c 100644 --- a/src/engine/strat_engine/crypt/handle/v2.rs +++ b/src/engine/strat_engine/crypt/handle/v2.rs @@ -5,6 +5,7 @@ use std::{ fmt::Debug, fs::File, + io::Write, iter::once, path::{Path, PathBuf}, }; @@ -17,10 +18,14 @@ use devicemapper::{Device, DmName, DmNameBuf, Sectors}; 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,24 +34,28 @@ use crate::{ engine::{ engine::MAX_STRATIS_PASS_SIZE, strat_engine::{ - backstore::get_devno_from_path, - cmd::{clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind}, + backstore::{backstore::v2, get_devno_from_path, get_logical_sector_size}, + cmd::{ + clevis_luks_bind, clevis_luks_regen, clevis_luks_unbind, run_decrypt, run_encrypt, + }, crypt::{ consts::{ DEFAULT_CRYPT_DATA_OFFSET_V2, DEFAULT_CRYPT_KEYSLOTS_SIZE, DEFAULT_CRYPT_METADATA_SIZE_V2, STRATIS_MEK_SIZE, }, shared::{ - acquire_crypt_device, activate, add_keyring_keyslot, clevis_decrypt, - clevis_info_from_json, device_from_physical_path, - encryption_info_from_metadata, ensure_wiped, get_keyslot_number, - interpret_clevis_config, load_vk_to_keyring, read_key, wipe_fallback, + acquire_crypt_device, activate, add_keyring_keyslot, clevis_info_from_json, + device_from_physical_path, encryption_info_from_metadata, ensure_wiped, + get_keyslot_number, get_passphrase, handle_do_reencrypt, + handle_setup_reencrypt, interpret_clevis_config, load_vk_to_keyring, + wipe_fallback, }, }, device::blkdev_size, dm::DEVICEMAPPER_PATH, keys::read_key_process, names::format_crypt_backstore_name, + thinpool::ThinPool, }, types::{ DevicePath, EncryptionInfo, InputEncryptionInfo, KeyDescription, PoolUuid, @@ -56,6 +65,8 @@ use crate::{ stratis::{StratisError, StratisResult}, }; +const REQUIRED_CRYPT_INIT_WRITE_SIZE: usize = 4096; + /// Load crypt device metadata. pub fn load_crypt_metadata( device: &mut CryptDevice, @@ -179,39 +190,6 @@ fn setup_crypt_handle( ))) } -/// Get one of the passphrases for the encrypted device. -fn get_passphrase( - device: &mut CryptDevice, - encryption_info: &EncryptionInfo, -) -> StratisResult> { - for (ts, mech) in encryption_info.all_infos() { - match mech { - UnlockMechanism::KeyDesc(kd) => match read_key(kd) { - Ok(Some(key)) => { - return Ok(Either::Left((*ts, key))); - } - Ok(None) => { - info!("Key description was not in keyring; trying next unlock mechanism") - } - Err(e) => info!("Error searching keyring: {e}"), - }, - UnlockMechanism::ClevisInfo(_) => match clevis_decrypt(device, *ts) { - Ok(Some(pass)) => { - return Ok(Either::Right(pass)); - } - Ok(None) => { - info!("Failed to find the given token; trying next unlock method"); - } - Err(e) => info!("Error attempting to unlock with clevis: {e}"), - }, - } - } - - Err(StratisError::Msg( - "Unable to get passphrase for any available token slots".to_string(), - )) -} - /// Handle for performing all operations on an encrypted device. /// /// `Clone` is derived for this data structure because `CryptHandle` acquires @@ -307,13 +285,13 @@ impl CryptHandle { }) } - 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: &InputEncryptionInfo, luks2_params: Option<&CryptParamsLuks2>, - ) -> StratisResult<()> { + ) -> StratisResult { let mut luks2_params_ref: Option> = luks2_params.map(|lp| lp.try_into()).transpose()?; @@ -426,6 +404,21 @@ impl CryptHandle { let encryption_info = encryption_info_from_metadata(device)?; + Ok(encryption_info) + } + + /// 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: &InputEncryptionInfo, + luks2_params: Option<&CryptParamsLuks2>, + ) -> StratisResult<()> { + let encryption_info = + Self::initialize_unlock_methods(device, physical_path, encryption_info, luks2_params)?; + let activation_name = format_crypt_backstore_name(&pool_uuid); activate( device, @@ -568,7 +561,7 @@ impl CryptHandle { clevis_luks_bind( self.luks2_device_path(), - &either.map_left(|(ts, _)| ts), + &either.map_left(|(_, ts, _)| ts).map_right(|(_, key)| key), token_slot, pin, &json_owned, @@ -657,8 +650,8 @@ impl CryptHandle { } let mut device = self.acquire_crypt_device()?; - let key = - get_passphrase(&mut device, self.encryption_info())?.either(|(_, key)| key, |key| key); + let key = get_passphrase(&mut device, self.encryption_info())? + .either(|(_, _, key)| key, |(_, key)| key); let t = match token_slot { Some(t) => t, @@ -729,6 +722,217 @@ impl CryptHandle { Ok(()) } + /// Set up encryption for an unencrypted pool. + pub fn setup_encrypt( + pool_uuid: PoolUuid, + thinpool: &ThinPool, + unencrypted_path: &Path, + encryption_info: &InputEncryptionInfo, + ) -> StratisResult<(u32, (u32, SizedKeyMemory))> { + let mut tmp_file = tempfile::NamedTempFile::new()?; + tmp_file + .as_file_mut() + .write_all(&[0; REQUIRED_CRYPT_INIT_WRITE_SIZE])?; + + let mut device = CryptInit::init(Path::new(&tmp_file.path()))?; + let data_offset = DEFAULT_CRYPT_DATA_OFFSET_V2; + device.set_data_offset(*data_offset)?; + + let min_sector = thinpool.min_logical_sector_size()?; + let sector_size = match min_sector { + Some(min) => convert_int!(*min, u128, u32)?, + None => { + let sector_size = get_logical_sector_size(unencrypted_path)?; + convert_int!(*sector_size, u128, u32)? + } + }; + let params = CryptParamsLuks2 { + data_alignment: 0, + data_device: None, + integrity: None, + integrity_params: None, + pbkdf: None, + label: None, + sector_size, + subsystem: None, + }; + + let encryption_info = Self::initialize_unlock_methods( + &mut device, + tmp_file.path(), + encryption_info, + Some(¶ms), + )?; + let (keyslot, key) = get_passphrase(&mut device, &encryption_info)? + .either(|(keyslot, _, key)| (keyslot, key), |tup| tup); + device.reencrypt_handle().reencrypt_init_by_passphrase( + None, + key.as_ref(), + None, + Some(keyslot), + Some(("aes", "xts-plain64")), + 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: Some(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), tmp_file.path())?; + + 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, + )?; + + Ok((sector_size, (keyslot, key))) + } + + /// Perform the online encryption operation that was set up. + /// + /// Precondition: setup_encrypt was already called + /// Precondition: crypt device was already added as the backing device for the thin pool + /// failure to do so will result in corruption + pub fn do_encrypt( + unencrypted_path: &Path, + pool_uuid: PoolUuid, + sector_size: u32, + key_info: (u32, SizedKeyMemory), + ) -> StratisResult<()> { + { + let mut device = acquire_crypt_device(unencrypted_path)?; + let activation_name = &format_crypt_backstore_name(&pool_uuid).to_string(); + let (keyslot, key) = key_info; + device.reencrypt_handle().reencrypt_init_by_passphrase( + Some(activation_name), + key.as_ref(), + None, + Some(keyslot), + Some(("aes", "xts-plain64")), + 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: Some(CryptParamsLuks2 { + data_alignment: 0, + data_device: None, + integrity: None, + integrity_params: None, + pbkdf: None, + label: None, + sector_size, + subsystem: None, + }), + flags: CryptReencrypt::RESUME_ONLY, + }, + )?; + } + + info!("Starting encryption operation on pool with UUID {pool_uuid}; may take a while"); + // The corresponding libcryptsetup call is device.reencrypt_handle().reencrypt2::<()>(None, None)?; + run_encrypt(unencrypted_path)?; + + Ok(()) + } + + /// Generate the CryptHandle from the LUKS2 device path. + /// + /// Precondition: LUKS2 device path was fully encrypted successfully + pub fn finish_encrypt( + unencrypted_path: &Path, + pool_uuid: PoolUuid, + ) -> StratisResult { + CryptHandle::setup(unencrypted_path, pool_uuid, TokenUnlockMethod::Any, None) + .map(|h| h.expect("should have crypt device after online encrypt")) + } + + /// Set up a reencryption operation on the given crypt device. + /// + /// Can be rolled back. + pub fn setup_reencrypt(&self) -> StratisResult<(u32, SizedKeyMemory, u32)> { + handle_setup_reencrypt(self.luks2_device_path(), self.encryption_info()) + } + + /// Perform the reencryption operation on the encrypted pool to convert to switch to another + /// volume key. + /// + /// Cannot be rolled back. + pub fn do_reencrypt( + &self, + pool_uuid: PoolUuid, + slot: u32, + key: SizedKeyMemory, + new_slot: u32, + ) -> StratisResult<()> { + handle_do_reencrypt( + self.metadata.activation_name.to_string().as_str(), + pool_uuid, + self.luks2_device_path(), + slot, + key, + new_slot, + ) + } + + /// Decrypt the crypt device for an encrypted pool. + pub fn decrypt(&self, pool_uuid: PoolUuid) -> StratisResult<()> { + { + let activation_name = format_crypt_backstore_name(&pool_uuid); + let mut device = acquire_crypt_device(self.luks2_device_path())?; + let (keyslot, key) = get_passphrase(&mut device, self.encryption_info())? + .either(|(keyslot, _, key)| (keyslot, key), |tup| tup); + + device.reencrypt_handle().reencrypt_init_by_passphrase( + Some(&activation_name.to_string()), + key.as_ref(), + Some(keyslot), + None, + None, + CryptParamsReencrypt { + mode: CryptReencryptModeInfo::Decrypt, + direction: CryptReencryptDirectionInfo::Forward, + resilience: "checksum".to_string(), + hash: "sha256".to_string(), + data_shift: 0, + max_hotzone_size: 0, + device_size: 0, + luks2: None, + flags: CryptReencrypt::empty(), + }, + )?; + } + + info!("Starting decryption operation on pool with UUID {pool_uuid}; may take a while"); + // the corresponding libcryptsetup call is device.reencrypt_handle().reencrypt2::<()>(None, None)?; + run_decrypt(self.luks2_device_path())?; + Ok(()) + } + /// Deactivate the device referenced by the current device handle. #[cfg(test)] pub fn deactivate(&self) -> StratisResult<()> { diff --git a/src/engine/strat_engine/crypt/shared.rs b/src/engine/strat_engine/crypt/shared.rs index d8e0362576..50c277f91e 100644 --- a/src/engine/strat_engine/crypt/shared.rs +++ b/src/engine/strat_engine/crypt/shared.rs @@ -21,12 +21,14 @@ use devicemapper::{DevId, DmName, DmOptions, Sectors, SECTOR_SIZE}; use libcryptsetup_rs::{ c_uint, consts::{ - flags::{CryptActivate, CryptVolumeKey, CryptWipe}, + flags::{CryptActivate, CryptReencrypt, CryptVolumeKey, CryptWipe}, vals::{ - CryptDebugLevel, CryptLogLevel, CryptStatusInfo, CryptWipePattern, EncryptionFormat, + CryptDebugLevel, CryptLogLevel, CryptReencryptDirectionInfo, CryptReencryptModeInfo, + CryptStatusInfo, CryptWipePattern, EncryptionFormat, }, }, - register, set_debug_level, set_log_callback, CryptDevice, CryptInit, + get_sector_size, register, set_debug_level, set_log_callback, CryptDevice, CryptInit, + CryptParamsLuks2, CryptParamsReencrypt, SafeMemHandle, TokenInput, }; use crate::{ @@ -36,21 +38,23 @@ use crate::{ crypt::consts::{ CLEVIS_RECURSION_LIMIT, CLEVIS_TANG_TRUST_URL, CLEVIS_TOKEN_NAME, CLEVIS_TOKEN_TYPE, LUKS2_SECTOR_SIZE, LUKS2_TOKEN_ID, LUKS2_TOKEN_TYPE, - TOKEN_KEYSLOTS_KEY, TOKEN_TYPE_KEY, + STRATIS_MEK_SIZE, TOKEN_KEYSLOTS_KEY, TOKEN_TYPE_KEY, }, dm::get_dm, keys, }, types::{ - KeyDescription, PoolUuid, SizedKeyMemory, UnlockMechanism, VolumeKeyKeyDescription, + EncryptionInfo, KeyDescription, PoolUuid, SizedKeyMemory, UnlockMechanism, + VolumeKeyKeyDescription, }, - EncryptionInfo, }, stratis::{StratisError, StratisResult}, }; static CLEVIS_ERROR: OnceLock>> = OnceLock::new(); +type PassphraseInfo = Either<(u32, u32, SizedKeyMemory), (u32, SizedKeyMemory)>; + /// Set up crypt logging to log cryptsetup debug information at the trace level. pub fn set_up_crypt_logging() { fn logging_callback(level: CryptLogLevel, msg: &str, _: Option<&mut ()>) { @@ -990,6 +994,244 @@ pub fn clevis_decrypt( 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 { + for (ts, mech) in encryption_info.all_infos() { + let keyslot = match get_keyslot_number(device, *ts) { + Ok(Some(ks)) => ks, + Ok(None) => { + warn!("Unable to find associated keyslot for token slot"); + continue; + } + Err(e) => { + warn!("Error while querying associated keyslot for token slot: {e}"); + continue; + } + }; + match mech { + UnlockMechanism::KeyDesc(kd) => match read_key(kd) { + Ok(Some(key)) => { + return Ok(Either::Left((keyslot, *ts, key))); + } + Ok(None) => { + info!("Key description was not in keyring; trying next unlock mechanism") + } + Err(e) => info!("Error searching keyring: {e}"), + }, + UnlockMechanism::ClevisInfo(_) => match clevis_decrypt(device, *ts) { + Ok(Some(pass)) => { + return Ok(Either::Right((keyslot, pass))); + } + Ok(None) => { + info!("Failed to find the given token; trying next unlock method"); + } + Err(e) => info!("Error attempting to unlock with clevis: {e}"), + }, + } + } + + Err(StratisError::Msg( + "Unable to get passphrase for any available token slots".to_string(), + )) +} + +/// Get all of the passphrases for the encrypted device for online reencryption. +pub fn get_all_passphrases( + device: &mut CryptDevice, + encryption_info: &EncryptionInfo, +) -> StratisResult> { + let mut passphrases = Vec::new(); + for (ts, mech) in encryption_info.all_infos() { + match mech { + UnlockMechanism::KeyDesc(kd) => match read_key(kd) { + Ok(Some(pass)) => { + passphrases.push((*ts, pass)); + } + Ok(None) => { + return Err(StratisError::Msg(format!( + "Key description {} was not in keyring", + kd.as_application_str(), + ))) + } + Err(e) => { + return Err(StratisError::Chained( + "Error searching keyring".to_string(), + Box::new(e), + )) + } + }, + UnlockMechanism::ClevisInfo(_) => match clevis_decrypt(device, *ts) { + Ok(Some(pass)) => { + passphrases.push((*ts, pass)); + } + Ok(None) => { + return Err(StratisError::Msg( + "Error getting Clevis passphrase".to_string(), + )) + } + Err(e) => { + return Err(StratisError::Chained( + "Error getting Clevis passphrase".to_string(), + Box::new(e), + )) + } + }, + } + } + + Ok(passphrases) +} + +/// Sets up a reencryption operation. +/// +/// The setup includes: +/// * Generating a new volume key with no data segment associated +/// * Duplicating all of the existing keyslots and tokens to point at this volume key +/// * Returning a single existing key and new keyslot to use in the online reencryption operation +/// +/// Can be rolled back. +pub fn handle_setup_reencrypt( + luks2_path: &Path, + encryption_info: &EncryptionInfo, +) -> StratisResult<(u32, SizedKeyMemory, u32)> { + fn set_up_reencryption_token( + device: &mut CryptDevice, + new_keyslot: u32, + ts: u32, + mut token_contents: Value, + ) -> StratisResult<()> { + if let Some(obj) = token_contents.as_object_mut() { + let tokens = match obj.remove(TOKEN_KEYSLOTS_KEY) { + Some(Value::Array(mut v)) => { + v.push(Value::String(new_keyslot.to_string())); + Value::Array(v) + } + Some(_) | None => { + return Err(StratisError::Msg(format!( + "Could not find appropriate formatted value for {TOKEN_KEYSLOTS_KEY}" + ))); + } + }; + obj.insert(TOKEN_KEYSLOTS_KEY.to_string(), tokens); + } + device + .token_handle() + .json_set(TokenInput::ReplaceToken(ts, &token_contents))?; + + Ok(()) + } + + let mut device = acquire_crypt_device(luks2_path)?; + + let mut keys = get_all_passphrases(&mut device, encryption_info)?; + // Check required to avoid panic if there is only one unlock mechanism + let other_keys = if keys.len() < 2 { + vec![] + } else { + keys.split_off(1) + }; + let (single_ts, single_key) = keys + .pop() + .ok_or_else(|| StratisError::Msg("No unlock methods found".to_string()))?; + let single_token_contents = device.token_handle().json_get(single_ts)?; + let single_keyslot = get_keyslot_number(&mut device, single_ts)?.ok_or_else(|| { + StratisError::Msg(format!( + "Could not find keyslot associated with token slot {single_ts}" + )) + })?; + + let single_new_keyslot = device.keyslot_handle().add_by_key( + None, + Some(Either::Right(STRATIS_MEK_SIZE)), + single_key.as_ref(), + CryptVolumeKey::NO_SEGMENT, + )?; + + set_up_reencryption_token( + &mut device, + single_new_keyslot, + single_ts, + single_token_contents, + )?; + + let mut new_vk = SafeMemHandle::alloc(STRATIS_MEK_SIZE)?; + device.volume_key_handle().get( + Some(single_new_keyslot), + new_vk.as_mut(), + Some(single_key.as_ref()), + )?; + + for (ts, key) in other_keys { + let token_contents = device.token_handle().json_get(ts)?; + + let new_keyslot = device.keyslot_handle().add_by_key( + None, + Some(Either::Left(new_vk.as_ref())), + key.as_ref(), + CryptVolumeKey::NO_SEGMENT | CryptVolumeKey::DIGEST_REUSE, + )?; + set_up_reencryption_token(&mut device, new_keyslot, ts, token_contents)?; + } + + Ok((single_keyslot, single_key, single_new_keyslot)) +} + +/// Perform the online reencryption operation. +/// +/// Cannot be rolled back. +pub fn handle_do_reencrypt( + device_name: &str, + pool_uuid: PoolUuid, + luks2_path: &Path, + single_keyslot: u32, + single_key: SizedKeyMemory, + single_new_keyslot: u32, +) -> StratisResult<()> { + { + let mut device = acquire_crypt_device(luks2_path)?; + + let cipher = device.status_handle().get_cipher()?; + let cipher_mode = device.status_handle().get_cipher_mode()?; + let sector_size = convert_int!(get_sector_size(Some(&mut device)), i32, u32)?; + device.reencrypt_handle().reencrypt_init_by_passphrase( + Some(device_name), + single_key.as_ref(), + Some(single_keyslot), + Some(single_new_keyslot), + Some((&cipher, &cipher_mode)), + CryptParamsReencrypt { + mode: CryptReencryptModeInfo::Reencrypt, + direction: CryptReencryptDirectionInfo::Forward, + resilience: "checksum".to_string(), + hash: "sha256".to_string(), + data_shift: 0, + max_hotzone_size: 0, + device_size: 0, + luks2: Some(CryptParamsLuks2 { + data_alignment: 0, + data_device: None, + integrity: None, + integrity_params: None, + pbkdf: None, + label: None, + sector_size, + subsystem: None, + }), + flags: CryptReencrypt::empty(), + }, + )?; + } + + info!("Starting reencryption operation on pool with UUID {pool_uuid}; may take a while"); + // The corresponding libcryptsetup call is device.reencrypt_handle().reencrypt2::<()>(None, None)?; + cmd::run_reencrypt(luks2_path)?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/engine/strat_engine/engine.rs b/src/engine/strat_engine/engine.rs index e07ef86d59..cef4b402d7 100644 --- a/src/engine/strat_engine/engine.rs +++ b/src/engine/strat_engine/engine.rs @@ -27,7 +27,7 @@ use crate::{ backstore::ProcessedPathInfos, cmd::verify_executables, dm::get_dm, - keys::StratKeyActions, + keys::{validate_key_descs, StratKeyActions}, liminal::{find_all, DeviceSet, LiminalDevices}, names::KeyDescription, ns::MemoryFilesystem, @@ -108,6 +108,10 @@ impl StratEngine { blockdev_paths: &[&Path], encryption_info: Option<&InputEncryptionInfo>, ) -> StratisResult> { + if let Some(ei) = encryption_info { + validate_key_descs(ei.key_descs())?; + } + validate_name(name)?; let name = Name::new(name.to_owned()); @@ -185,6 +189,13 @@ impl StratEngine { } } + async fn upgrade_pool( + &self, + lock: SomeLockReadGuard, + ) -> SomeLockWriteGuard { + self.pools.upgrade(lock).await + } + pub async fn get_pool( &self, key: PoolIdentifier, @@ -519,6 +530,10 @@ impl Engine for StratEngine { encryption_info: Option<&InputEncryptionInfo>, integrity_spec: IntegritySpec, ) -> StratisResult> { + if let Some(ei) = encryption_info { + validate_key_descs(ei.key_descs())?; + } + validate_name(name)?; let name = Name::new(name.to_owned()); let integrity_spec = ValidatedIntegritySpec::try_from(integrity_spec)?; @@ -731,6 +746,13 @@ impl Engine for StratEngine { } } + async fn upgrade_pool( + &self, + lock: SomeLockReadGuard, + ) -> SomeLockWriteGuard { + self.upgrade_pool(lock).await.into_dyn() + } + async fn get_pool( &self, key: PoolIdentifier, diff --git a/src/engine/strat_engine/keys.rs b/src/engine/strat_engine/keys.rs index a8a74b49af..f74ecf5489 100644 --- a/src/engine/strat_engine/keys.rs +++ b/src/engine/strat_engine/keys.rs @@ -32,6 +32,19 @@ pub(super) fn search_key_persistent(key_desc: &KeyDescription) -> StratisResult< search_key(keyring_id, key_desc.to_system_string()) } +/// Validate that all key descriptions are in the persistent kernel keyring. +pub fn validate_key_descs<'a>(iter: impl Iterator) -> StratisResult<()> { + for key_desc in iter { + search_key_persistent(key_desc)?.ok_or_else(|| { + StratisError::Msg(format!( + "Key with key description {} not found in kernel keyring", + key_desc.as_application_str() + )) + })?; + } + Ok(()) +} + /// Search the process keyring for the given key description. pub(super) fn search_key_process( key_desc: &VolumeKeyKeyDescription, diff --git a/src/engine/strat_engine/pool/dispatch.rs b/src/engine/strat_engine/pool/dispatch.rs index 4ddd53ced1..44ead17a39 100644 --- a/src/engine/strat_engine/pool/dispatch.rs +++ b/src/engine/strat_engine/pool/dispatch.rs @@ -15,9 +15,10 @@ use crate::{ strat_engine::pool::{v1, v2}, types::{ ActionAvailability, BlockDevTier, Clevis, CreateAction, DeleteAction, DevUuid, - EncryptionInfo, FilesystemUuid, GrowAction, Key, KeyDescription, Name, - OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, PoolUuid, PropChangeAction, - RegenAction, RenameAction, SetCreateAction, SetDeleteAction, StratSigblockVersion, + EncryptedDevice, EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, Key, + KeyDescription, Name, OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, PoolUuid, + PropChangeAction, ReencryptedDevice, RegenAction, RenameAction, SetCreateAction, + SetDeleteAction, SizedKeyMemory, StratSigblockVersion, }, }, stratis::StratisResult, @@ -350,6 +351,89 @@ impl Pool for AnyPool { } } + fn start_encrypt_pool( + &mut self, + pool_uuid: PoolUuid, + encryption_info: &InputEncryptionInfo, + ) -> StratisResult> { + match self { + AnyPool::V1(p) => p.start_encrypt_pool(pool_uuid, encryption_info), + AnyPool::V2(p) => p.start_encrypt_pool(pool_uuid, encryption_info), + } + } + + fn do_encrypt_pool( + &self, + pool_uuid: PoolUuid, + sector_size: u32, + key_info: (u32, SizedKeyMemory), + ) -> StratisResult<()> { + match self { + AnyPool::V1(p) => p.do_encrypt_pool(pool_uuid, sector_size, key_info), + AnyPool::V2(p) => p.do_encrypt_pool(pool_uuid, sector_size, key_info), + } + } + + fn finish_encrypt_pool(&mut self, name: &Name, pool_uuid: PoolUuid) -> StratisResult<()> { + match self { + AnyPool::V1(p) => p.finish_encrypt_pool(name, pool_uuid), + AnyPool::V2(p) => p.finish_encrypt_pool(name, pool_uuid), + } + } + + fn start_reencrypt_pool(&mut self) -> StratisResult> { + match self { + AnyPool::V1(p) => p.start_reencrypt_pool(), + AnyPool::V2(p) => p.start_reencrypt_pool(), + } + } + + fn do_reencrypt_pool( + &self, + pool_uuid: PoolUuid, + key_info: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()> { + match self { + AnyPool::V1(p) => p.do_reencrypt_pool(pool_uuid, key_info), + AnyPool::V2(p) => p.do_reencrypt_pool(pool_uuid, key_info), + } + } + + fn finish_reencrypt_pool( + &mut self, + name: &Name, + pool_uuid: PoolUuid, + ) -> StratisResult { + match self { + AnyPool::V1(p) => p.finish_reencrypt_pool(name, pool_uuid), + AnyPool::V2(p) => p.finish_reencrypt_pool(name, pool_uuid), + } + } + + fn decrypt_pool_idem_check( + &mut self, + pool_uuid: PoolUuid, + ) -> StratisResult> { + match self { + AnyPool::V1(p) => p.decrypt_pool_idem_check(pool_uuid), + AnyPool::V2(p) => p.decrypt_pool_idem_check(pool_uuid), + } + } + + fn do_decrypt_pool(&self, pool_uuid: PoolUuid) -> StratisResult<()> { + match self { + AnyPool::V1(p) => p.do_decrypt_pool(pool_uuid), + AnyPool::V2(p) => p.do_decrypt_pool(pool_uuid), + } + } + + fn finish_decrypt_pool(&mut self, pool_uuid: PoolUuid, name: &Name) -> StratisResult<()> { + match self { + AnyPool::V1(p) => p.finish_decrypt_pool(pool_uuid, name), + AnyPool::V2(p) => p.finish_decrypt_pool(pool_uuid, name), + } + } + fn current_metadata(&self, pool_name: &Name) -> StratisResult { match self { AnyPool::V1(p) => p.current_metadata(pool_name), @@ -416,4 +500,11 @@ impl Pool for AnyPool { AnyPool::V2(p) => p.load_volume_key(uuid), } } + + fn last_reencrypt(&self) -> Option> { + match self { + AnyPool::V1(p) => p.last_reencrypt(), + AnyPool::V2(p) => p.last_reencrypt(), + } + } } diff --git a/src/engine/strat_engine/pool/v1.rs b/src/engine/strat_engine/pool/v1.rs index 5d1a21e574..b5035275ce 100644 --- a/src/engine/strat_engine/pool/v1.rs +++ b/src/engine/strat_engine/pool/v1.rs @@ -18,13 +18,10 @@ use devicemapper::{Bytes, DmNameBuf, Sectors}; use stratisd_proc_macros::strat_pool_impl_gen; #[cfg(any(test, feature = "extras"))] -use crate::engine::{ - strat_engine::{ - backstore::UnownedDevices, - metadata::MDADataSize, - thinpool::{ThinPoolSizeParams, DATA_BLOCK_SIZE}, - }, - types::InputEncryptionInfo, +use crate::engine::strat_engine::{ + backstore::UnownedDevices, + metadata::MDADataSize, + thinpool::{ThinPoolSizeParams, DATA_BLOCK_SIZE}, }; use crate::{ engine::{ @@ -40,6 +37,7 @@ use crate::{ ProcessedPathInfos, }, crypt::{handle::v1::CryptHandle, CLEVIS_LUKS_TOKEN_ID, LUKS2_TOKEN_ID}, + keys::{search_key_persistent, validate_key_descs}, liminal::DeviceSet, metadata::disown_device, serde_structs::{FlexDevsSave, PoolSave, Recordable}, @@ -47,12 +45,12 @@ use crate::{ }, types::{ ActionAvailability, BlockDevTier, Clevis, Compare, CreateAction, DeleteAction, DevUuid, - Diff, FilesystemUuid, GrowAction, Key, KeyDescription, Name, OptionalTokenSlotInput, - PoolDiff, PoolEncryptionInfo, PoolUuid, RegenAction, RenameAction, SetCreateAction, - SetDeleteAction, StratFilesystemDiff, StratPoolDiff, StratSigblockVersion, - TokenUnlockMethod, + Diff, EncryptedDevice, EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, + Key, KeyDescription, Name, OffsetDirection, OptionalTokenSlotInput, PoolDiff, + PoolEncryptionInfo, PoolUuid, PropChangeAction, ReencryptedDevice, RegenAction, + RenameAction, SetCreateAction, SetDeleteAction, SizedKeyMemory, StratFilesystemDiff, + StratPoolDiff, StratSigblockVersion, TokenUnlockMethod, }, - EncryptionInfo, PropChangeAction, }, stratis::{StratisError, StratisResult}, }; @@ -177,6 +175,7 @@ pub struct StratPool { thin_pool: ThinPool, action_avail: ActionAvailability, metadata_size: Sectors, + last_reencrypt: Option>, } #[strat_pool_impl_gen] @@ -191,6 +190,10 @@ impl StratPool { devices: UnownedDevices, encryption_info: Option<&InputEncryptionInfo>, ) -> StratisResult<(PoolUuid, StratPool)> { + if let Some(ei) = encryption_info { + validate_key_descs(ei.key_descs())?; + } + let pool_uuid = PoolUuid::new_v4(); // FIXME: Initializing with the minimum MDA size is not necessarily @@ -243,6 +246,7 @@ impl StratPool { thin_pool: thinpool, action_avail: ActionAvailability::Full, metadata_size, + last_reencrypt: None, }; pool.write_metadata(&Name::new(name.to_owned()))?; @@ -280,8 +284,8 @@ impl StratPool { if action_avail != ActionAvailability::Full { warn!( - "Disabling some actions for pool {pool_name} with UUID {uuid}; pool is designated {action_avail}" - ); + "Disabling some actions for pool {pool_name} with UUID {uuid}; pool is designated {action_avail}" + ); } let thinpool = ThinPool::setup( @@ -303,6 +307,7 @@ impl StratPool { thin_pool: thinpool, action_avail, metadata_size, + last_reencrypt: metadata.last_reencrypt, }; // The value of the started field in the pool metadata needs to be @@ -429,6 +434,7 @@ impl StratPool { thinpool_dev: self.thin_pool.record(), started: Some(true), features: vec![], + last_reencrypt: self.last_reencrypt, } } @@ -695,10 +701,14 @@ impl Pool for StratPool { ) .and_then(|bdi| { self.thin_pool - .set_device(self.backstore.device().expect( - "Since thin pool exists, space must have been allocated \ + .set_device( + self.backstore.device().expect( + "Since thin pool exists, space must have been allocated \ from the backstore, so backstore must have a cap device", - )) + ), + Sectors(0), + OffsetDirection::Forwards, + ) .and(Ok(bdi)) }); self.thin_pool.resume()?; @@ -764,6 +774,13 @@ impl Pool for StratPool { return Err(StratisError::Msg("Specifying the token slot for binding is not supported in V1 pools; please migrate to V2 pools to use this feature".to_string())); } + search_key_persistent(key_description)?.ok_or_else(|| { + StratisError::Msg(format!( + "Key with key description {} not found in keyring", + key_description.as_application_str() + )) + })?; + let changed = self.backstore.bind_keyring(key_description)?; if changed { Ok(CreateAction::Created((Key, LUKS2_TOKEN_ID))) @@ -783,6 +800,23 @@ impl Pool for StratPool { return Err(StratisError::Msg("Specifying the token slot for rebinding is not supported in V1 pools; please migrate to V2 pools to use this feature".to_string())); } + if let Some(ei) = self.encryption_info_legacy() { + if let Some(kd) = ei.key_description()? { + search_key_persistent(kd)?.ok_or_else(|| { + StratisError::Msg(format!( + "Key with key description {} not found in keyring", + kd.as_application_str() + )) + })?; + } + } + search_key_persistent(new_key_desc)?.ok_or_else(|| { + StratisError::Msg(format!( + "Key with key description {} not found in keyring", + new_key_desc.as_application_str() + )) + })?; + match self.backstore.rebind_keyring(new_key_desc)? { Some(true) => Ok(RenameAction::Renamed(Key)), Some(false) => Ok(RenameAction::Identity), @@ -1335,6 +1369,92 @@ impl Pool for StratPool { } } + #[pool_mutating_action("NoRequests")] + fn start_encrypt_pool( + &mut self, + _: PoolUuid, + _: &InputEncryptionInfo, + ) -> StratisResult> { + Err(StratisError::Msg( + "Encrypting an unencrypted device is only supported in V2 of the metadata".to_string(), + )) + } + + #[pool_mutating_action("NoRequests")] + fn do_encrypt_pool(&self, _: PoolUuid, _: u32, _: (u32, SizedKeyMemory)) -> StratisResult<()> { + Err(StratisError::Msg( + "Encrypting an unencrypted device is only supported in V2 of the metadata".to_string(), + )) + } + + #[pool_mutating_action("NoRequests")] + fn finish_encrypt_pool(&mut self, _: &Name, _: PoolUuid) -> StratisResult<()> { + Err(StratisError::Msg( + "Encrypting an unencrypted device is only supported in V2 of the metadata".to_string(), + )) + } + + #[pool_mutating_action("NoRequests")] + fn start_reencrypt_pool(&mut self) -> StratisResult> { + validate_key_descs(match self.encryption_info() { + Some(Either::Left(ref ei)) => Box::new(ei.all_key_descriptions().map(|(_, kd)| kd)) + as Box>, + Some(Either::Right(ref pei)) => Box::new(pei.key_description()?.into_iter()) + as Box>, + None => { + return Err(StratisError::Msg( + "Cannot reencrypt an unencrypted pool".to_string(), + )) + } + })?; + + self.backstore.prepare_reencrypt() + } + + #[pool_mutating_action("NoRequests")] + fn do_reencrypt_pool( + &self, + pool_uuid: PoolUuid, + key_info: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()> { + self.backstore.reencrypt(pool_uuid, key_info) + } + + #[pool_mutating_action("NoRequests")] + fn finish_reencrypt_pool( + &mut self, + name: &Name, + pool_uuid: PoolUuid, + ) -> StratisResult { + self.last_reencrypt = Some(Utc::now()); + self.write_metadata(name)?; + Ok(ReencryptedDevice(pool_uuid)) + } + + #[pool_mutating_action("NoRequests")] + fn decrypt_pool_idem_check( + &mut self, + _: PoolUuid, + ) -> StratisResult> { + Err(StratisError::Msg( + "Decrypting an encrypted device is only supported in V2 of the metadata".to_string(), + )) + } + + #[pool_mutating_action("NoRequests")] + fn do_decrypt_pool(&self, _: PoolUuid) -> StratisResult<()> { + Err(StratisError::Msg( + "Decrypting an encrypted device is only supported in V2 of the metadata".to_string(), + )) + } + + #[pool_mutating_action("NoRequests")] + fn finish_decrypt_pool(&mut self, _: PoolUuid, _: &Name) -> StratisResult<()> { + Err(StratisError::Msg( + "Decrypting an encrypted device is only supported in V2 of the metadata".to_string(), + )) + } + fn current_metadata(&self, pool_name: &Name) -> StratisResult { serde_json::to_string(&self.record(pool_name)).map_err(|e| e.into()) } @@ -1385,6 +1505,10 @@ impl Pool for StratPool { fn load_volume_key(&mut self, _: PoolUuid) -> StratisResult { Ok(false) } + + fn last_reencrypt(&self) -> Option> { + self.last_reencrypt + } } pub struct StratPoolState { @@ -1438,6 +1562,7 @@ impl DumpState<'_> for StratPool { #[cfg(test)] mod tests { use std::{ + env, fs::OpenOptions, io::{BufWriter, Read, Write}, }; @@ -1451,7 +1576,7 @@ mod tests { strat_engine::{ cmd::udev_settle, pool::AnyPool, - tests::{loopbacked, real}, + tests::{crypt, loopbacked, real}, thinpool::ThinPoolStatusDigest, }, types::{EngineAction, IntegritySpec, PoolIdentifier, TokenUnlockMethod}, @@ -2014,4 +2139,76 @@ mod tests { test_remove_cache, ); } + + /// Tests online reencryption functionality by performing online reencryption and then stopping and + /// starting the pool. + fn clevis_test_online_reencrypt(paths: &[&Path]) { + fn test_online_encrypt_with_key(paths: &[&Path], key_desc: &KeyDescription) { + let (send, _recv) = unbounded_channel(); + let engine = StratEngine::initialize(send).unwrap(); + + let pool_uuid = test_async!(engine.create_pool_legacy( + "encrypt_with_both", + paths, + InputEncryptionInfo::new( + vec![(Some(LUKS2_TOKEN_ID), key_desc.to_owned())], + vec![( + Some(CLEVIS_LUKS_TOKEN_ID), + ( + "tang".to_string(), + json!({ + "url": env::var("TANG_URL").expect("TANG_URL env var required"), + "stratis:tang:trust_url": true, + }), + ) + )] + ) + .unwrap() + .as_ref(), + )) + .unwrap() + .changed() + .unwrap(); + + { + let mut handle = + test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(pool_uuid))).unwrap(); + let (_, _, pool) = handle.as_mut_tuple(); + assert!(pool.is_encrypted()); + let key_info = pool.start_reencrypt_pool().unwrap(); + pool.do_reencrypt_pool(pool_uuid, key_info).unwrap(); + pool.finish_reencrypt_pool(&Name::new("encrypt_with_both".to_string()), pool_uuid) + .unwrap(); + assert!(pool.is_encrypted()); + } + + test_async!(engine.stop_pool(PoolIdentifier::Uuid(pool_uuid), true)).unwrap(); + test_async!(engine.start_pool( + PoolIdentifier::Uuid(pool_uuid), + TokenUnlockMethod::Any, + None, + false, + )) + .unwrap(); + test_async!(engine.destroy_pool(pool_uuid)).unwrap(); + } + + crypt::insert_and_cleanup_key(paths, test_online_encrypt_with_key); + } + + #[test] + fn clevis_loop_test_online_reencrypt() { + loopbacked::test_with_spec( + &loopbacked::DeviceLimits::Exactly(2, None), + clevis_test_online_reencrypt, + ); + } + + #[test] + fn clevis_real_test_online_reencrypt() { + real::test_with_spec( + &real::DeviceLimits::Exactly(2, None, None), + clevis_test_online_reencrypt, + ); + } } diff --git a/src/engine/strat_engine/pool/v2.rs b/src/engine/strat_engine/pool/v2.rs index 0f90e25a54..9dc69ee073 100644 --- a/src/engine/strat_engine/pool/v2.rs +++ b/src/engine/strat_engine/pool/v2.rs @@ -29,6 +29,8 @@ use crate::{ blockdev::{v2::StratBlockDev, InternalBlockDev}, ProcessedPathInfos, UnownedDevices, }, + crypt::DEFAULT_CRYPT_DATA_OFFSET_V2, + keys::{search_key_persistent, validate_key_descs}, liminal::DeviceSet, metadata::{disown_device, MDADataSize}, serde_structs::{FlexDevsSave, PoolFeatures, PoolSave, Recordable}, @@ -36,11 +38,12 @@ use crate::{ }, types::{ ActionAvailability, BlockDevTier, Clevis, Compare, CreateAction, DeleteAction, DevUuid, - Diff, EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, Key, - KeyDescription, Name, OptionalTokenSlotInput, PoolDiff, PoolEncryptionInfo, PoolUuid, - PropChangeAction, RegenAction, RenameAction, SetCreateAction, SetDeleteAction, - SizedKeyMemory, StratFilesystemDiff, StratPoolDiff, StratSigblockVersion, - TokenUnlockMethod, ValidatedIntegritySpec, + Diff, EncryptedDevice, EncryptionInfo, FilesystemUuid, GrowAction, InputEncryptionInfo, + Key, KeyDescription, Name, OffsetDirection, OptionalTokenSlotInput, PoolDiff, + PoolEncryptionInfo, PoolUuid, PropChangeAction, ReencryptedDevice, RegenAction, + RenameAction, SetCreateAction, SetDeleteAction, SizedKeyMemory, StratFilesystemDiff, + StratPoolDiff, StratSigblockVersion, TokenUnlockMethod, UnlockMechanism, + ValidatedIntegritySpec, }, }, stratis::{StratisError, StratisResult}, @@ -142,6 +145,7 @@ pub struct StratPool { thin_pool: ThinPool, action_avail: ActionAvailability, metadata_size: Sectors, + last_reencrypt: Option>, } #[strat_pool_impl_gen] @@ -156,6 +160,10 @@ impl StratPool { encryption_info: Option<&InputEncryptionInfo>, integrity_spec: ValidatedIntegritySpec, ) -> StratisResult<(PoolUuid, StratPool)> { + if let Some(ei) = encryption_info { + validate_key_descs(ei.key_descs())?; + } + let pool_uuid = PoolUuid::new_v4(); // FIXME: Initializing with the minimum MDA size is not necessarily @@ -208,6 +216,7 @@ impl StratPool { thin_pool: thinpool, action_avail: ActionAvailability::Full, metadata_size, + last_reencrypt: None, }; pool.write_metadata(&Name::new(name.to_owned()))?; @@ -271,6 +280,7 @@ impl StratPool { thin_pool: thinpool, action_avail, metadata_size, + last_reencrypt: metadata.last_reencrypt, }; // The value of the started field in the pool metadata needs to be @@ -391,6 +401,7 @@ impl StratPool { thinpool_dev: self.thin_pool.record(), started: Some(true), features, + last_reencrypt: self.last_reencrypt, } } @@ -687,6 +698,8 @@ impl Pool for StratPool { token_slot: OptionalTokenSlotInput, key_description: &KeyDescription, ) -> StratisResult> { + search_key_persistent(key_description)?; + let changed = self.backstore.bind_keyring(token_slot, key_description)?; match changed { Some(t) => { @@ -703,6 +716,41 @@ impl Pool for StratPool { token_slot: Option, new_key_desc: &KeyDescription, ) -> StratisResult> { + if let Some(e) = self.encryption_info() { + match e { + Either::Left(ei) => match token_slot { + Some(t) => { + let info = ei.get_info(t).ok_or_else(|| { + StratisError::Msg(format!( + "Failed to find key description for token slot {t}" + )) + })?; + match info { + UnlockMechanism::KeyDesc(kd) => { + search_key_persistent(kd)?; + } + _ => { + return Err(StratisError::Msg(format!( + "Token slot {t} is associated with a Clevis binding" + ))); + } + } + } + None => { + if let Some((_, kd)) = ei.single_key_description() { + search_key_persistent(kd)?; + } + } + }, + Either::Right(pei) => { + if let Some(kd) = pei.key_description()? { + search_key_persistent(kd)?; + } + } + } + } + search_key_persistent(new_key_desc)?; + match self.backstore.rebind_keyring(token_slot, new_key_desc)? { Some(true) => Ok(RenameAction::Renamed(Key)), Some(false) => Ok(RenameAction::Identity), @@ -1205,6 +1253,113 @@ impl Pool for StratPool { } } + #[pool_mutating_action("NoRequests")] + fn start_encrypt_pool( + &mut self, + pool_uuid: PoolUuid, + encryption_info: &InputEncryptionInfo, + ) -> StratisResult> { + validate_key_descs(encryption_info.key_descs())?; + + match self.backstore.encryption_info() { + Some(_) => Ok(CreateAction::Identity), + None => { + let offset = DEFAULT_CRYPT_DATA_OFFSET_V2; + let direction = OffsetDirection::Backwards; + self.backstore + .prepare_encrypt( + pool_uuid, + &mut self.thin_pool, + offset, + direction, + encryption_info, + ) + .map(CreateAction::Created) + } + } + } + + #[pool_mutating_action("NoRequests")] + fn do_encrypt_pool( + &self, + pool_uuid: PoolUuid, + sector_size: u32, + key_info: (u32, SizedKeyMemory), + ) -> StratisResult<()> { + Backstore::do_encrypt(pool_uuid, sector_size, key_info) + } + + #[pool_mutating_action("NoRequests")] + fn finish_encrypt_pool(&mut self, name: &Name, pool_uuid: PoolUuid) -> StratisResult<()> { + self.backstore.finish_encrypt(pool_uuid)?; + self.write_metadata(name)?; + Ok(()) + } + + #[pool_mutating_action("NoRequests")] + fn start_reencrypt_pool(&mut self) -> StratisResult> { + validate_key_descs(match self.encryption_info() { + Some(Either::Left(ref ei)) => Box::new(ei.all_key_descriptions().map(|(_, kd)| kd)) + as Box>, + Some(Either::Right(ref pei)) => Box::new(pei.key_description()?.into_iter()) + as Box>, + None => { + return Err(StratisError::Msg( + "Cannot reencrypt an unencrypted pool".to_string(), + )) + } + })?; + + self.backstore.prepare_reencrypt() + } + + #[pool_mutating_action("NoRequests")] + fn do_reencrypt_pool( + &self, + pool_uuid: PoolUuid, + key_info: Vec<(u32, SizedKeyMemory, u32)>, + ) -> StratisResult<()> { + self.backstore.reencrypt(pool_uuid, key_info) + } + + #[pool_mutating_action("NoRequests")] + fn finish_reencrypt_pool( + &mut self, + name: &Name, + pool_uuid: PoolUuid, + ) -> StratisResult { + self.last_reencrypt = Some(Utc::now()); + self.write_metadata(name)?; + Ok(ReencryptedDevice(pool_uuid)) + } + + #[pool_mutating_action("NoRequests")] + fn decrypt_pool_idem_check( + &mut self, + pool_uuid: PoolUuid, + ) -> StratisResult> { + match self.backstore.encryption_info() { + None => Ok(DeleteAction::Identity), + Some(_) => Ok(DeleteAction::Deleted(EncryptedDevice(pool_uuid))), + } + } + + #[pool_mutating_action("NoRequests")] + fn do_decrypt_pool(&self, pool_uuid: PoolUuid) -> StratisResult<()> { + self.backstore.do_decrypt(pool_uuid) + } + + #[pool_mutating_action("NoRequests")] + fn finish_decrypt_pool(&mut self, pool_uuid: PoolUuid, name: &Name) -> StratisResult<()> { + let offset = DEFAULT_CRYPT_DATA_OFFSET_V2; + let direction = OffsetDirection::Forwards; + self.backstore + .finish_decrypt(pool_uuid, &mut self.thin_pool, offset, direction)?; + self.last_reencrypt = None; + self.write_metadata(name)?; + Ok(()) + } + fn current_metadata(&self, pool_name: &Name) -> StratisResult { serde_json::to_string(&self.record(pool_name)).map_err(|e| e.into()) } @@ -1257,6 +1412,10 @@ impl Pool for StratPool { fn load_volume_key(&mut self, uuid: PoolUuid) -> StratisResult { Backstore::load_volume_key(uuid) } + + fn last_reencrypt(&self) -> Option> { + self.last_reencrypt + } } pub struct StratPoolState { @@ -2077,4 +2236,236 @@ mod tests { test_remove_cache, ); } + + /// Tests online encryption functionality by performing online encryption and then stopping and + /// starting the pool. + fn clevis_test_online_encrypt(paths: &[&Path]) { + fn test_online_encrypt_with_key(paths: &[&Path], key_desc: &KeyDescription) { + unshare_mount_namespace().unwrap(); + + let (send, _recv) = unbounded_channel(); + let engine = StratEngine::initialize(send).unwrap(); + + let pool_uuid = test_async!(engine.create_pool( + "encrypt_with_both", + paths, + None, + IntegritySpec::default(), + )) + .unwrap() + .changed() + .unwrap(); + + { + let mut handle = + test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(pool_uuid))).unwrap(); + assert!(!handle.is_encrypted()); + let (sector_size, key_info) = handle.start_encrypt_pool( + pool_uuid, + &InputEncryptionInfo::new( + vec![(None, key_desc.to_owned())], + vec![( + None, + ( + "tang".to_string(), + json!({ + "url": env::var("TANG_URL").expect("TANG_URL env var required"), + "stratis:tang:trust_url": true, + }), + ), + )], + ) + .unwrap() + .unwrap(), + ) + .unwrap() + .changed() + .unwrap(); + let handle = handle.downgrade(); + handle + .do_encrypt_pool(pool_uuid, sector_size, key_info) + .unwrap(); + let mut handle = test_async!(engine.upgrade_pool(handle.into_dyn())); + let (name, _, _) = handle.as_mut_tuple(); + handle.finish_encrypt_pool(&name, pool_uuid).unwrap(); + assert!(handle.is_encrypted()); + } + + test_async!(engine.stop_pool(PoolIdentifier::Uuid(pool_uuid), true)).unwrap(); + test_async!(engine.start_pool( + PoolIdentifier::Uuid(pool_uuid), + TokenUnlockMethod::Any, + None, + false, + )) + .unwrap(); + test_async!(engine.destroy_pool(pool_uuid)).unwrap(); + } + + crypt::insert_and_cleanup_key(paths, test_online_encrypt_with_key); + } + + #[test] + fn clevis_loop_test_online_encrypt() { + loopbacked::test_with_spec( + &loopbacked::DeviceLimits::Exactly(2, None), + clevis_test_online_encrypt, + ); + } + + #[test] + fn clevis_real_test_online_encrypt() { + real::test_with_spec( + &real::DeviceLimits::Exactly(2, None, None), + clevis_test_online_encrypt, + ); + } + + /// Tests online reencryption functionality by performing online reencryption and then stopping and + /// starting the pool. + fn clevis_test_online_reencrypt(paths: &[&Path]) { + fn test_online_encrypt_with_key(paths: &[&Path], key_desc: &KeyDescription) { + unshare_mount_namespace().unwrap(); + + let (send, _recv) = unbounded_channel(); + let engine = StratEngine::initialize(send).unwrap(); + + let pool_uuid = test_async!(engine.create_pool( + "encrypt_with_both", + paths, + InputEncryptionInfo::new( + vec![(None, key_desc.to_owned())], + vec![( + None, + ( + "tang".to_string(), + json!({ + "url": env::var("TANG_URL").expect("TANG_URL env var required"), + "stratis:tang:trust_url": true, + }), + ) + )] + ) + .unwrap() + .as_ref(), + IntegritySpec::default(), + )) + .unwrap() + .changed() + .unwrap(); + + { + let mut handle = + test_async!(engine.get_mut_pool(PoolIdentifier::Uuid(pool_uuid))).unwrap(); + let (_, _, pool) = handle.as_mut_tuple(); + assert!(pool.is_encrypted()); + let key_info = pool.start_reencrypt_pool().unwrap(); + pool.do_reencrypt_pool(pool_uuid, key_info).unwrap(); + pool.finish_reencrypt_pool(&Name::new("encrypt_with_both".to_string()), pool_uuid) + .unwrap(); + assert!(pool.is_encrypted()); + } + + test_async!(engine.stop_pool(PoolIdentifier::Uuid(pool_uuid), true)).unwrap(); + test_async!(engine.start_pool( + PoolIdentifier::Uuid(pool_uuid), + TokenUnlockMethod::Any, + None, + false, + )) + .unwrap(); + test_async!(engine.destroy_pool(pool_uuid)).unwrap(); + } + + crypt::insert_and_cleanup_key(paths, test_online_encrypt_with_key); + } + + #[test] + fn clevis_loop_test_online_reencrypt() { + loopbacked::test_with_spec( + &loopbacked::DeviceLimits::Exactly(2, None), + clevis_test_online_reencrypt, + ); + } + + #[test] + fn clevis_real_test_online_reencrypt() { + real::test_with_spec( + &real::DeviceLimits::Exactly(2, None, None), + clevis_test_online_reencrypt, + ); + } + + /// Tests online encryption functionality by performing online encryption and then stopping and + /// starting the pool. + fn clevis_test_online_decrypt(paths: &[&Path]) { + fn test_online_encrypt_with_key(paths: &[&Path], key_desc: &KeyDescription) { + unshare_mount_namespace().unwrap(); + + let (send, _recv) = unbounded_channel(); + let engine = StratEngine::initialize(send).unwrap(); + + let pool_uuid = test_async!(engine.create_pool( + "encrypt_with_both", + paths, + InputEncryptionInfo::new( + vec![(None, key_desc.to_owned())], + vec![( + None, + ( + "tang".to_string(), + json!({ + "url": env::var("TANG_URL").expect("TANG_URL env var required"), + "stratis:tang:trust_url": true, + }), + ), + )], + ) + .unwrap() + .as_ref(), + IntegritySpec::default(), + )) + .unwrap() + .changed() + .unwrap(); + + { + let handle = test_async!(engine.get_pool(PoolIdentifier::Uuid(pool_uuid))).unwrap(); + assert!(handle.is_encrypted()); + handle.do_decrypt_pool(pool_uuid).unwrap(); + let (name, _, _) = handle.as_tuple(); + let mut handle = test_async!(engine.upgrade_pool(handle.into_dyn())); + handle.finish_decrypt_pool(pool_uuid, &name).unwrap(); + assert!(!handle.is_encrypted()); + } + + test_async!(engine.stop_pool(PoolIdentifier::Uuid(pool_uuid), true)).unwrap(); + test_async!(engine.start_pool( + PoolIdentifier::Uuid(pool_uuid), + TokenUnlockMethod::None, + None, + false, + )) + .unwrap(); + test_async!(engine.destroy_pool(pool_uuid)).unwrap(); + } + + crypt::insert_and_cleanup_key(paths, test_online_encrypt_with_key); + } + + #[test] + fn clevis_loop_test_online_decrypt() { + loopbacked::test_with_spec( + &loopbacked::DeviceLimits::Exactly(2, None), + clevis_test_online_decrypt, + ); + } + + #[test] + fn clevis_real_test_online_decrypt() { + real::test_with_spec( + &real::DeviceLimits::Exactly(2, None, None), + clevis_test_online_decrypt, + ); + } } diff --git a/src/engine/strat_engine/serde_structs.rs b/src/engine/strat_engine/serde_structs.rs index 3aa9a2760d..ad7f3ead6a 100644 --- a/src/engine/strat_engine/serde_structs.rs +++ b/src/engine/strat_engine/serde_structs.rs @@ -12,7 +12,9 @@ // can convert to or from them when saving our current state, or // restoring state from saved metadata. -use serde::{Serialize, Serializer}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; use devicemapper::{Sectors, ThinDevId}; @@ -91,6 +93,30 @@ impl From> for Features { } } +fn serialize_date_time( + timestamp: &Option>, + serializer: S, +) -> Result +where + S: Serializer, +{ + serializer.serialize_i64(timestamp.expect("is some").timestamp()) +} + +fn deserialize_date_time<'a, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'a>, +{ + match Value::deserialize(deserializer) { + Ok(Value::Number(n)) => Ok(DateTime::::from_timestamp( + n.as_i64() + .ok_or_else(|| serde::de::Error::custom("Invalid integer type"))?, + 0, + )), + _ => Err(serde::de::Error::custom("Invalid data type")), + } +} + // ALL structs that represent variable length metadata in pre-order // depth-first traversal order. Note that when organized by types rather than // values the structure is a DAG not a tree. This just means that there are @@ -108,6 +134,11 @@ pub struct PoolSave { #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] pub features: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + #[serde(serialize_with = "serialize_date_time")] + #[serde(deserialize_with = "deserialize_date_time")] + pub last_reencrypt: Option>, } #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/engine/strat_engine/thinpool/dm_structs.rs b/src/engine/strat_engine/thinpool/dm_structs.rs index f79709e681..c50d1d2135 100644 --- a/src/engine/strat_engine/thinpool/dm_structs.rs +++ b/src/engine/strat_engine/thinpool/dm_structs.rs @@ -80,6 +80,8 @@ pub mod linear_table { LinearTargetParams, Sectors, TargetLine, }; + use crate::engine::types::OffsetDirection; + /// Transform a list of segments belonging to a single device into a /// list of target lines for a linear device. pub fn segs_to_table( @@ -106,33 +108,46 @@ pub mod linear_table { pub fn set_target_device( table: &LinearDevTargetTable, device: Device, + offset: Sectors, + offset_direction: OffsetDirection, ) -> Vec> { - let xform_target_line = - |line: &TargetLine| -> TargetLine { - let new_params = match line.params { - LinearDevTargetParams::Linear(ref params) => LinearDevTargetParams::Linear( - LinearTargetParams::new(device, params.start_offset), - ), - LinearDevTargetParams::Flakey(ref params) => { - let feature_args = params.feature_args.iter().cloned().collect::>(); - LinearDevTargetParams::Flakey(FlakeyTargetParams::new( - device, - params.start_offset, - params.up_interval, - params.down_interval, - feature_args, - )) - } - }; - - TargetLine::new(line.start, line.length, new_params) + let xform_target_line = |line: &TargetLine, + offset, + offset_direction| + -> TargetLine { + let new_params = match line.params { + LinearDevTargetParams::Linear(ref params) => { + LinearDevTargetParams::Linear(LinearTargetParams::new( + device, + match offset_direction { + OffsetDirection::Forwards => params.start_offset + offset, + OffsetDirection::Backwards => params.start_offset - offset, + }, + )) + } + LinearDevTargetParams::Flakey(ref params) => { + let feature_args = params.feature_args.iter().cloned().collect::>(); + LinearDevTargetParams::Flakey(FlakeyTargetParams::new( + device, + match offset_direction { + OffsetDirection::Forwards => params.start_offset + offset, + OffsetDirection::Backwards => params.start_offset - offset, + }, + params.up_interval, + params.down_interval, + feature_args, + )) + } }; + TargetLine::new(line.start, line.length, new_params) + }; + table .table .clone() .iter() - .map(&xform_target_line) + .map(|line| xform_target_line(line, offset, offset_direction)) .collect::>() } } diff --git a/src/engine/strat_engine/thinpool/filesystem.rs b/src/engine/strat_engine/thinpool/filesystem.rs index bd7558d258..e10259d101 100644 --- a/src/engine/strat_engine/thinpool/filesystem.rs +++ b/src/engine/strat_engine/thinpool/filesystem.rs @@ -29,6 +29,7 @@ use crate::{ engine::{DumpState, Filesystem, StateDiff}, shared::unsigned_to_timestamp, strat_engine::{ + backstore::get_logical_sector_size, cmd::{create_fs, set_uuid, xfs_growfs}, devlinks, dm::{get_dm, thin_device}, @@ -453,6 +454,11 @@ impl StratFilesystem { pub fn thin_id(&self) -> ThinDevId { self.thin_dev.id() } + + /// Get the sector size reported by libblkid for this filesystem. + pub fn logical_sector_size(&self) -> StratisResult { + get_logical_sector_size(&self.devnode()) + } } impl Filesystem for StratFilesystem { diff --git a/src/engine/strat_engine/thinpool/thinpool.rs b/src/engine/strat_engine/thinpool/thinpool.rs index 31603b4fc4..01973e2ca3 100644 --- a/src/engine/strat_engine/thinpool/thinpool.rs +++ b/src/engine/strat_engine/thinpool/thinpool.rs @@ -45,8 +45,8 @@ use crate::{ }, structures::Table, types::{ - Compare, Diff, FilesystemUuid, Name, PoolUuid, SetDeleteAction, StratFilesystemDiff, - ThinPoolDiff, + ActionAvailability, Compare, Diff, FilesystemUuid, Name, OffsetDirection, PoolUuid, + SetDeleteAction, StratFilesystemDiff, ThinPoolDiff, }, }, stratis::{StratisError, StratisResult}, @@ -682,6 +682,17 @@ impl ThinPool { ) .map_err(|e| e.into()) } + + /// Get the minimum logical sector size for all filesystems in the thin pool or return None if + /// there are no filesystems. + pub fn min_logical_sector_size(&self) -> StratisResult> { + let sectors = self + .filesystems() + .iter() + .map(|(_, _, fs)| fs.logical_sector_size()) + .collect::>>()?; + Ok(sectors.iter().min().cloned()) + } } impl ThinPool { @@ -804,28 +815,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 meta_table = - linear_table::set_target_device(self.thin_pool.meta_dev().table(), backstore_device); - let data_table = - linear_table::set_target_device(self.thin_pool.data_dev().table(), backstore_device); - let mdv_table = - linear_table::set_target_device(self.mdv.device().table(), backstore_device); - - 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 { @@ -1918,6 +1907,110 @@ where Ok(SetDeleteAction::new(removed, updated_origins)) } + + /// Set the device on all DM devices + pub fn set_device( + &mut self, + backstore_device: Device, + offset: Sectors, + offset_direction: OffsetDirection, + ) -> StratisResult { + fn apply_offset( + start: &mut Sectors, + offset: Sectors, + offset_direction: OffsetDirection, + ) -> StratisResult<()> { + match offset_direction { + OffsetDirection::Forwards => { + *start = start.checked_add(offset).ok_or_else(|| { + StratisError::Msg(format!("Allocation shift would overflow integer, start: {start}, offset: {offset}")) + })?; + } + OffsetDirection::Backwards => { + *start = Sectors(start.checked_sub(*offset).ok_or_else(|| { + StratisError::Msg(format!("Allocation shift would underflow integer, start: {start}, offset: {offset}")) + })?); + } + } + Ok(()) + } + + if backstore_device == self.backstore_device { + return Ok(false); + } + + let original_meta = self.thin_pool.meta_dev().table().clone(); + let original_data = self.thin_pool.data_dev().table().clone(); + + let meta_table = linear_table::set_target_device( + self.thin_pool.meta_dev().table(), + backstore_device, + offset, + offset_direction, + ); + let data_table = linear_table::set_target_device( + self.thin_pool.data_dev().table(), + backstore_device, + offset, + offset_direction, + ); + let mdv_table = linear_table::set_target_device( + self.mdv.device().table(), + backstore_device, + offset, + offset_direction, + ); + + self.thin_pool.set_meta_table(get_dm(), meta_table)?; + if let Err(e) = self.thin_pool.set_data_table(get_dm(), data_table) { + match self.thin_pool.set_meta_table(get_dm(), original_meta.table) { + Ok(_) => return Err(StratisError::from(e)), + Err(rollback_e) => { + return Err(StratisError::RollbackError { + causal_error: Box::new(StratisError::from(e)), + rollback_error: Box::new(StratisError::from(rollback_e)), + level: ActionAvailability::NoPoolChanges, + }) + } + } + } + if let Err(e) = self.mdv.set_table(mdv_table) { + if let Err(rollback_e) = self.thin_pool.set_meta_table(get_dm(), original_meta.table) { + return Err(StratisError::RollbackError { + causal_error: Box::new(e), + rollback_error: Box::new(StratisError::from(rollback_e)), + level: ActionAvailability::NoPoolChanges, + }); + } + match self.thin_pool.set_data_table(get_dm(), original_data.table) { + Ok(_) => return Err(e), + Err(rollback_e) => { + return Err(StratisError::RollbackError { + causal_error: Box::new(e), + rollback_error: Box::new(StratisError::from(rollback_e)), + level: ActionAvailability::NoPoolChanges, + }) + } + } + } + + for (start, _) in self.segments.mdv_segments.iter_mut() { + apply_offset(start, offset, offset_direction)?; + } + for (start, _) in self.segments.data_segments.iter_mut() { + apply_offset(start, offset, offset_direction)?; + } + for (start, _) in self.segments.meta_segments.iter_mut() { + apply_offset(start, offset, offset_direction)?; + } + for (start, _) in self.segments.meta_spare_segments.iter_mut() { + apply_offset(start, offset, offset_direction)?; + } + + self.backstore_device = backstore_device; + + Ok(true) + } } impl Into for &ThinPool { @@ -2868,7 +2961,8 @@ mod tests { .device() .expect("Space already allocated from backstore, backstore must have device"); assert_ne!(old_device, new_device); - pool.set_device(new_device).unwrap(); + pool.set_device(new_device, Sectors(0), OffsetDirection::Forwards) + .unwrap(); pool.resume().unwrap(); let mut buf = [0u8; 10]; diff --git a/src/engine/structures/lock.rs b/src/engine/structures/lock.rs index ae8d94004e..3688f0fa1c 100644 --- a/src/engine/structures/lock.rs +++ b/src/engine/structures/lock.rs @@ -213,6 +213,17 @@ where trace!("Lock record after acquisition: {self}"); } + /// Add a record for a single element indicating a write lock acquisition. + fn upgrade_to_write_lock(&mut self, uuid: U, idx: Option) { + self.write_locked.insert(uuid); + + if let Some(i) = idx { + self.pre_acquire_assertion(&WaitType::Upgrade(uuid), i); + } + + trace!("Lock record after acquisition: {self}"); + } + /// Remove a record for a single element indicating a write lock acquisition. /// Precondition: Exactly one write lock must be acquired on the given element. fn remove_write_lock(&mut self, uuid: &U) { @@ -313,6 +324,11 @@ where /// Determines whether two requests conflict. fn conflicts(already_woken: &WaitType, ty: &WaitType) -> bool { match (already_woken, ty) { + ( + WaitType::Upgrade(uuid1), + WaitType::SomeRead(uuid2) | WaitType::SomeWrite(uuid2) | WaitType::Upgrade(uuid2), + ) => uuid1 == uuid2, + (WaitType::Upgrade(_), WaitType::AllRead | WaitType::AllWrite) => true, (WaitType::SomeRead(_), WaitType::SomeRead(_) | WaitType::AllRead) => false, (WaitType::SomeRead(uuid1), WaitType::SomeWrite(uuid2)) => uuid1 == uuid2, (WaitType::SomeRead(_), _) => true, @@ -347,6 +363,12 @@ where || self.all_read_locked > 0 || self.all_write_locked } + WaitType::Upgrade(uuid) => { + self.read_locked.get(uuid).unwrap_or(&0) > &1 + || self.write_locked.contains(uuid) + || self.all_read_locked > 0 + || self.all_write_locked + } WaitType::AllRead => !self.write_locked.is_empty() || self.all_write_locked, WaitType::AllWrite => { !self.read_locked.is_empty() @@ -435,6 +457,7 @@ where /// A record of the type of a waiting request. #[derive(Debug, PartialEq)] enum WaitType { + Upgrade(U), SomeRead(U), SomeWrite(U), AllRead, @@ -582,6 +605,15 @@ where guard } + /// Upgrade a read lock to a write lock. + pub async fn upgrade(&self, lock: SomeLockReadGuard) -> SomeLockWriteGuard { + trace!("Upgrading single read lock to write lock"); + let idx = self.next_idx(); + let guard = Upgrade(self.clone(), lock, idx).await; + trace!("Read lock upgraded"); + guard + } + /// Issue a write on a single element identified by a name or UUID. pub async fn write(&self, key: PoolIdentifier) -> Option> { trace!("Acquiring write lock on pool {key:?}"); @@ -631,6 +663,60 @@ where } } +/// Future returned by AllOrSomeLock::upgrade(). +struct Upgrade(AllOrSomeLock, SomeLockReadGuard, u64); + +impl Future for Upgrade +where + U: AsUuid, +{ + type Output = SomeLockWriteGuard; + + fn poll(self: Pin<&mut Self>, cxt: &mut Context<'_>) -> Poll { + let (mut lock_record, inner) = self.0.acquire_mutex(); + + let wait_type = WaitType::Upgrade(self.1 .1); + let poll = if lock_record.should_wait(&wait_type, self.2) { + lock_record.add_waiter( + &AtomicBool::new(true), + wait_type, + cxt.waker().clone(), + self.2, + ); + Poll::Pending + } else { + lock_record.upgrade_to_write_lock(self.1 .1, Some(self.2)); + let (_, rf) = unsafe { inner.get().as_mut() } + .expect("cannot be null") + .get_mut_by_uuid(self.1 .1) + .expect("Checked above"); + Poll::Ready(SomeLockWriteGuard( + Arc::clone(&self.1 .0), + self.1 .1, + self.1 .2.clone(), + rf, + true, + )) + }; + + poll + } +} + +impl Drop for Upgrade +where + U: AsUuid, +{ + fn drop(&mut self) { + let mut lock_record = self + .0 + .lock_record + .lock() + .expect("Mutex only locked internally"); + lock_record.cancel(self.2); + } +} + /// Future returned by AllOrSomeLock::read(). struct SomeRead(AllOrSomeLock, PoolIdentifier, AtomicBool, u64); @@ -848,6 +934,15 @@ where unsafe { self.3.as_mut() }.expect("Cannot create null pointer from Rust references"), ) } + + pub fn downgrade(mut self) -> SomeLockReadGuard { + let mut lock_record = self.0.lock().unwrap(); + lock_record.write_locked.remove(&self.1); + lock_record.read_locked.insert(self.1, 1); + lock_record.wake(); + self.4 = false; + SomeLockReadGuard(Arc::clone(&self.0), self.1, self.2.clone(), self.3, true) + } } impl SomeLockWriteGuard diff --git a/src/engine/types/actions.rs b/src/engine/types/actions.rs index 872096ff5e..414cb1cb88 100644 --- a/src/engine/types/actions.rs +++ b/src/engine/types/actions.rs @@ -23,6 +23,9 @@ pub struct Key; /// Return value indicating clevis operation pub struct Clevis; +/// Return value indicating an encrypt operation on the pool +pub struct EncryptedDevice(pub PoolUuid); + /// A trait for a generic kind of action. Defines the type of the thing to /// be changed, and also a method to indicate what changed. pub trait EngineAction { @@ -137,6 +140,22 @@ where } } +impl Display for CreateAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CreateAction::Created(EncryptedDevice(uuid)) => { + write!( + f, + "Unencrypted pool with UUID {uuid} successfully encrypted" + ) + } + CreateAction::Identity => { + write!(f, "The requested pool was already encrypted") + } + } + } +} + /// Idempotent type representing a create action for a mapping from a key to a value #[derive(Debug, PartialEq, Eq)] pub enum MappingCreateAction { @@ -574,6 +593,19 @@ impl Display for DeleteAction { } } +impl Display for DeleteAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DeleteAction::Deleted(EncryptedDevice(uuid)) => { + write!(f, "Encrypted pool with UUID {uuid} successfully decrypted") + } + DeleteAction::Identity => { + write!(f, "The requested pool was already decrypted") + } + } + } +} + /// An action which may delete multiple things. /// This action may also cause other values to require updating. #[derive(Debug, PartialEq, Eq)] @@ -834,3 +866,16 @@ impl EngineAction for PropChangeAction { } } } + +/// Return value indicating a successful reencrypt operation on the pool +pub struct ReencryptedDevice(pub PoolUuid); + +impl Display for ReencryptedDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let uuid = self.0; + write!( + f, + "Reencryption operation on pool with UUID {uuid} was completed successfully" + ) + } +} diff --git a/src/engine/types/keys.rs b/src/engine/types/keys.rs index 3728a13068..74ae35e9fc 100644 --- a/src/engine/types/keys.rs +++ b/src/engine/types/keys.rs @@ -245,6 +245,15 @@ impl InputEncryptionInfo { clevis_infos_with_token_id, )) } + + pub fn key_descs(&self) -> impl Iterator { + self.encryption_infos + .iter() + .filter_map(|(_, enc)| match enc { + UnlockMechanism::KeyDesc(k) => Some(k), + _ => None, + }) + } } impl From for InputEncryptionInfo { diff --git a/src/engine/types/mod.rs b/src/engine/types/mod.rs index f98a3ff679..a0c83b3e3e 100644 --- a/src/engine/types/mod.rs +++ b/src/engine/types/mod.rs @@ -28,10 +28,10 @@ pub use crate::{ structures::Lockable, types::{ actions::{ - Clevis, CreateAction, DeleteAction, EngineAction, GrowAction, Key, - MappingCreateAction, MappingDeleteAction, PropChangeAction, RegenAction, - RenameAction, SetCreateAction, SetDeleteAction, SetUnlockAction, StartAction, - StopAction, ToDisplay, + Clevis, CreateAction, DeleteAction, EncryptedDevice, EngineAction, GrowAction, Key, + MappingCreateAction, MappingDeleteAction, PropChangeAction, ReencryptedDevice, + RegenAction, RenameAction, SetCreateAction, SetDeleteAction, SetUnlockAction, + StartAction, StopAction, ToDisplay, }, diff::{ Compare, Diff, PoolDiff, StratBlockDevDiff, StratFilesystemDiff, StratPoolDiff, @@ -495,6 +495,37 @@ pub enum StratSigblockVersion { V2 = 2, } +impl TryFrom for StratSigblockVersion { + type Error = StratisError; + + fn try_from(value: u8) -> Result { + match value { + 1u8 => Ok(StratSigblockVersion::V1), + 2u8 => Ok(StratSigblockVersion::V2), + _ => Err(StratisError::Msg(format!( + "Unknown sigblock version: {value}" + ))), + } + } +} + +impl From for u8 { + fn from(version: StratSigblockVersion) -> Self { + match version { + StratSigblockVersion::V1 => 1u8, + StratSigblockVersion::V2 => 2u8, + } + } +} + +#[derive(Clone, Copy)] +pub enum OffsetDirection { + /// Subtract the offset from the current offset. + Backwards, + /// Add the offset to the current offset. + Forwards, +} + /// A way to specify an integrity tag size. It is possible for the specification /// to be non-numeric but translatable to some number of bits. #[derive(