tape: add hardware encryption key managenent api
This commit is contained in:
		| @ -9,6 +9,7 @@ pub mod verify; | |||||||
| pub mod drive; | pub mod drive; | ||||||
| pub mod changer; | pub mod changer; | ||||||
| pub mod media_pool; | pub mod media_pool; | ||||||
|  | pub mod tape_encryption_keys; | ||||||
|  |  | ||||||
| const SUBDIRS: SubdirMap = &[ | const SUBDIRS: SubdirMap = &[ | ||||||
|     ("access", &access::ROUTER), |     ("access", &access::ROUTER), | ||||||
| @ -18,6 +19,7 @@ const SUBDIRS: SubdirMap = &[ | |||||||
|     ("media-pool", &media_pool::ROUTER), |     ("media-pool", &media_pool::ROUTER), | ||||||
|     ("remote", &remote::ROUTER), |     ("remote", &remote::ROUTER), | ||||||
|     ("sync", &sync::ROUTER), |     ("sync", &sync::ROUTER), | ||||||
|  |     ("tape-encryption-keys", &tape_encryption_keys::ROUTER), | ||||||
|     ("verify", &verify::ROUTER), |     ("verify", &verify::ROUTER), | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										166
									
								
								src/api2/config/tape_encryption_keys.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/api2/config/tape_encryption_keys.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,166 @@ | |||||||
|  | use anyhow::{bail, Error}; | ||||||
|  | use serde_json::Value; | ||||||
|  |  | ||||||
|  | use proxmox::{ | ||||||
|  |     api::{ | ||||||
|  |         api, | ||||||
|  |         ApiMethod, | ||||||
|  |         Router, | ||||||
|  |         RpcEnvironment, | ||||||
|  |     }, | ||||||
|  |     tools::fs::open_file_locked, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     config::{ | ||||||
|  |         tape_encryption_keys::{ | ||||||
|  |             TAPE_KEYS_LOCKFILE, | ||||||
|  |             EncryptionKeyInfo, | ||||||
|  |             load_keys, | ||||||
|  |             save_keys, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     api2::types::{ | ||||||
|  |         TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA, | ||||||
|  |         PROXMOX_CONFIG_DIGEST_SCHEMA, | ||||||
|  |         TapeKeyMetadata, | ||||||
|  |     }, | ||||||
|  |     backup::Fingerprint, | ||||||
|  |     tools::format::as_fingerprint, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | #[api( | ||||||
|  |     protected: true, | ||||||
|  |     input: { | ||||||
|  |         properties: {}, | ||||||
|  |     }, | ||||||
|  |     returns: { | ||||||
|  |         description: "The list of tape encryption keys (with config digest).", | ||||||
|  |         type: Array, | ||||||
|  |         items: { type: TapeKeyMetadata }, | ||||||
|  |     }, | ||||||
|  | )] | ||||||
|  | /// List existing keys | ||||||
|  | pub fn list_keys( | ||||||
|  |     _param: Value, | ||||||
|  |     _info: &ApiMethod, | ||||||
|  |     mut rpcenv: &mut dyn RpcEnvironment, | ||||||
|  | ) -> Result<Vec<TapeKeyMetadata>, Error> { | ||||||
|  |  | ||||||
|  |     let (key_map, digest) = load_keys()?; | ||||||
|  |  | ||||||
|  |     let mut list = Vec::new(); | ||||||
|  |      | ||||||
|  |     for (_fingerprint, item) in key_map { | ||||||
|  |         list.push(TapeKeyMetadata { | ||||||
|  |             hint: item.hint, | ||||||
|  |             fingerprint: as_fingerprint(item.fingerprint.bytes()), | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); | ||||||
|  |  | ||||||
|  |     Ok(list) | ||||||
|  | } | ||||||
|  | #[api( | ||||||
|  |     protected: true, | ||||||
|  |     input: { | ||||||
|  |         properties: { | ||||||
|  |             password: { | ||||||
|  |                 description: "A secret password.", | ||||||
|  |                 min_length: 5, | ||||||
|  |             }, | ||||||
|  |             hint: { | ||||||
|  |                 description: "Password restore hint", | ||||||
|  |                 min_length: 1, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | )] | ||||||
|  | /// Create a new encryption key | ||||||
|  | pub fn create_key( | ||||||
|  |     password: String, | ||||||
|  |     hint: String, | ||||||
|  |     _rpcenv: &mut dyn RpcEnvironment | ||||||
|  | ) -> Result<Fingerprint, Error> { | ||||||
|  |  | ||||||
|  |     let key = openssl::sha::sha256(password.as_bytes()); // fixme: better KDF ?? | ||||||
|  |  | ||||||
|  |     let item = EncryptionKeyInfo::new(&key, hint); | ||||||
|  |  | ||||||
|  |     let _lock = open_file_locked( | ||||||
|  |         TAPE_KEYS_LOCKFILE, | ||||||
|  |         std::time::Duration::new(10, 0), | ||||||
|  |         true, | ||||||
|  |     )?; | ||||||
|  |  | ||||||
|  |     let (mut key_map, _) = load_keys()?; | ||||||
|  |  | ||||||
|  |     let fingerprint = item.fingerprint.clone(); | ||||||
|  |  | ||||||
|  |     if let Some(_) = key_map.get(&fingerprint) { | ||||||
|  |         bail!("encryption key '{}' already exists.", fingerprint); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     key_map.insert(fingerprint.clone(), item); | ||||||
|  |     save_keys(key_map)?; | ||||||
|  |  | ||||||
|  |     Ok(fingerprint) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[api( | ||||||
|  |     protected: true, | ||||||
|  |     input: { | ||||||
|  |         properties: { | ||||||
|  |             fingerprint: { | ||||||
|  |                 schema: TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA, | ||||||
|  |             }, | ||||||
|  |             digest: { | ||||||
|  |                 optional: true, | ||||||
|  |                 schema: PROXMOX_CONFIG_DIGEST_SCHEMA, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | )] | ||||||
|  | /// Remove a encryption key from the database | ||||||
|  | /// | ||||||
|  | /// Please note that you can no longer access tapes using this key. | ||||||
|  | pub fn delete_key( | ||||||
|  |     fingerprint: Fingerprint, | ||||||
|  |     digest: Option<String>, | ||||||
|  |     _rpcenv: &mut dyn RpcEnvironment, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |   | ||||||
|  |     let _lock = open_file_locked( | ||||||
|  |         TAPE_KEYS_LOCKFILE, | ||||||
|  |         std::time::Duration::new(10, 0), | ||||||
|  |         true, | ||||||
|  |     )?; | ||||||
|  |  | ||||||
|  |     let (mut key_map, expected_digest) = load_keys()?; | ||||||
|  |  | ||||||
|  |     if let Some(ref digest) = digest { | ||||||
|  |         let digest = proxmox::tools::hex_to_digest(digest)?; | ||||||
|  |         crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     match key_map.get(&fingerprint) { | ||||||
|  |         Some(_) => { key_map.remove(&fingerprint); }, | ||||||
|  |         None => bail!("tape encryption key '{}' does not exist.", fingerprint), | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     save_keys(key_map)?; | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ITEM_ROUTER: Router = Router::new() | ||||||
|  |     //.get(&API_METHOD_READ_KEY_METADATA) | ||||||
|  |     //.put(&API_METHOD_UPDATE_KEY_METADATA) | ||||||
|  |     .delete(&API_METHOD_DELETE_KEY); | ||||||
|  |  | ||||||
|  | pub const ROUTER: Router = Router::new() | ||||||
|  |     .get(&API_METHOD_LIST_KEYS) | ||||||
|  |     .post(&API_METHOD_CREATE_KEY) | ||||||
|  |     .match_all("fingerprint", &ITEM_ROUTER); | ||||||
| @ -1,7 +1,7 @@ | |||||||
| use std::path::Path; | use std::path::Path; | ||||||
| use std::sync::Arc; | use std::sync::Arc; | ||||||
|  |  | ||||||
| use anyhow::{bail, format_err, Error}; | use anyhow::{bail, Error}; | ||||||
| use serde_json::Value; | use serde_json::Value; | ||||||
|  |  | ||||||
| use proxmox::{ | use proxmox::{ | ||||||
| @ -880,8 +880,7 @@ pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> { | |||||||
|     let (config, _digest) = config::drive::config()?; |     let (config, _digest) = config::drive::config()?; | ||||||
|  |  | ||||||
|     let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; |     let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; | ||||||
|     let mut handle = drive_config.open() |     let mut handle = drive_config.open()?; | ||||||
|         .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, drive_config.path, err))?; |  | ||||||
|  |  | ||||||
|     handle.cartridge_memory() |     handle.cartridge_memory() | ||||||
| } | } | ||||||
| @ -906,8 +905,7 @@ pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> { | |||||||
|     let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; |     let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?; | ||||||
|  |  | ||||||
|     // Note: use open_linux_tape_device, because this also works if no medium loaded |     // Note: use open_linux_tape_device, because this also works if no medium loaded | ||||||
|     let file = open_linux_tape_device(&drive_config.path) |     let file = open_linux_tape_device(&drive_config.path)?; | ||||||
|         .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, drive_config.path, err))?; |  | ||||||
|  |  | ||||||
|     let mut handle = LinuxTapeHandle::new(file); |     let mut handle = LinuxTapeHandle::new(file); | ||||||
|  |  | ||||||
|  | |||||||
| @ -77,7 +77,7 @@ const_regex!{ | |||||||
|  |  | ||||||
|     pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), "|", APITOKEN_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|",  IPRE_BRACKET!() ,"):)?(?:([0-9]{1,5}):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$"); |     pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), "|", APITOKEN_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|",  IPRE_BRACKET!() ,"):)?(?:([0-9]{1,5}):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$"); | ||||||
|  |  | ||||||
|     pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$"; |     pub FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$"; | ||||||
|  |  | ||||||
|     pub ACL_PATH_REGEX = concat!(r"^(?:/|", r"(?:/", PROXMOX_SAFE_ID_REGEX_STR!(), ")+", r")$"); |     pub ACL_PATH_REGEX = concat!(r"^(?:/|", r"(?:/", PROXMOX_SAFE_ID_REGEX_STR!(), ")+", r")$"); | ||||||
|  |  | ||||||
| @ -103,8 +103,8 @@ pub const IP_FORMAT: ApiStringFormat = | |||||||
| pub const PVE_CONFIG_DIGEST_FORMAT: ApiStringFormat = | pub const PVE_CONFIG_DIGEST_FORMAT: ApiStringFormat = | ||||||
|     ApiStringFormat::Pattern(&SHA256_HEX_REGEX); |     ApiStringFormat::Pattern(&SHA256_HEX_REGEX); | ||||||
|  |  | ||||||
| pub const CERT_FINGERPRINT_SHA256_FORMAT: ApiStringFormat = | pub const FINGERPRINT_SHA256_FORMAT: ApiStringFormat = | ||||||
|     ApiStringFormat::Pattern(&CERT_FINGERPRINT_SHA256_REGEX); |     ApiStringFormat::Pattern(&FINGERPRINT_SHA256_REGEX); | ||||||
|  |  | ||||||
| pub const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat = | pub const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat = | ||||||
|     ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX); |     ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX); | ||||||
| @ -163,17 +163,22 @@ pub const PBS_PASSWORD_SCHEMA: Schema = StringSchema::new("User Password.") | |||||||
| pub const CERT_FINGERPRINT_SHA256_SCHEMA: Schema = StringSchema::new( | pub const CERT_FINGERPRINT_SHA256_SCHEMA: Schema = StringSchema::new( | ||||||
|     "X509 certificate fingerprint (sha256)." |     "X509 certificate fingerprint (sha256)." | ||||||
| ) | ) | ||||||
|     .format(&CERT_FINGERPRINT_SHA256_FORMAT) |     .format(&FINGERPRINT_SHA256_FORMAT) | ||||||
|     .schema(); |     .schema(); | ||||||
|  |  | ||||||
| pub const PROXMOX_CONFIG_DIGEST_SCHEMA: Schema = StringSchema::new(r#"\ | pub const TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA: Schema = StringSchema::new( | ||||||
| Prevent changes if current configuration file has different SHA256 digest. |     "Tape encryption key fingerprint (sha256)." | ||||||
| This can be used to prevent concurrent modifications. |  | ||||||
| "# |  | ||||||
| ) | ) | ||||||
|     .format(&PVE_CONFIG_DIGEST_FORMAT) |     .format(&FINGERPRINT_SHA256_FORMAT) | ||||||
|     .schema(); |     .schema(); | ||||||
|  |  | ||||||
|  | pub const PROXMOX_CONFIG_DIGEST_SCHEMA: Schema = StringSchema::new( | ||||||
|  |     "Prevent changes if current configuration file has different \ | ||||||
|  |     SHA256 digest. This can be used to prevent concurrent \ | ||||||
|  |     modifications." | ||||||
|  | ) | ||||||
|  |     .format(&PVE_CONFIG_DIGEST_FORMAT) .schema(); | ||||||
|  |  | ||||||
|  |  | ||||||
| pub const CHUNK_DIGEST_FORMAT: ApiStringFormat = | pub const CHUNK_DIGEST_FORMAT: ApiStringFormat = | ||||||
|     ApiStringFormat::Pattern(&SHA256_HEX_REGEX); |     ApiStringFormat::Pattern(&SHA256_HEX_REGEX); | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ use proxmox::api::{ | |||||||
| use crate::api2::types::{ | use crate::api2::types::{ | ||||||
|     PROXMOX_SAFE_ID_FORMAT, |     PROXMOX_SAFE_ID_FORMAT, | ||||||
|     CHANGER_NAME_SCHEMA, |     CHANGER_NAME_SCHEMA, | ||||||
|  |     CERT_FINGERPRINT_SHA256_SCHEMA, | ||||||
|  }; |  }; | ||||||
|  |  | ||||||
| pub const DRIVE_NAME_SCHEMA: Schema = StringSchema::new("Drive Identifier.") | pub const DRIVE_NAME_SCHEMA: Schema = StringSchema::new("Drive Identifier.") | ||||||
| @ -205,3 +206,18 @@ pub struct LinuxDriveAndMediaStatus { | |||||||
|     #[serde(skip_serializing_if="Option::is_none")] |     #[serde(skip_serializing_if="Option::is_none")] | ||||||
|     pub medium_passes: Option<u64>, |     pub medium_passes: Option<u64>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[api( | ||||||
|  |     properties: { | ||||||
|  |         fingerprint: { | ||||||
|  |             schema: CERT_FINGERPRINT_SHA256_SCHEMA, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | )] | ||||||
|  | #[derive(Deserialize, Serialize)] | ||||||
|  | /// Hardware Encryption key Metadata | ||||||
|  | pub struct TapeKeyMetadata { | ||||||
|  |     /// Password hint | ||||||
|  |     pub hint: String, | ||||||
|  |     pub fingerprint: String, | ||||||
|  | } | ||||||
|  | |||||||
| @ -39,7 +39,7 @@ pub enum CryptMode { | |||||||
|     SignOnly, |     SignOnly, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Eq, PartialEq, Deserialize, Serialize)] | #[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize, Serialize)] | ||||||
| #[serde(transparent)] | #[serde(transparent)] | ||||||
| /// 32-byte fingerprint, usually calculated with SHA256. | /// 32-byte fingerprint, usually calculated with SHA256. | ||||||
| pub struct Fingerprint { | pub struct Fingerprint { | ||||||
|  | |||||||
| @ -952,6 +952,7 @@ fn main() { | |||||||
|         .insert("drive", drive_commands()) |         .insert("drive", drive_commands()) | ||||||
|         .insert("pool", pool_commands()) |         .insert("pool", pool_commands()) | ||||||
|         .insert("media", media_commands()) |         .insert("media", media_commands()) | ||||||
|  |         .insert("key", encryption_key_commands()) | ||||||
|         .insert( |         .insert( | ||||||
|             "load-media", |             "load-media", | ||||||
|             CliCommand::new(&API_METHOD_LOAD_MEDIA) |             CliCommand::new(&API_METHOD_LOAD_MEDIA) | ||||||
|  | |||||||
							
								
								
									
										70
									
								
								src/bin/proxmox_tape/encryption_key.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/bin/proxmox_tape/encryption_key.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | |||||||
|  | use anyhow::Error; | ||||||
|  | use serde_json::Value; | ||||||
|  |  | ||||||
|  | use proxmox::{ | ||||||
|  |     api::{ | ||||||
|  |         api, | ||||||
|  |         cli::*, | ||||||
|  |         RpcEnvironment, | ||||||
|  |         ApiHandler, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use proxmox_backup::{ | ||||||
|  |     api2::{ | ||||||
|  |         self, | ||||||
|  |     }, | ||||||
|  |     config::tape_encryption_keys::complete_key_fingerprint, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | pub fn encryption_key_commands() -> CommandLineInterface { | ||||||
|  |  | ||||||
|  |     let cmd_def = CliCommandMap::new() | ||||||
|  |         .insert("list", CliCommand::new(&API_METHOD_LIST_KEYS)) | ||||||
|  |         .insert( | ||||||
|  |             "create", | ||||||
|  |             CliCommand::new(&api2::config::tape_encryption_keys::API_METHOD_CREATE_KEY) | ||||||
|  |         ) | ||||||
|  |         .insert( | ||||||
|  |             "remove", | ||||||
|  |             CliCommand::new(&api2::config::tape_encryption_keys::API_METHOD_DELETE_KEY) | ||||||
|  |                 .arg_param(&["fingerprint"]) | ||||||
|  |                 .completion_cb("fingerprint", complete_key_fingerprint) | ||||||
|  |         ) | ||||||
|  |         ; | ||||||
|  |  | ||||||
|  |     cmd_def.into() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #[api( | ||||||
|  |     input: { | ||||||
|  |         properties: { | ||||||
|  |             "output-format": { | ||||||
|  |                 schema: OUTPUT_FORMAT, | ||||||
|  |                 optional: true, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | )] | ||||||
|  | /// List keys | ||||||
|  | fn list_keys( | ||||||
|  |     param: Value, | ||||||
|  |     rpcenv: &mut dyn RpcEnvironment, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |     let output_format = get_output_format(¶m); | ||||||
|  |     let info = &api2::config::tape_encryption_keys::API_METHOD_LIST_KEYS; | ||||||
|  |     let mut data = match info.handler { | ||||||
|  |         ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?, | ||||||
|  |         _ => unreachable!(), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let options = default_table_format_options() | ||||||
|  |         .column(ColumnConfig::new("fingerprint")) | ||||||
|  |         .column(ColumnConfig::new("hint")) | ||||||
|  |         ; | ||||||
|  |  | ||||||
|  |     format_and_print_result_full(&mut data, &info.returns, &output_format, &options); | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
| @ -9,3 +9,6 @@ pub use pool::*; | |||||||
|  |  | ||||||
| mod media; | mod media; | ||||||
| pub use media::*; | pub use media::*; | ||||||
|  |  | ||||||
|  | mod encryption_key; | ||||||
|  | pub use encryption_key::*; | ||||||
|  | |||||||
| @ -21,9 +21,11 @@ use proxmox::{ | |||||||
|  |  | ||||||
| use proxmox_backup::{ | use proxmox_backup::{ | ||||||
|     config, |     config, | ||||||
|  |     backup::Fingerprint, | ||||||
|     api2::types::{ |     api2::types::{ | ||||||
|         LINUX_DRIVE_PATH_SCHEMA, |         LINUX_DRIVE_PATH_SCHEMA, | ||||||
|         DRIVE_NAME_SCHEMA, |         DRIVE_NAME_SCHEMA, | ||||||
|  |         TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA, | ||||||
|         LinuxTapeDrive, |         LinuxTapeDrive, | ||||||
|     }, |     }, | ||||||
|     tape::{ |     tape::{ | ||||||
| @ -42,8 +44,7 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> { | |||||||
|         let (config, _digest) = config::drive::config()?; |         let (config, _digest) = config::drive::config()?; | ||||||
|         let drive: LinuxTapeDrive = config.lookup("linux", &name)?; |         let drive: LinuxTapeDrive = config.lookup("linux", &name)?; | ||||||
|         eprintln!("using device {}", drive.path); |         eprintln!("using device {}", drive.path); | ||||||
|         drive.open() |         drive.open()? | ||||||
|             .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", name, drive.path, err))? |  | ||||||
|     } else if let Some(device) = param["device"].as_str() { |     } else if let Some(device) = param["device"].as_str() { | ||||||
|         eprintln!("using device {}", device); |         eprintln!("using device {}", device); | ||||||
|         LinuxTapeHandle::new(open_linux_tape_device(&device)?) |         LinuxTapeHandle::new(open_linux_tape_device(&device)?) | ||||||
| @ -57,8 +58,7 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> { | |||||||
|         let (config, _digest) = config::drive::config()?; |         let (config, _digest) = config::drive::config()?; | ||||||
|         let drive: LinuxTapeDrive = config.lookup("linux", &name)?; |         let drive: LinuxTapeDrive = config.lookup("linux", &name)?; | ||||||
|         eprintln!("using device {}", drive.path); |         eprintln!("using device {}", drive.path); | ||||||
|         drive.open() |         drive.open()? | ||||||
|             .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", name, drive.path, err))? |  | ||||||
|     } else { |     } else { | ||||||
|         let (config, _digest) = config::drive::config()?; |         let (config, _digest) = config::drive::config()?; | ||||||
|  |  | ||||||
| @ -72,8 +72,7 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> { | |||||||
|             let name = drive_names[0]; |             let name = drive_names[0]; | ||||||
|             let drive: LinuxTapeDrive = config.lookup("linux", &name)?; |             let drive: LinuxTapeDrive = config.lookup("linux", &name)?; | ||||||
|             eprintln!("using device {}", drive.path); |             eprintln!("using device {}", drive.path); | ||||||
|             drive.open() |             drive.open()? | ||||||
|                 .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", name, drive.path, err))? |  | ||||||
|         } else { |         } else { | ||||||
|             bail!("no drive/device specified"); |             bail!("no drive/device specified"); | ||||||
|         } |         } | ||||||
| @ -187,6 +186,47 @@ fn tape_alert_flags( | |||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | #[api( | ||||||
|  |     input: { | ||||||
|  |         properties: { | ||||||
|  |             fingerprint: { | ||||||
|  |                 schema: TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA, | ||||||
|  |                 optional: true, | ||||||
|  |             }, | ||||||
|  |             drive: { | ||||||
|  |                 schema: DRIVE_NAME_SCHEMA, | ||||||
|  |                 optional: true, | ||||||
|  |             }, | ||||||
|  |             device: { | ||||||
|  |                 schema: LINUX_DRIVE_PATH_SCHEMA, | ||||||
|  |                 optional: true, | ||||||
|  |             }, | ||||||
|  |             stdin: { | ||||||
|  |                 description: "Use standard input as device handle.", | ||||||
|  |                 type: bool, | ||||||
|  |                 optional: true, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  | )] | ||||||
|  | /// Set or clear encryption key | ||||||
|  | fn set_encryption( | ||||||
|  |     fingerprint: Option<Fingerprint>, | ||||||
|  |     param: Value, | ||||||
|  | ) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |     let result = proxmox::try_block!({ | ||||||
|  |         let mut handle = get_tape_handle(¶m)?; | ||||||
|  |  | ||||||
|  |         handle.set_encryption(fingerprint)?; | ||||||
|  |         Ok(()) | ||||||
|  |     }).map_err(|err: Error| err.to_string()); | ||||||
|  |  | ||||||
|  |     println!("{}", serde_json::to_string_pretty(&result)?); | ||||||
|  |  | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
| #[api( | #[api( | ||||||
|    input: { |    input: { | ||||||
|         properties: { |         properties: { | ||||||
| @ -260,6 +300,10 @@ fn main() -> Result<(), Error> { | |||||||
|             "volume-statistics", |             "volume-statistics", | ||||||
|             CliCommand::new(&API_METHOD_VOLUME_STATISTICS) |             CliCommand::new(&API_METHOD_VOLUME_STATISTICS) | ||||||
|         ) |         ) | ||||||
|  |         .insert( | ||||||
|  |             "encryption", | ||||||
|  |             CliCommand::new(&API_METHOD_SET_ENCRYPTION) | ||||||
|  |         ) | ||||||
|         ; |         ; | ||||||
|  |  | ||||||
|     let mut rpcenv = CliEnvironment::new(); |     let mut rpcenv = CliEnvironment::new(); | ||||||
|  | |||||||
| @ -27,6 +27,7 @@ pub mod user; | |||||||
| pub mod verify; | pub mod verify; | ||||||
| pub mod drive; | pub mod drive; | ||||||
| pub mod media_pool; | pub mod media_pool; | ||||||
|  | pub mod tape_encryption_keys; | ||||||
|  |  | ||||||
| /// Check configuration directory permissions | /// Check configuration directory permissions | ||||||
| /// | /// | ||||||
|  | |||||||
							
								
								
									
										126
									
								
								src/config/tape_encryption_keys.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/config/tape_encryption_keys.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | use std::collections::HashMap; | ||||||
|  |  | ||||||
|  | use anyhow::{bail, Error}; | ||||||
|  | use serde::{Deserialize, Serialize}; | ||||||
|  | use openssl::sha::sha256; | ||||||
|  |  | ||||||
|  | use proxmox::tools::fs::{ | ||||||
|  |     file_read_optional_string, | ||||||
|  |     replace_file, | ||||||
|  |     CreateOptions, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use crate::{ | ||||||
|  |     backup::{ | ||||||
|  |         Fingerprint, | ||||||
|  |     }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | mod hex_key { | ||||||
|  |     use serde::{self, Deserialize, Serializer, Deserializer}; | ||||||
|  |  | ||||||
|  |     pub fn serialize<S>( | ||||||
|  |         csum: &[u8; 32], | ||||||
|  |         serializer: S, | ||||||
|  |     ) -> Result<S::Ok, S::Error> | ||||||
|  |     where | ||||||
|  |         S: Serializer, | ||||||
|  |     { | ||||||
|  |         let s = proxmox::tools::digest_to_hex(csum); | ||||||
|  |         serializer.serialize_str(&s) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn deserialize<'de, D>( | ||||||
|  |         deserializer: D, | ||||||
|  |     ) -> Result<[u8; 32], D::Error> | ||||||
|  |     where | ||||||
|  |         D: Deserializer<'de>, | ||||||
|  |     { | ||||||
|  |         let s = String::deserialize(deserializer)?; | ||||||
|  |         proxmox::tools::hex_to_digest(&s).map_err(serde::de::Error::custom) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Store Hardware Encryption keys | ||||||
|  | #[derive(Deserialize, Serialize)] | ||||||
|  | pub struct EncryptionKeyInfo { | ||||||
|  |     pub hint: String, | ||||||
|  |     #[serde(with = "hex_key")] | ||||||
|  |     pub key: [u8; 32], | ||||||
|  |     pub fingerprint: Fingerprint, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl EncryptionKeyInfo { | ||||||
|  |  | ||||||
|  |     pub fn new(key: &[u8; 32], hint: String) -> Self { | ||||||
|  |         Self { | ||||||
|  |             hint, | ||||||
|  |             key: key.clone(), | ||||||
|  |             fingerprint: Fingerprint::new(sha256(key)), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub const TAPE_KEYS_FILENAME: &str = "/etc/proxmox-backup/tape-encryption-keys.json"; | ||||||
|  | pub const TAPE_KEYS_LOCKFILE: &str = "/etc/proxmox-backup/.tape-encryption-keys.lck"; | ||||||
|  |  | ||||||
|  | pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>,  [u8;32]), Error> { | ||||||
|  |  | ||||||
|  |     let content = file_read_optional_string(TAPE_KEYS_FILENAME)?; | ||||||
|  |     let content = content.unwrap_or_else(|| String::from("[]")); | ||||||
|  |  | ||||||
|  |     let digest = openssl::sha::sha256(content.as_bytes()); | ||||||
|  |  | ||||||
|  |     let list: Vec<EncryptionKeyInfo> = serde_json::from_str(&content)?; | ||||||
|  |  | ||||||
|  |     let mut map = HashMap::new(); | ||||||
|  |      | ||||||
|  |     for item in list { | ||||||
|  |         let expected_fingerprint = Fingerprint::new(sha256(&item.key)); | ||||||
|  |         if item.fingerprint != expected_fingerprint { | ||||||
|  |             bail!( | ||||||
|  |                 "inconsistent fingerprint ({} != {})", | ||||||
|  |                 item.fingerprint, | ||||||
|  |                 expected_fingerprint, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         map.insert(item.fingerprint.clone(), item); | ||||||
|  |     } | ||||||
|  |     | ||||||
|  |     Ok((map, digest)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn save_keys(map: HashMap<Fingerprint, EncryptionKeyInfo>) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |     let mut list = Vec::new(); | ||||||
|  |  | ||||||
|  |     for (_fp, item) in map { | ||||||
|  |         list.push(item); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let raw = serde_json::to_string_pretty(&list)?; | ||||||
|  |      | ||||||
|  |     let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); | ||||||
|  |     // set the correct owner/group/permissions while saving file | ||||||
|  |     // owner(rw) = root, group(r)= root | ||||||
|  |     let options = CreateOptions::new() | ||||||
|  |         .perm(mode) | ||||||
|  |         .owner(nix::unistd::ROOT) | ||||||
|  |         .group(nix::unistd::Gid::from_raw(0)); | ||||||
|  |  | ||||||
|  |     replace_file(TAPE_KEYS_FILENAME, raw.as_bytes(), options)?; | ||||||
|  |      | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // shell completion helper | ||||||
|  | pub fn complete_key_fingerprint(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | ||||||
|  |     let data = match load_keys() { | ||||||
|  |         Ok((data, _digest)) => data, | ||||||
|  |         Err(_) => return Vec::new(), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     data.keys().map(|fp| crate::tools::format::as_fingerprint(fp.bytes())).collect() | ||||||
|  | } | ||||||
|  |  | ||||||
| @ -9,6 +9,8 @@ use nix::fcntl::{fcntl, FcntlArg, OFlag}; | |||||||
| use proxmox::sys::error::SysResult; | use proxmox::sys::error::SysResult; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|  |     config, | ||||||
|  |     backup::Fingerprint, | ||||||
|     tools::run_command, |     tools::run_command, | ||||||
|     api2::types::{ |     api2::types::{ | ||||||
|         TapeDensity, |         TapeDensity, | ||||||
| @ -24,6 +26,7 @@ use crate::{ | |||||||
|         mam_extract_media_usage, |         mam_extract_media_usage, | ||||||
|         read_tape_alert_flags, |         read_tape_alert_flags, | ||||||
|         read_volume_statistics, |         read_volume_statistics, | ||||||
|  |         set_encryption, | ||||||
|         drive::{ |         drive::{ | ||||||
|             LinuxTapeDrive, |             LinuxTapeDrive, | ||||||
|             TapeDriver, |             TapeDriver, | ||||||
| @ -504,6 +507,43 @@ impl TapeDriver for LinuxTapeHandle { | |||||||
|             .map_err(|err| format_err!("{}", err)) |             .map_err(|err| format_err!("{}", err)) | ||||||
|             .map(|bits| TapeAlertFlags::from_bits_truncate(bits)) |             .map(|bits| TapeAlertFlags::from_bits_truncate(bits)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Set or clear encryption key | ||||||
|  |     /// | ||||||
|  |     /// Note: Only 'root' user may run RAW SG commands, so we need to | ||||||
|  |     /// spawn setuid binary 'sg-tape-cmd'. Also, encryption key file | ||||||
|  |     /// is only readable by root. | ||||||
|  |     fn set_encryption(&mut self, key_fingerprint: Option<Fingerprint>) -> Result<(), Error> { | ||||||
|  |  | ||||||
|  |         if nix::unistd::Uid::effective().is_root() { | ||||||
|  |  | ||||||
|  |             if let Some(ref key_fingerprint) = key_fingerprint { | ||||||
|  |  | ||||||
|  |                 let (key_map, _digest) = config::tape_encryption_keys::load_keys()?; | ||||||
|  |                 match key_map.get(key_fingerprint) { | ||||||
|  |                     Some(item) => { | ||||||
|  |                         return set_encryption(&mut self.file, Some(item.key)); | ||||||
|  |                     } | ||||||
|  |                     None => bail!("unknown tape encryption key '{}'", key_fingerprint), | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 return set_encryption(&mut self.file, None); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         let mut command = std::process::Command::new( | ||||||
|  |             "/usr/lib/x86_64-linux-gnu/proxmox-backup/sg-tape-cmd"); | ||||||
|  |         command.args(&["encryption"]); | ||||||
|  |         if let Some(fingerprint) = key_fingerprint { | ||||||
|  |             let fingerprint = crate::tools::format::as_fingerprint(fingerprint.bytes()); | ||||||
|  |             command.args(&["--fingerprint", &fingerprint]); | ||||||
|  |         } | ||||||
|  |         command.args(&["--stdin"]); | ||||||
|  |         command.stdin(unsafe { std::process::Stdio::from_raw_fd(self.file.as_raw_fd())}); | ||||||
|  |         let output = run_command(command, None)?; | ||||||
|  |         let result: Result<(), String> = serde_json::from_str(&output)?; | ||||||
|  |         result.map_err(|err| format_err!("{}", err)) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Write a single EOF mark without flushing buffers | /// Write a single EOF mark without flushing buffers | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ use proxmox::tools::io::ReadExt; | |||||||
| use proxmox::api::section_config::SectionConfigData; | use proxmox::api::section_config::SectionConfigData; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|  |     backup::Fingerprint, | ||||||
|     api2::types::{ |     api2::types::{ | ||||||
|         VirtualTapeDrive, |         VirtualTapeDrive, | ||||||
|         LinuxTapeDrive, |         LinuxTapeDrive, | ||||||
| @ -163,6 +164,14 @@ pub trait TapeDriver { | |||||||
|     fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> { |     fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> { | ||||||
|         Ok(TapeAlertFlags::empty()) |         Ok(TapeAlertFlags::empty()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /// Set or clear encryption key | ||||||
|  |     fn set_encryption(&mut self, key_fingerprint: Option<Fingerprint>) -> Result<(), Error> { | ||||||
|  |         if key_fingerprint.is_some() { | ||||||
|  |             bail!("drive does not support encryption"); | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Get the media changer (MediaChange + name) associated with a tape drive. | /// Get the media changer (MediaChange + name) associated with a tape drive. | ||||||
| @ -234,14 +243,12 @@ pub fn open_drive( | |||||||
|             match section_type_name.as_ref() { |             match section_type_name.as_ref() { | ||||||
|                 "virtual" => { |                 "virtual" => { | ||||||
|                     let tape = VirtualTapeDrive::deserialize(config)?; |                     let tape = VirtualTapeDrive::deserialize(config)?; | ||||||
|                     let handle = tape.open() |                     let handle = tape.open()?; | ||||||
|                         .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?; |  | ||||||
|                     Ok(Box::new(handle)) |                     Ok(Box::new(handle)) | ||||||
|                 } |                 } | ||||||
|                 "linux" => { |                 "linux" => { | ||||||
|                     let tape = LinuxTapeDrive::deserialize(config)?; |                     let tape = LinuxTapeDrive::deserialize(config)?; | ||||||
|                     let handle = tape.open() |                     let handle = tape.open()?; | ||||||
|                         .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?; |  | ||||||
|                     Ok(Box::new(handle)) |                     Ok(Box::new(handle)) | ||||||
|                 } |                 } | ||||||
|                 _ => bail!("unknown drive type '{}' - internal error"), |                 _ => bail!("unknown drive type '{}' - internal error"), | ||||||
|  | |||||||
| @ -40,6 +40,7 @@ impl VirtualTapeDrive { | |||||||
|  |  | ||||||
|     /// This needs to lock the drive |     /// This needs to lock the drive | ||||||
|     pub fn open(&self) -> Result<VirtualTapeHandle, Error> { |     pub fn open(&self) -> Result<VirtualTapeHandle, Error> { | ||||||
|  |         proxmox::try_block!({ | ||||||
|             let mut lock_path = std::path::PathBuf::from(&self.path); |             let mut lock_path = std::path::PathBuf::from(&self.path); | ||||||
|             lock_path.push(".drive.lck"); |             lock_path.push(".drive.lck"); | ||||||
|  |  | ||||||
| @ -52,6 +53,7 @@ impl VirtualTapeDrive { | |||||||
|                 max_size: self.max_size.unwrap_or(64*1024*1024), |                 max_size: self.max_size.unwrap_or(64*1024*1024), | ||||||
|                 path: std::path::PathBuf::from(&self.path), |                 path: std::path::PathBuf::from(&self.path), | ||||||
|             }) |             }) | ||||||
|  |         }).map_err(|err: Error| format_err!("open drive '{}' ({}) failed - {}", self.name, self.path, err)) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user