tape: generate random encryptions keys and store key_config on media
This commit is contained in:
parent
8ca37d6a65
commit
feb1645f37
@ -15,9 +15,12 @@ use crate::{
|
|||||||
config::{
|
config::{
|
||||||
tape_encryption_keys::{
|
tape_encryption_keys::{
|
||||||
TAPE_KEYS_LOCKFILE,
|
TAPE_KEYS_LOCKFILE,
|
||||||
EncryptionKeyInfo,
|
generate_tape_encryption_key,
|
||||||
load_keys,
|
load_keys,
|
||||||
|
load_key_configs,
|
||||||
save_keys,
|
save_keys,
|
||||||
|
save_key_configs,
|
||||||
|
insert_key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
api2::types::{
|
api2::types::{
|
||||||
@ -25,12 +28,13 @@ use crate::{
|
|||||||
PROXMOX_CONFIG_DIGEST_SCHEMA,
|
PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||||
TapeKeyMetadata,
|
TapeKeyMetadata,
|
||||||
},
|
},
|
||||||
backup::Fingerprint,
|
backup::{
|
||||||
|
Fingerprint,
|
||||||
|
},
|
||||||
tools::format::as_fingerprint,
|
tools::format::as_fingerprint,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
protected: true,
|
|
||||||
input: {
|
input: {
|
||||||
properties: {},
|
properties: {},
|
||||||
},
|
},
|
||||||
@ -47,14 +51,14 @@ pub fn list_keys(
|
|||||||
mut rpcenv: &mut dyn RpcEnvironment,
|
mut rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Vec<TapeKeyMetadata>, Error> {
|
) -> Result<Vec<TapeKeyMetadata>, Error> {
|
||||||
|
|
||||||
let (key_map, digest) = load_keys()?;
|
let (key_map, digest) = load_key_configs()?;
|
||||||
|
|
||||||
let mut list = Vec::new();
|
let mut list = Vec::new();
|
||||||
|
|
||||||
for (_fingerprint, item) in key_map {
|
for (fingerprint, item) in key_map {
|
||||||
list.push(TapeKeyMetadata {
|
list.push(TapeKeyMetadata {
|
||||||
hint: item.hint,
|
hint: item.hint,
|
||||||
fingerprint: as_fingerprint(item.fingerprint.bytes()),
|
fingerprint: as_fingerprint(fingerprint.bytes()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,11 +75,15 @@ pub fn list_keys(
|
|||||||
min_length: 5,
|
min_length: 5,
|
||||||
},
|
},
|
||||||
hint: {
|
hint: {
|
||||||
description: "Password restore hint",
|
description: "Password restore hint.",
|
||||||
min_length: 1,
|
min_length: 1,
|
||||||
|
max_length: 32,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
returns: {
|
||||||
|
schema: TAPE_ENCRYPTION_KEY_FINGERPRINT_SCHEMA,
|
||||||
|
},
|
||||||
)]
|
)]
|
||||||
/// Create a new encryption key
|
/// Create a new encryption key
|
||||||
pub fn create_key(
|
pub fn create_key(
|
||||||
@ -84,26 +92,11 @@ pub fn create_key(
|
|||||||
_rpcenv: &mut dyn RpcEnvironment
|
_rpcenv: &mut dyn RpcEnvironment
|
||||||
) -> Result<Fingerprint, Error> {
|
) -> Result<Fingerprint, Error> {
|
||||||
|
|
||||||
let key = openssl::sha::sha256(password.as_bytes()); // fixme: better KDF ??
|
let (key, key_config) = generate_tape_encryption_key(password.as_bytes())?;
|
||||||
|
|
||||||
let item = EncryptionKeyInfo::new(&key, hint);
|
let fingerprint = key_config.fingerprint.clone().unwrap();
|
||||||
|
|
||||||
let _lock = open_file_locked(
|
insert_key(key, key_config, hint)?;
|
||||||
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)
|
Ok(fingerprint)
|
||||||
}
|
}
|
||||||
@ -138,18 +131,21 @@ pub fn delete_key(
|
|||||||
true,
|
true,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (mut key_map, expected_digest) = load_keys()?;
|
let (mut config_map, expected_digest) = load_key_configs()?;
|
||||||
|
let (mut key_map, _) = load_keys()?;
|
||||||
|
|
||||||
if let Some(ref digest) = digest {
|
if let Some(ref digest) = digest {
|
||||||
let digest = proxmox::tools::hex_to_digest(digest)?;
|
let digest = proxmox::tools::hex_to_digest(digest)?;
|
||||||
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
|
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
match key_map.get(&fingerprint) {
|
match config_map.get(&fingerprint) {
|
||||||
Some(_) => { key_map.remove(&fingerprint); },
|
Some(_) => { config_map.remove(&fingerprint); },
|
||||||
None => bail!("tape encryption key '{}' does not exist.", fingerprint),
|
None => bail!("tape encryption key '{}' does not exist.", fingerprint),
|
||||||
}
|
}
|
||||||
|
save_key_configs(config_map)?;
|
||||||
|
|
||||||
|
key_map.remove(&fingerprint);
|
||||||
save_keys(key_map)?;
|
save_keys(key_map)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -24,6 +24,7 @@ use crate::{
|
|||||||
self,
|
self,
|
||||||
drive::check_drive_exists,
|
drive::check_drive_exists,
|
||||||
},
|
},
|
||||||
|
backup::decrypt_key_config,
|
||||||
api2::{
|
api2::{
|
||||||
types::{
|
types::{
|
||||||
UPID_SCHEMA,
|
UPID_SCHEMA,
|
||||||
@ -408,7 +409,7 @@ fn write_media_label(
|
|||||||
worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool));
|
worker.log(format!("Label media '{}' for pool '{}'", label.label_text, pool));
|
||||||
let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime, None);
|
let set = MediaSetLabel::with_data(&pool, [0u8; 16].into(), 0, label.ctime, None);
|
||||||
|
|
||||||
drive.write_media_set_label(&set)?;
|
drive.write_media_set_label(&set, None)?;
|
||||||
media_set_label = Some(set);
|
media_set_label = Some(set);
|
||||||
} else {
|
} else {
|
||||||
worker.log(format!("Label media '{}' (no pool assignment)", label.label_text));
|
worker.log(format!("Label media '{}' (no pool assignment)", label.label_text));
|
||||||
@ -427,7 +428,7 @@ fn write_media_label(
|
|||||||
drive.rewind()?;
|
drive.rewind()?;
|
||||||
|
|
||||||
match drive.read_label() {
|
match drive.read_label() {
|
||||||
Ok(Some(info)) => {
|
Ok((Some(info), _)) => {
|
||||||
if info.label.uuid != media_id.label.uuid {
|
if info.label.uuid != media_id.label.uuid {
|
||||||
bail!("verify label failed - got wrong label uuid");
|
bail!("verify label failed - got wrong label uuid");
|
||||||
}
|
}
|
||||||
@ -447,7 +448,7 @@ fn write_media_label(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Ok(None) => bail!("verify label failed (got empty media)"),
|
Ok((None, _)) => bail!("verify label failed (got empty media)"),
|
||||||
Err(err) => bail!("verify label failed - {}", err),
|
Err(err) => bail!("verify label failed - {}", err),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -456,6 +457,46 @@ fn write_media_label(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
drive: {
|
||||||
|
schema: DRIVE_NAME_SCHEMA,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
description: "Encryption key password.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Try to restore a tape encryption key
|
||||||
|
pub async fn restore_key(
|
||||||
|
drive: String,
|
||||||
|
password: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let (config, _digest) = config::drive::config()?;
|
||||||
|
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut drive = open_drive(&config, &drive)?;
|
||||||
|
|
||||||
|
let (_media_id, key_config) = drive.read_label()?;
|
||||||
|
|
||||||
|
if let Some(key_config) = key_config {
|
||||||
|
let hint = String::from("fixme: add hint");
|
||||||
|
// fixme: howto show restore hint
|
||||||
|
let password_fn = || { Ok(password.as_bytes().to_vec()) };
|
||||||
|
let (key, ..) = decrypt_key_config(&key_config, &password_fn)?;
|
||||||
|
config::tape_encryption_keys::insert_key(key, key_config, hint)?;
|
||||||
|
} else {
|
||||||
|
bail!("media does not contain any encryption key configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}).await?
|
||||||
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
properties: {
|
properties: {
|
||||||
@ -472,7 +513,7 @@ fn write_media_label(
|
|||||||
type: MediaIdFlat,
|
type: MediaIdFlat,
|
||||||
},
|
},
|
||||||
)]
|
)]
|
||||||
/// Read media label
|
/// Read media label (optionally inventorize media)
|
||||||
pub async fn read_label(
|
pub async fn read_label(
|
||||||
drive: String,
|
drive: String,
|
||||||
inventorize: Option<bool>,
|
inventorize: Option<bool>,
|
||||||
@ -483,7 +524,7 @@ pub async fn read_label(
|
|||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut drive = open_drive(&config, &drive)?;
|
let mut drive = open_drive(&config, &drive)?;
|
||||||
|
|
||||||
let media_id = drive.read_label()?;
|
let (media_id, _key_config) = drive.read_label()?;
|
||||||
|
|
||||||
let media_id = match media_id {
|
let media_id = match media_id {
|
||||||
Some(media_id) => {
|
Some(media_id) => {
|
||||||
@ -723,10 +764,10 @@ pub fn update_inventory(
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
worker.warn(format!("unable to read label form media '{}' - {}", label_text, err));
|
worker.warn(format!("unable to read label form media '{}' - {}", label_text, err));
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok((None, _)) => {
|
||||||
worker.log(format!("media '{}' is empty", label_text));
|
worker.log(format!("media '{}' is empty", label_text));
|
||||||
}
|
}
|
||||||
Ok(Some(media_id)) => {
|
Ok((Some(media_id), _key_config)) => {
|
||||||
if label_text != media_id.label.label_text {
|
if label_text != media_id.label.label_text {
|
||||||
worker.warn(format!("label text missmatch ({} != {})", label_text, media_id.label.label_text));
|
worker.warn(format!("label text missmatch ({} != {})", label_text, media_id.label.label_text));
|
||||||
continue;
|
continue;
|
||||||
@ -970,14 +1011,20 @@ pub fn catalog_media(
|
|||||||
drive.rewind()?;
|
drive.rewind()?;
|
||||||
|
|
||||||
let media_id = match drive.read_label()? {
|
let media_id = match drive.read_label()? {
|
||||||
Some(media_id) => {
|
(Some(media_id), key_config) => {
|
||||||
worker.log(format!(
|
worker.log(format!(
|
||||||
"found media label: {}",
|
"found media label: {}",
|
||||||
serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
|
serde_json::to_string_pretty(&serde_json::to_value(&media_id)?)?
|
||||||
));
|
));
|
||||||
|
if key_config.is_some() {
|
||||||
|
worker.log(format!(
|
||||||
|
"encryption key config: {}",
|
||||||
|
serde_json::to_string_pretty(&serde_json::to_value(&key_config)?)?
|
||||||
|
));
|
||||||
|
}
|
||||||
media_id
|
media_id
|
||||||
},
|
},
|
||||||
None => bail!("media is empty (no media label found)"),
|
(None, _) => bail!("media is empty (no media label found)"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_path = Path::new(TAPE_STATUS_DIR);
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
||||||
|
@ -22,10 +22,13 @@ use crate::tools::format::{as_fingerprint, bytes_as_fingerprint};
|
|||||||
use proxmox::api::api;
|
use proxmox::api::api;
|
||||||
|
|
||||||
// openssl::sha::sha256(b"Proxmox Backup Encryption Key Fingerprint")
|
// openssl::sha::sha256(b"Proxmox Backup Encryption Key Fingerprint")
|
||||||
const FINGERPRINT_INPUT: [u8; 32] = [ 110, 208, 239, 119, 71, 31, 255, 77,
|
/// This constant is used to compute fingerprints.
|
||||||
|
const FINGERPRINT_INPUT: [u8; 32] = [
|
||||||
|
110, 208, 239, 119, 71, 31, 255, 77,
|
||||||
85, 199, 168, 254, 74, 157, 182, 33,
|
85, 199, 168, 254, 74, 157, 182, 33,
|
||||||
97, 64, 127, 19, 76, 114, 93, 223,
|
97, 64, 127, 19, 76, 114, 93, 223,
|
||||||
48, 153, 45, 37, 236, 69, 237, 38, ];
|
48, 153, 45, 37, 236, 69, 237, 38,
|
||||||
|
];
|
||||||
#[api(default: "encrypt")]
|
#[api(default: "encrypt")]
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
@ -30,7 +30,7 @@ impl Default for Kdf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
pub enum KeyDerivationConfig {
|
pub enum KeyDerivationConfig {
|
||||||
Scrypt {
|
Scrypt {
|
||||||
n: u64,
|
n: u64,
|
||||||
@ -82,7 +82,7 @@ impl KeyDerivationConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
pub struct KeyConfig {
|
pub struct KeyConfig {
|
||||||
pub kdf: Option<KeyDerivationConfig>,
|
pub kdf: Option<KeyDerivationConfig>,
|
||||||
#[serde(with = "proxmox::tools::serde::epoch_as_rfc3339")]
|
#[serde(with = "proxmox::tools::serde::epoch_as_rfc3339")]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use anyhow::Error;
|
use anyhow::{bail, Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use proxmox::{
|
use proxmox::{
|
||||||
@ -8,11 +8,16 @@ use proxmox::{
|
|||||||
RpcEnvironment,
|
RpcEnvironment,
|
||||||
ApiHandler,
|
ApiHandler,
|
||||||
},
|
},
|
||||||
|
sys::linux::tty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use proxmox_backup::{
|
use proxmox_backup::{
|
||||||
|
config,
|
||||||
api2::{
|
api2::{
|
||||||
self,
|
self,
|
||||||
|
types::{
|
||||||
|
DRIVE_NAME_SCHEMA,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
config::tape_encryption_keys::complete_key_fingerprint,
|
config::tape_encryption_keys::complete_key_fingerprint,
|
||||||
};
|
};
|
||||||
@ -23,7 +28,11 @@ pub fn encryption_key_commands() -> CommandLineInterface {
|
|||||||
.insert("list", CliCommand::new(&API_METHOD_LIST_KEYS))
|
.insert("list", CliCommand::new(&API_METHOD_LIST_KEYS))
|
||||||
.insert(
|
.insert(
|
||||||
"create",
|
"create",
|
||||||
CliCommand::new(&api2::config::tape_encryption_keys::API_METHOD_CREATE_KEY)
|
CliCommand::new(&API_METHOD_CREATE_KEY)
|
||||||
|
)
|
||||||
|
.insert(
|
||||||
|
"restore",
|
||||||
|
CliCommand::new(&API_METHOD_RESTORE_KEY)
|
||||||
)
|
)
|
||||||
.insert(
|
.insert(
|
||||||
"remove",
|
"remove",
|
||||||
@ -36,6 +45,79 @@ pub fn encryption_key_commands() -> CommandLineInterface {
|
|||||||
cmd_def.into()
|
cmd_def.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
drive: {
|
||||||
|
schema: DRIVE_NAME_SCHEMA,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Restore encryption key from tape (read password from stdin)
|
||||||
|
async fn restore_key(
|
||||||
|
mut param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let (config, _digest) = config::drive::config()?;
|
||||||
|
param["drive"] = crate::lookup_drive_name(¶m, &config)?.into();
|
||||||
|
|
||||||
|
if !tty::stdin_isatty() {
|
||||||
|
bail!("no password input mechanism available");
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = tty::read_password("Tepe Encryption Key Password: ")?;
|
||||||
|
param["password"] = String::from_utf8(password)?.into();
|
||||||
|
|
||||||
|
let info = &api2::tape::drive::API_METHOD_RESTORE_KEY;
|
||||||
|
match info.handler {
|
||||||
|
ApiHandler::Async(handler) => (handler)(param, info, rpcenv).await?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
hint: {
|
||||||
|
description: "Password restore hint.",
|
||||||
|
type: String,
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Create key (read password from stdin)
|
||||||
|
fn create_key(
|
||||||
|
mut param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
|
if !tty::stdin_isatty() {
|
||||||
|
bail!("no password input mechanism available");
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = tty::read_and_verify_password("Tape Encryption Key Password: ")?;
|
||||||
|
|
||||||
|
param["password"] = String::from_utf8(password)?.into();
|
||||||
|
|
||||||
|
let info = &api2::config::tape_encryption_keys::API_METHOD_CREATE_KEY;
|
||||||
|
let fingerprint = match info.handler {
|
||||||
|
ApiHandler::Sync(handler) => (handler)(param, info, rpcenv)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{}", fingerprint);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashSet, HashMap};
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, Error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use openssl::sha::sha256;
|
|
||||||
|
|
||||||
use proxmox::tools::fs::{
|
use proxmox::tools::fs::{
|
||||||
file_read_optional_string,
|
file_read_optional_string,
|
||||||
replace_file,
|
replace_file,
|
||||||
|
open_file_locked,
|
||||||
CreateOptions,
|
CreateOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backup::{
|
backup::{
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
|
Kdf,
|
||||||
|
KeyConfig,
|
||||||
|
CryptConfig,
|
||||||
|
encrypt_key_with_passphrase,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,29 +45,53 @@ mod hex_key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store Hardware Encryption keys
|
/// Store Hardware Encryption keys (private part)
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct EncryptionKeyInfo {
|
pub struct EncryptionKeyInfo {
|
||||||
pub hint: String,
|
pub fingerprint: Fingerprint,
|
||||||
#[serde(with = "hex_key")]
|
#[serde(with = "hex_key")]
|
||||||
pub key: [u8; 32],
|
pub key: [u8; 32],
|
||||||
pub fingerprint: Fingerprint,
|
}
|
||||||
|
|
||||||
|
/// Store Hardware Encryption keys (public part)
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub struct EncryptionKeyConfig {
|
||||||
|
pub hint: String,
|
||||||
|
pub key_config: KeyConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_tape_key_fingerprint(key: &[u8; 32]) -> Result<Fingerprint, Error> {
|
||||||
|
let crypt_config = CryptConfig::new(key.clone())?;
|
||||||
|
Ok(crypt_config.fingerprint())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_tape_encryption_key(password: &[u8]) -> Result<([u8; 32], KeyConfig), Error> {
|
||||||
|
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
proxmox::sys::linux::fill_with_random_data(&mut key)?;
|
||||||
|
|
||||||
|
let mut key_config = encrypt_key_with_passphrase(&key, password, Kdf::Scrypt)?;
|
||||||
|
key_config.fingerprint = Some(compute_tape_key_fingerprint(&key)?);
|
||||||
|
Ok((key, key_config))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EncryptionKeyInfo {
|
impl EncryptionKeyInfo {
|
||||||
|
pub fn new(key: [u8; 32], fingerprint: Fingerprint) -> Self {
|
||||||
pub fn new(key: &[u8; 32], hint: String) -> Self {
|
Self { fingerprint, key }
|
||||||
Self {
|
|
||||||
hint,
|
|
||||||
key: key.clone(),
|
|
||||||
fingerprint: Fingerprint::new(sha256(key)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EncryptionKeyConfig {
|
||||||
|
pub fn new(key_config: KeyConfig, hint: String) -> Self {
|
||||||
|
Self { hint, key_config }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const TAPE_KEYS_FILENAME: &str = "/etc/proxmox-backup/tape-encryption-keys.json";
|
pub const TAPE_KEYS_FILENAME: &str = "/etc/proxmox-backup/tape-encryption-keys.json";
|
||||||
|
pub const TAPE_KEY_CONFIG_FILENAME: &str = "/etc/proxmox-backup/tape-encryption-key-config.json";
|
||||||
pub const TAPE_KEYS_LOCKFILE: &str = "/etc/proxmox-backup/.tape-encryption-keys.lck";
|
pub const TAPE_KEYS_LOCKFILE: &str = "/etc/proxmox-backup/.tape-encryption-keys.lck";
|
||||||
|
|
||||||
|
/// Load tape encryption keys (private part)
|
||||||
pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8;32]), Error> {
|
pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8;32]), Error> {
|
||||||
|
|
||||||
let content = file_read_optional_string(TAPE_KEYS_FILENAME)?;
|
let content = file_read_optional_string(TAPE_KEYS_FILENAME)?;
|
||||||
@ -71,12 +99,12 @@ pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8;32])
|
|||||||
|
|
||||||
let digest = openssl::sha::sha256(content.as_bytes());
|
let digest = openssl::sha::sha256(content.as_bytes());
|
||||||
|
|
||||||
let list: Vec<EncryptionKeyInfo> = serde_json::from_str(&content)?;
|
let key_list: Vec<EncryptionKeyInfo> = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
for item in list {
|
for item in key_list {
|
||||||
let expected_fingerprint = Fingerprint::new(sha256(&item.key));
|
let expected_fingerprint = compute_tape_key_fingerprint(&item.key)?;
|
||||||
if item.fingerprint != expected_fingerprint {
|
if item.fingerprint != expected_fingerprint {
|
||||||
bail!(
|
bail!(
|
||||||
"inconsistent fingerprint ({} != {})",
|
"inconsistent fingerprint ({} != {})",
|
||||||
@ -85,7 +113,39 @@ pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8;32])
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
map.insert(item.fingerprint.clone(), item);
|
if map.insert(item.fingerprint.clone(), item).is_some() {
|
||||||
|
bail!("found duplicate fingerprint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((map, digest))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load tape encryption key configurations (public part)
|
||||||
|
pub fn load_key_configs() -> Result<(HashMap<Fingerprint, EncryptionKeyConfig>, [u8;32]), Error> {
|
||||||
|
|
||||||
|
let content = file_read_optional_string(TAPE_KEY_CONFIG_FILENAME)?;
|
||||||
|
let content = content.unwrap_or_else(|| String::from("[]"));
|
||||||
|
|
||||||
|
let digest = openssl::sha::sha256(content.as_bytes());
|
||||||
|
|
||||||
|
let key_list: Vec<EncryptionKeyConfig> = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
let mut hint_set = HashSet::new();
|
||||||
|
|
||||||
|
for item in key_list {
|
||||||
|
match item.key_config.fingerprint {
|
||||||
|
Some(ref fingerprint) => {
|
||||||
|
if !hint_set.insert(item.hint.clone()) {
|
||||||
|
bail!("found duplicate password hint '{}'", item.hint);
|
||||||
|
}
|
||||||
|
if map.insert(fingerprint.clone(), item).is_some() {
|
||||||
|
bail!("found duplicate fingerprint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => bail!("missing fingerprint"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((map, digest))
|
Ok((map, digest))
|
||||||
@ -114,13 +174,73 @@ pub fn save_keys(map: HashMap<Fingerprint, EncryptionKeyInfo>) -> Result<(), Err
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_key_configs(map: HashMap<Fingerprint, EncryptionKeyConfig>) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let mut list = Vec::new();
|
||||||
|
|
||||||
|
let mut hint_set = HashSet::new();
|
||||||
|
|
||||||
|
for (_fp, item) in map {
|
||||||
|
if !hint_set.insert(item.hint.clone()) {
|
||||||
|
bail!("found duplicate password hint '{}'", item.hint);
|
||||||
|
}
|
||||||
|
list.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = serde_json::to_string_pretty(&list)?;
|
||||||
|
|
||||||
|
let backup_user = crate::backup::backup_user()?;
|
||||||
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
|
||||||
|
// set the correct owner/group/permissions while saving file
|
||||||
|
// owner(rw) = root, group(r)= backup
|
||||||
|
let options = CreateOptions::new()
|
||||||
|
.perm(mode)
|
||||||
|
.owner(nix::unistd::ROOT)
|
||||||
|
.group(backup_user.gid);
|
||||||
|
|
||||||
|
replace_file(TAPE_KEY_CONFIG_FILENAME, raw.as_bytes(), options)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_key(key: [u8;32], key_config: KeyConfig, hint: String) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let _lock = open_file_locked(
|
||||||
|
TAPE_KEYS_LOCKFILE,
|
||||||
|
std::time::Duration::new(10, 0),
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (mut key_map, _) = load_keys()?;
|
||||||
|
let (mut config_map, _) = load_key_configs()?;
|
||||||
|
|
||||||
|
let fingerprint = match key_config.fingerprint.clone() {
|
||||||
|
Some(fingerprint) => fingerprint,
|
||||||
|
None => bail!("missing encryption key fingerprint - internal error"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(_) = config_map.get(&fingerprint) {
|
||||||
|
bail!("encryption key '{}' already exists.", fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = EncryptionKeyInfo::new(key, fingerprint.clone());
|
||||||
|
key_map.insert(fingerprint.clone(), item);
|
||||||
|
save_keys(key_map)?;
|
||||||
|
|
||||||
|
let item = EncryptionKeyConfig::new(key_config, hint);
|
||||||
|
config_map.insert(fingerprint.clone(), item);
|
||||||
|
save_key_configs(config_map)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// shell completion helper
|
// shell completion helper
|
||||||
pub fn complete_key_fingerprint(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
pub fn complete_key_fingerprint(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
|
||||||
let data = match load_keys() {
|
let data = match load_key_configs() {
|
||||||
Ok((data, _digest)) => data,
|
Ok((data, _digest)) => data,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
data.keys().map(|fp| crate::tools::format::as_fingerprint(fp.bytes())).collect()
|
data.keys().map(|fp| crate::tools::format::as_fingerprint(fp.bytes())).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,10 @@ use proxmox::sys::error::SysResult;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config,
|
config,
|
||||||
backup::Fingerprint,
|
backup::{
|
||||||
|
Fingerprint,
|
||||||
|
KeyConfig,
|
||||||
|
},
|
||||||
tools::run_command,
|
tools::run_command,
|
||||||
api2::types::{
|
api2::types::{
|
||||||
TapeDensity,
|
TapeDensity,
|
||||||
@ -448,7 +451,11 @@ impl TapeDriver for LinuxTapeHandle {
|
|||||||
Ok(Box::new(handle))
|
Ok(Box::new(handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<(), Error> {
|
fn write_media_set_label(
|
||||||
|
&mut self,
|
||||||
|
media_set_label: &MediaSetLabel,
|
||||||
|
key_config: Option<&KeyConfig>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
let file_number = self.current_file_number()?;
|
let file_number = self.current_file_number()?;
|
||||||
if file_number != 1 {
|
if file_number != 1 {
|
||||||
@ -466,7 +473,20 @@ impl TapeDriver for LinuxTapeHandle {
|
|||||||
let mut handle = TapeWriterHandle {
|
let mut handle = TapeWriterHandle {
|
||||||
writer: BlockedWriter::new(&mut self.file),
|
writer: BlockedWriter::new(&mut self.file),
|
||||||
};
|
};
|
||||||
let raw = serde_json::to_string_pretty(&serde_json::to_value(media_set_label)?)?;
|
|
||||||
|
let mut value = serde_json::to_value(media_set_label)?;
|
||||||
|
if media_set_label.encryption_key_fingerprint.is_some() {
|
||||||
|
match key_config {
|
||||||
|
Some(key_config) => {
|
||||||
|
value["key-config"] = serde_json::to_value(key_config)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bail!("missing encryption key config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = serde_json::to_string_pretty(&value)?;
|
||||||
|
|
||||||
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
|
let header = MediaContentHeader::new(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, raw.len() as u32);
|
||||||
handle.write_header(&header, raw.as_bytes())?;
|
handle.write_header(&header, raw.as_bytes())?;
|
||||||
|
@ -20,12 +20,16 @@ pub use linux_list_drives::*;
|
|||||||
|
|
||||||
use anyhow::{bail, format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use ::serde::{Deserialize};
|
use ::serde::{Deserialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use proxmox::tools::io::ReadExt;
|
use proxmox::tools::io::ReadExt;
|
||||||
use proxmox::api::section_config::SectionConfigData;
|
use proxmox::api::section_config::SectionConfigData;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
backup::Fingerprint,
|
backup::{
|
||||||
|
Fingerprint,
|
||||||
|
KeyConfig,
|
||||||
|
},
|
||||||
api2::types::{
|
api2::types::{
|
||||||
VirtualTapeDrive,
|
VirtualTapeDrive,
|
||||||
LinuxTapeDrive,
|
LinuxTapeDrive,
|
||||||
@ -101,18 +105,26 @@ pub trait TapeDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Write the media set label to tape
|
/// Write the media set label to tape
|
||||||
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<(), Error>;
|
///
|
||||||
|
/// If the media-set is encrypted, we also store the encryption
|
||||||
|
/// key_config, so that it is possible to restore the key.
|
||||||
|
fn write_media_set_label(
|
||||||
|
&mut self,
|
||||||
|
media_set_label: &MediaSetLabel,
|
||||||
|
key_config: Option<&KeyConfig>,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
/// Read the media label
|
/// Read the media label
|
||||||
///
|
///
|
||||||
/// This tries to read both media labels (label and media_set_label).
|
/// This tries to read both media labels (label and
|
||||||
fn read_label(&mut self) -> Result<Option<MediaId>, Error> {
|
/// media_set_label). Also returns the optional encryption key configuration.
|
||||||
|
fn read_label(&mut self) -> Result<(Option<MediaId>, Option<KeyConfig>), Error> {
|
||||||
|
|
||||||
self.rewind()?;
|
self.rewind()?;
|
||||||
|
|
||||||
let label = {
|
let label = {
|
||||||
let mut reader = match self.read_next_file()? {
|
let mut reader = match self.read_next_file()? {
|
||||||
None => return Ok(None), // tape is empty
|
None => return Ok((None, None)), // tape is empty
|
||||||
Some(reader) => reader,
|
Some(reader) => reader,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -135,7 +147,7 @@ pub trait TapeDriver {
|
|||||||
|
|
||||||
// try to read MediaSet label
|
// try to read MediaSet label
|
||||||
let mut reader = match self.read_next_file()? {
|
let mut reader = match self.read_next_file()? {
|
||||||
None => return Ok(Some(media_id)),
|
None => return Ok((Some(media_id), None)),
|
||||||
Some(reader) => reader,
|
Some(reader) => reader,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,7 +155,17 @@ pub trait TapeDriver {
|
|||||||
header.check(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, 1, 64*1024)?;
|
header.check(PROXMOX_BACKUP_MEDIA_SET_LABEL_MAGIC_1_0, 1, 64*1024)?;
|
||||||
let data = reader.read_exact_allocated(header.size as usize)?;
|
let data = reader.read_exact_allocated(header.size as usize)?;
|
||||||
|
|
||||||
let media_set_label: MediaSetLabel = serde_json::from_slice(&data)
|
let mut data: Value = serde_json::from_slice(&data)
|
||||||
|
.map_err(|err| format_err!("unable to parse media set label - {}", err))?;
|
||||||
|
|
||||||
|
let key_config_value = data["key-config"].take();
|
||||||
|
let key_config: Option<KeyConfig> = if !key_config_value.is_null() {
|
||||||
|
Some(serde_json::from_value(key_config_value)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let media_set_label: MediaSetLabel = serde_json::from_value(data)
|
||||||
.map_err(|err| format_err!("unable to parse media set label - {}", err))?;
|
.map_err(|err| format_err!("unable to parse media set label - {}", err))?;
|
||||||
|
|
||||||
// make sure we read the EOF marker
|
// make sure we read the EOF marker
|
||||||
@ -153,7 +175,7 @@ pub trait TapeDriver {
|
|||||||
|
|
||||||
media_id.media_set_label = Some(media_set_label);
|
media_id.media_set_label = Some(media_set_label);
|
||||||
|
|
||||||
Ok(Some(media_id))
|
Ok((Some(media_id), key_config))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eject media
|
/// Eject media
|
||||||
@ -278,7 +300,7 @@ pub fn request_and_load_media(
|
|||||||
), Error> {
|
), Error> {
|
||||||
|
|
||||||
let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox::tools::Uuid| {
|
let check_label = |handle: &mut dyn TapeDriver, uuid: &proxmox::tools::Uuid| {
|
||||||
if let Ok(Some(media_id)) = handle.read_label() {
|
if let Ok((Some(media_id), _)) = handle.read_label() {
|
||||||
worker.log(format!(
|
worker.log(format!(
|
||||||
"found media label {} ({})",
|
"found media label {} ({})",
|
||||||
media_id.label.label_text,
|
media_id.label.label_text,
|
||||||
@ -348,7 +370,7 @@ pub fn request_and_load_media(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match handle.read_label() {
|
match handle.read_label() {
|
||||||
Ok(Some(media_id)) => {
|
Ok((Some(media_id), _)) => {
|
||||||
if media_id.label.uuid == label.uuid {
|
if media_id.label.uuid == label.uuid {
|
||||||
worker.log(format!(
|
worker.log(format!(
|
||||||
"found media label {} ({})",
|
"found media label {} ({})",
|
||||||
@ -367,7 +389,7 @@ pub fn request_and_load_media(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok((None, _)) => {
|
||||||
if last_media_uuid.is_some() {
|
if last_media_uuid.is_some() {
|
||||||
worker.log(format!("found empty media without label (please label all tapes first)"));
|
worker.log(format!("found empty media without label (please label all tapes first)"));
|
||||||
last_media_uuid = None;
|
last_media_uuid = None;
|
||||||
|
@ -11,6 +11,7 @@ use proxmox::tools::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
backup::KeyConfig,
|
||||||
tape::{
|
tape::{
|
||||||
TapeWrite,
|
TapeWrite,
|
||||||
TapeRead,
|
TapeRead,
|
||||||
@ -325,10 +326,18 @@ impl TapeDriver for VirtualTapeHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_media_set_label(&mut self, media_set_label: &MediaSetLabel) -> Result<(), Error> {
|
fn write_media_set_label(
|
||||||
|
&mut self,
|
||||||
|
media_set_label: &MediaSetLabel,
|
||||||
|
key_config: Option<&KeyConfig>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
|
||||||
self.set_encryption(None)?;
|
self.set_encryption(None)?;
|
||||||
|
|
||||||
|
if key_config.is_some() {
|
||||||
|
bail!("encryption is not implemented - internal error");
|
||||||
|
}
|
||||||
|
|
||||||
let mut status = self.load_status()?;
|
let mut status = self.load_status()?;
|
||||||
match status.current_tape {
|
match status.current_tape {
|
||||||
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
Some(VirtualTapeStatus { ref name, ref mut pos }) => {
|
||||||
|
@ -30,6 +30,7 @@ use crate::{
|
|||||||
media_changer,
|
media_changer,
|
||||||
file_formats::MediaSetLabel,
|
file_formats::MediaSetLabel,
|
||||||
},
|
},
|
||||||
|
config::tape_encryption_keys::load_key_configs,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -452,12 +453,27 @@ fn update_media_set_label(
|
|||||||
Some(ref set) => set,
|
Some(ref set) => set,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let key_config = if let Some(ref fingerprint) = new_set.encryption_key_fingerprint {
|
||||||
|
let (config_map, _digest) = load_key_configs()?;
|
||||||
|
match config_map.get(fingerprint) {
|
||||||
|
Some(item) => {
|
||||||
|
// fixme: store item.hint??? should be in key-config instead
|
||||||
|
Some(item.key_config.clone())
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
bail!("unable to find tape encryption key config '{}'", fingerprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let status_path = Path::new(TAPE_STATUS_DIR);
|
let status_path = Path::new(TAPE_STATUS_DIR);
|
||||||
|
|
||||||
match old_set {
|
match old_set {
|
||||||
None => {
|
None => {
|
||||||
worker.log(format!("wrinting new media set label"));
|
worker.log(format!("wrinting new media set label"));
|
||||||
drive.write_media_set_label(new_set)?;
|
drive.write_media_set_label(new_set, key_config.as_ref())?;
|
||||||
media_catalog = MediaCatalog::overwrite(status_path, media_id, false)?;
|
media_catalog = MediaCatalog::overwrite(status_path, media_id, false)?;
|
||||||
}
|
}
|
||||||
Some(media_set_label) => {
|
Some(media_set_label) => {
|
||||||
@ -476,7 +492,7 @@ fn update_media_set_label(
|
|||||||
media_set_label.uuid.to_string(), media_set_label.seq_nr)
|
media_set_label.uuid.to_string(), media_set_label.seq_nr)
|
||||||
);
|
);
|
||||||
|
|
||||||
drive.write_media_set_label(new_set)?;
|
drive.write_media_set_label(new_set, key_config.as_ref())?;
|
||||||
media_catalog = MediaCatalog::overwrite(status_path, media_id, false)?;
|
media_catalog = MediaCatalog::overwrite(status_path, media_id, false)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user