tape: add hardware encryption key managenent api
This commit is contained in:
		@ -9,6 +9,7 @@ pub mod verify;
 | 
			
		||||
pub mod drive;
 | 
			
		||||
pub mod changer;
 | 
			
		||||
pub mod media_pool;
 | 
			
		||||
pub mod tape_encryption_keys;
 | 
			
		||||
 | 
			
		||||
const SUBDIRS: SubdirMap = &[
 | 
			
		||||
    ("access", &access::ROUTER),
 | 
			
		||||
@ -18,6 +19,7 @@ const SUBDIRS: SubdirMap = &[
 | 
			
		||||
    ("media-pool", &media_pool::ROUTER),
 | 
			
		||||
    ("remote", &remote::ROUTER),
 | 
			
		||||
    ("sync", &sync::ROUTER),
 | 
			
		||||
    ("tape-encryption-keys", &tape_encryption_keys::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::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use anyhow::{bail, Error};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
 | 
			
		||||
use proxmox::{
 | 
			
		||||
@ -880,8 +880,7 @@ pub fn cartridge_memory(drive: String) -> Result<Vec<MamAttribute>, Error> {
 | 
			
		||||
    let (config, _digest) = config::drive::config()?;
 | 
			
		||||
 | 
			
		||||
    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
 | 
			
		||||
    let mut handle = drive_config.open()
 | 
			
		||||
        .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, drive_config.path, err))?;
 | 
			
		||||
    let mut handle = drive_config.open()?;
 | 
			
		||||
 | 
			
		||||
    handle.cartridge_memory()
 | 
			
		||||
}
 | 
			
		||||
@ -906,8 +905,7 @@ pub fn status(drive: String) -> Result<LinuxDriveAndMediaStatus, Error> {
 | 
			
		||||
    let drive_config: LinuxTapeDrive = config.lookup("linux", &drive)?;
 | 
			
		||||
 | 
			
		||||
    // Note: use open_linux_tape_device, because this also works if no medium loaded
 | 
			
		||||
    let file = open_linux_tape_device(&drive_config.path)
 | 
			
		||||
        .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, drive_config.path, err))?;
 | 
			
		||||
    let file = open_linux_tape_device(&drive_config.path)?;
 | 
			
		||||
 | 
			
		||||
    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 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")$");
 | 
			
		||||
 | 
			
		||||
@ -103,8 +103,8 @@ pub const IP_FORMAT: ApiStringFormat =
 | 
			
		||||
pub const PVE_CONFIG_DIGEST_FORMAT: ApiStringFormat =
 | 
			
		||||
    ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
 | 
			
		||||
 | 
			
		||||
pub const CERT_FINGERPRINT_SHA256_FORMAT: ApiStringFormat =
 | 
			
		||||
    ApiStringFormat::Pattern(&CERT_FINGERPRINT_SHA256_REGEX);
 | 
			
		||||
pub const FINGERPRINT_SHA256_FORMAT: ApiStringFormat =
 | 
			
		||||
    ApiStringFormat::Pattern(&FINGERPRINT_SHA256_REGEX);
 | 
			
		||||
 | 
			
		||||
pub const PROXMOX_SAFE_ID_FORMAT: ApiStringFormat =
 | 
			
		||||
    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(
 | 
			
		||||
    "X509 certificate fingerprint (sha256)."
 | 
			
		||||
)
 | 
			
		||||
    .format(&CERT_FINGERPRINT_SHA256_FORMAT)
 | 
			
		||||
    .format(&FINGERPRINT_SHA256_FORMAT)
 | 
			
		||||
    .schema();
 | 
			
		||||
 | 
			
		||||
pub const PROXMOX_CONFIG_DIGEST_SCHEMA: Schema = StringSchema::new(r#"\
 | 
			
		||||
Prevent changes if current configuration file has different SHA256 digest.
 | 
			
		||||
This can be used to prevent concurrent modifications.
 | 
			
		||||
"#
 | 
			
		||||
pub const TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA: Schema = StringSchema::new(
 | 
			
		||||
    "Tape encryption key fingerprint (sha256)."
 | 
			
		||||
)
 | 
			
		||||
    .format(&PVE_CONFIG_DIGEST_FORMAT)
 | 
			
		||||
    .format(&FINGERPRINT_SHA256_FORMAT)
 | 
			
		||||
    .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 =
 | 
			
		||||
    ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,8 @@ use proxmox::api::{
 | 
			
		||||
use crate::api2::types::{
 | 
			
		||||
    PROXMOX_SAFE_ID_FORMAT,
 | 
			
		||||
    CHANGER_NAME_SCHEMA,
 | 
			
		||||
};
 | 
			
		||||
    CERT_FINGERPRINT_SHA256_SCHEMA,
 | 
			
		||||
 };
 | 
			
		||||
 | 
			
		||||
pub const DRIVE_NAME_SCHEMA: Schema = StringSchema::new("Drive Identifier.")
 | 
			
		||||
    .format(&PROXMOX_SAFE_ID_FORMAT)
 | 
			
		||||
@ -205,3 +206,18 @@ pub struct LinuxDriveAndMediaStatus {
 | 
			
		||||
    #[serde(skip_serializing_if="Option::is_none")]
 | 
			
		||||
    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,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
 | 
			
		||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Deserialize, Serialize)]
 | 
			
		||||
#[serde(transparent)]
 | 
			
		||||
/// 32-byte fingerprint, usually calculated with SHA256.
 | 
			
		||||
pub struct Fingerprint {
 | 
			
		||||
 | 
			
		||||
@ -952,6 +952,7 @@ fn main() {
 | 
			
		||||
        .insert("drive", drive_commands())
 | 
			
		||||
        .insert("pool", pool_commands())
 | 
			
		||||
        .insert("media", media_commands())
 | 
			
		||||
        .insert("key", encryption_key_commands())
 | 
			
		||||
        .insert(
 | 
			
		||||
            "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;
 | 
			
		||||
pub use media::*;
 | 
			
		||||
 | 
			
		||||
mod encryption_key;
 | 
			
		||||
pub use encryption_key::*;
 | 
			
		||||
 | 
			
		||||
@ -21,9 +21,11 @@ use proxmox::{
 | 
			
		||||
 | 
			
		||||
use proxmox_backup::{
 | 
			
		||||
    config,
 | 
			
		||||
    backup::Fingerprint,
 | 
			
		||||
    api2::types::{
 | 
			
		||||
        LINUX_DRIVE_PATH_SCHEMA,
 | 
			
		||||
        DRIVE_NAME_SCHEMA,
 | 
			
		||||
        TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA,
 | 
			
		||||
        LinuxTapeDrive,
 | 
			
		||||
    },
 | 
			
		||||
    tape::{
 | 
			
		||||
@ -42,8 +44,7 @@ fn get_tape_handle(param: &Value) -> Result<LinuxTapeHandle, Error> {
 | 
			
		||||
        let (config, _digest) = config::drive::config()?;
 | 
			
		||||
        let drive: LinuxTapeDrive = config.lookup("linux", &name)?;
 | 
			
		||||
        eprintln!("using device {}", drive.path);
 | 
			
		||||
        drive.open()
 | 
			
		||||
            .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", name, drive.path, err))?
 | 
			
		||||
        drive.open()?
 | 
			
		||||
    } else if let Some(device) = param["device"].as_str() {
 | 
			
		||||
        eprintln!("using 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 drive: LinuxTapeDrive = config.lookup("linux", &name)?;
 | 
			
		||||
        eprintln!("using device {}", drive.path);
 | 
			
		||||
        drive.open()
 | 
			
		||||
            .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", name, drive.path, err))?
 | 
			
		||||
        drive.open()?
 | 
			
		||||
    } else {
 | 
			
		||||
        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 drive: LinuxTapeDrive = config.lookup("linux", &name)?;
 | 
			
		||||
            eprintln!("using device {}", drive.path);
 | 
			
		||||
            drive.open()
 | 
			
		||||
                .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", name, drive.path, err))?
 | 
			
		||||
            drive.open()?
 | 
			
		||||
        } else {
 | 
			
		||||
            bail!("no drive/device specified");
 | 
			
		||||
        }
 | 
			
		||||
@ -187,6 +186,47 @@ fn tape_alert_flags(
 | 
			
		||||
    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(
 | 
			
		||||
   input: {
 | 
			
		||||
        properties: {
 | 
			
		||||
@ -260,6 +300,10 @@ fn main() -> Result<(), Error> {
 | 
			
		||||
            "volume-statistics",
 | 
			
		||||
            CliCommand::new(&API_METHOD_VOLUME_STATISTICS)
 | 
			
		||||
        )
 | 
			
		||||
        .insert(
 | 
			
		||||
            "encryption",
 | 
			
		||||
            CliCommand::new(&API_METHOD_SET_ENCRYPTION)
 | 
			
		||||
        )
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
    let mut rpcenv = CliEnvironment::new();
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ pub mod user;
 | 
			
		||||
pub mod verify;
 | 
			
		||||
pub mod drive;
 | 
			
		||||
pub mod media_pool;
 | 
			
		||||
pub mod tape_encryption_keys;
 | 
			
		||||
 | 
			
		||||
/// 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 crate::{
 | 
			
		||||
    config,
 | 
			
		||||
    backup::Fingerprint,
 | 
			
		||||
    tools::run_command,
 | 
			
		||||
    api2::types::{
 | 
			
		||||
        TapeDensity,
 | 
			
		||||
@ -24,6 +26,7 @@ use crate::{
 | 
			
		||||
        mam_extract_media_usage,
 | 
			
		||||
        read_tape_alert_flags,
 | 
			
		||||
        read_volume_statistics,
 | 
			
		||||
        set_encryption,
 | 
			
		||||
        drive::{
 | 
			
		||||
            LinuxTapeDrive,
 | 
			
		||||
            TapeDriver,
 | 
			
		||||
@ -504,6 +507,43 @@ impl TapeDriver for LinuxTapeHandle {
 | 
			
		||||
            .map_err(|err| format_err!("{}", err))
 | 
			
		||||
            .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
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ use proxmox::tools::io::ReadExt;
 | 
			
		||||
use proxmox::api::section_config::SectionConfigData;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    backup::Fingerprint,
 | 
			
		||||
    api2::types::{
 | 
			
		||||
        VirtualTapeDrive,
 | 
			
		||||
        LinuxTapeDrive,
 | 
			
		||||
@ -163,6 +164,14 @@ pub trait TapeDriver {
 | 
			
		||||
    fn tape_alert_flags(&mut self) -> Result<TapeAlertFlags, Error> {
 | 
			
		||||
        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.
 | 
			
		||||
@ -234,14 +243,12 @@ pub fn open_drive(
 | 
			
		||||
            match section_type_name.as_ref() {
 | 
			
		||||
                "virtual" => {
 | 
			
		||||
                    let tape = VirtualTapeDrive::deserialize(config)?;
 | 
			
		||||
                    let handle = tape.open()
 | 
			
		||||
                        .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?;
 | 
			
		||||
                   Ok(Box::new(handle))
 | 
			
		||||
                    let handle = tape.open()?;
 | 
			
		||||
                    Ok(Box::new(handle))
 | 
			
		||||
                }
 | 
			
		||||
                "linux" => {
 | 
			
		||||
                    let tape = LinuxTapeDrive::deserialize(config)?;
 | 
			
		||||
                    let handle = tape.open()
 | 
			
		||||
                        .map_err(|err| format_err!("open drive '{}' ({}) failed - {}", drive, tape.path, err))?;
 | 
			
		||||
                    let handle = tape.open()?;
 | 
			
		||||
                    Ok(Box::new(handle))
 | 
			
		||||
                }
 | 
			
		||||
                _ => bail!("unknown drive type '{}' - internal error"),
 | 
			
		||||
 | 
			
		||||
@ -40,18 +40,20 @@ impl VirtualTapeDrive {
 | 
			
		||||
 | 
			
		||||
    /// This needs to lock the drive
 | 
			
		||||
    pub fn open(&self) -> Result<VirtualTapeHandle, Error> {
 | 
			
		||||
        let mut lock_path = std::path::PathBuf::from(&self.path);
 | 
			
		||||
        lock_path.push(".drive.lck");
 | 
			
		||||
        proxmox::try_block!({
 | 
			
		||||
            let mut lock_path = std::path::PathBuf::from(&self.path);
 | 
			
		||||
            lock_path.push(".drive.lck");
 | 
			
		||||
 | 
			
		||||
        let timeout = std::time::Duration::new(10, 0);
 | 
			
		||||
        let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true)?;
 | 
			
		||||
            let timeout = std::time::Duration::new(10, 0);
 | 
			
		||||
            let lock = proxmox::tools::fs::open_file_locked(&lock_path, timeout, true)?;
 | 
			
		||||
 | 
			
		||||
        Ok(VirtualTapeHandle {
 | 
			
		||||
            _lock: lock,
 | 
			
		||||
            drive_name: self.name.clone(),
 | 
			
		||||
            max_size: self.max_size.unwrap_or(64*1024*1024),
 | 
			
		||||
            path: std::path::PathBuf::from(&self.path),
 | 
			
		||||
        })
 | 
			
		||||
            Ok(VirtualTapeHandle {
 | 
			
		||||
                _lock: lock,
 | 
			
		||||
                drive_name: self.name.clone(),
 | 
			
		||||
                max_size: self.max_size.unwrap_or(64*1024*1024),
 | 
			
		||||
                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