tape: add hardware encryption key managenent api
This commit is contained in:
parent
4e9cc3e97c
commit
d5a48b5ce4
|
@ -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),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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,7 +12,8 @@ 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.")
|
||||||
.format(&PROXMOX_SAFE_ID_FORMAT)
|
.format(&PROXMOX_SAFE_ID_FORMAT)
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
///
|
///
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue