diff --git a/confidential-data-hub/storage/Cargo.toml b/confidential-data-hub/storage/Cargo.toml index 9fc1e8773..bce3553ec 100644 --- a/confidential-data-hub/storage/Cargo.toml +++ b/confidential-data-hub/storage/Cargo.toml @@ -10,6 +10,7 @@ anyhow.workspace = true async-trait.workspace = true base64.workspace = true log.workspace = true +kms = { path = "../kms", default-features = false } rand = { workspace = true, optional = true } secret = { path = "../secret" } serde.workspace = true diff --git a/confidential-data-hub/storage/src/error.rs b/confidential-data-hub/storage/src/error.rs index 113ed610e..6693b4375 100644 --- a/confidential-data-hub/storage/src/error.rs +++ b/confidential-data-hub/storage/src/error.rs @@ -15,6 +15,9 @@ pub enum Error { #[error("Error when mounting Aliyun OSS")] AliyunOssError(#[from] volume_type::aliyun::error::AliyunError), + #[error("Error when mounting Block device")] + BlockDeviceError(#[from] volume_type::blockdevice::error::BlockDeviceError), + #[error("Failed to recognize the storage type")] StorageTypeNotRecognized(#[from] strum::ParseError), } diff --git a/confidential-data-hub/storage/src/volume_type/blockdevice/error.rs b/confidential-data-hub/storage/src/volume_type/blockdevice/error.rs new file mode 100644 index 000000000..dd11fda41 --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/blockdevice/error.rs @@ -0,0 +1,29 @@ +// Copyright (c) 2024 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// + +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum BlockDeviceError { + #[error("Error when getting encrypt/decrypt keys")] + GetKeysFailure(#[from] anyhow::Error), + + #[error("LUKS decryption mount failed")] + LUKSfsMountFailed, + + #[error("I/O error")] + IOError(#[from] std::io::Error), + + #[error("Failed to mount block device")] + BlockDeviceMountFailed, + + #[error("Serialize/Deserialize failed")] + SerdeError(#[from] serde_json::Error), + + #[error("Failed to recognize the storage type")] + StorageTypeNotRecognized(#[from] strum::ParseError), +} diff --git a/confidential-data-hub/storage/src/volume_type/blockdevice/mod.rs b/confidential-data-hub/storage/src/volume_type/blockdevice/mod.rs new file mode 100644 index 000000000..92e9e83fc --- /dev/null +++ b/confidential-data-hub/storage/src/volume_type/blockdevice/mod.rs @@ -0,0 +1,173 @@ +// Copyright (c) 2024 Intel +// +// SPDX-License-Identifier: Apache-2.0 +// +pub mod error; + +use super::SecureMount; +use anyhow::Context; +use async_trait::async_trait; +use base64::Engine; +use error::{BlockDeviceError, Result}; +use kms::{Annotations, ProviderSettings}; +use log::{debug, error}; +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use strum::{Display, EnumString}; +use tokio::{ + fs, + io::{AsyncReadExt, AsyncWriteExt}, + process::Command, +}; + +/// LUKS encrypt storage binary +const LUKS_ENCRYPT_STORAGE_BIN: &str = "/usr/local/bin/luks-encrypt-storage"; + +#[derive(EnumString, Serialize, Deserialize, Display, Debug, PartialEq, Eq)] +pub enum BlockDeviceEncryptType { + #[strum(serialize = "luks")] + LUKS, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +struct BlockDeviceParameters { + /// The device number, formatted as "MAJ:MIN". + #[serde(rename = "deviceId")] + pub device_id: String, + + /// The encryption type. Currently, only LUKS is supported. + #[serde(rename = "encryptType")] + pub encryption_type: BlockDeviceEncryptType, + + /// Encryption key. If not set, generate a random 4096-byte key + #[serde(rename = "encryptKey")] + pub encryption_key: Option, + + /// Indicates whether to enable dm-integrity. + #[serde(rename = "dataIntegrity")] + pub data_integrity: String, +} +pub(crate) struct BlockDevice; + +async fn random_encrypt_key() -> anyhow::Result { + let mut buffer = vec![0u8; 4096]; + rand::thread_rng().fill(&mut buffer[..]); + Ok(base64::engine::general_purpose::STANDARD.encode(&buffer)) +} + +async fn get_plaintext_key(resource: &str) -> anyhow::Result { + if resource.starts_with("sealed.") { + debug!("detected sealed secret"); + let unsealed = secret::unseal_secret(resource.as_bytes()).await?; + return String::from_utf8(unsealed).context("convert to String failed"); + } + + if resource.starts_with("kbs://") { + let secret = kms::new_getter("kbs", ProviderSettings::default()) + .await? + .get_secret(resource, &Annotations::default()) + .await + .map_err(|e| { + error!("get keys from kbs failed: {e}"); + BlockDeviceError::GetKeysFailure(e.into()) + })?; + return String::from_utf8(secret).context("convert to String failed"); + } + + Err(BlockDeviceError::GetKeysFailure(anyhow::anyhow!("unknown resource scheme")).into()) +} + +async fn create_storage_key_file( + storage_key_path: &str, + encrypt_key: Option, +) -> anyhow::Result<()> { + let mut storage_key_file = fs::File::create(storage_key_path).await?; + + let plain_key = match encrypt_key { + Some(encrypt_key) => get_plaintext_key(&encrypt_key).await?, + None => random_encrypt_key().await?, + }; + + storage_key_file.write_all(plain_key.as_bytes()).await?; + storage_key_file.flush().await?; + Ok(()) +} + +impl BlockDevice { + async fn real_mount( + &self, + options: &HashMap, + _flags: &[String], + mount_point: &str, + ) -> Result<()> { + // construct BlockDeviceParameters + let parameters = serde_json::to_string(options)?; + let bd_parameter: BlockDeviceParameters = serde_json::from_str(¶meters)?; + + if bd_parameter.encryption_type == BlockDeviceEncryptType::LUKS { + let random_string: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(5) + .map(char::from) + .collect(); + let storage_key_path = format!("/tmp/encrypted_storage_key_{}", random_string); + create_storage_key_file(&storage_key_path, bd_parameter.encryption_key).await?; + + let parameters = vec![ + bd_parameter.device_id, + mount_point.to_string(), + storage_key_path, + bd_parameter.data_integrity, + ]; + + let mut encrypt_device = Command::new(LUKS_ENCRYPT_STORAGE_BIN) + .args(parameters) + .spawn() + .map_err(|e| { + error!("luks-encrypt-storage cmd fork failed: {e}"); + BlockDeviceError::BlockDeviceMountFailed + })?; + + let bd_res = encrypt_device.wait().await?; + if !bd_res.success() { + { + let mut stderr = String::new(); + if let Some(mut err) = encrypt_device.stderr { + err.read_to_string(&mut stderr).await?; + error!("BlockDevice mount failed with stderr: {stderr}"); + } else { + error!("BlockDevice mount failed"); + } + + return Err(BlockDeviceError::BlockDeviceMountFailed); + } + } + }; + + Ok(()) + } +} + +#[async_trait] +impl SecureMount for BlockDevice { + /// Mount the block device to the given `mount_point``. + /// + /// If `bd.encrypt_type` is set to `LUKS`, the device will be formated as a LUKS-encrypted device. + /// Then use cryptsetup open the device and mount it to `mount_point` as plaintext. + /// + /// This is a wrapper for inner function to convert error type. + async fn mount( + &self, + options: &HashMap, + flags: &[String], + mount_point: &str, + ) -> super::Result<()> { + self.real_mount(options, flags, mount_point) + .await + .map_err(|e| e.into()) + } +} + +#[cfg(test)] +mod tests {} diff --git a/confidential-data-hub/storage/src/volume_type/mod.rs b/confidential-data-hub/storage/src/volume_type/mod.rs index 7a89d06b4..76f27dc28 100644 --- a/confidential-data-hub/storage/src/volume_type/mod.rs +++ b/confidential-data-hub/storage/src/volume_type/mod.rs @@ -5,7 +5,7 @@ #[cfg(feature = "aliyun")] pub mod aliyun; - +pub mod blockdevice; use std::{collections::HashMap, str::FromStr}; use crate::Result; @@ -20,6 +20,7 @@ pub enum Volume { #[cfg(feature = "aliyun")] #[strum(serialize = "alibaba-cloud-oss")] AliOss, + BlockDevice, } /// Indicating a mount point and its parameters. @@ -61,6 +62,12 @@ impl Storage { .await?; Ok(self.mount_point.clone()) } + Volume::BlockDevice => { + let bd = blockdevice::BlockDevice {}; + bd.mount(&self.options, &self.flags, &self.mount_point) + .await?; + Ok(self.mount_point.clone()) + } } } }