tape: implement encrypted backup - simple version

This is just a proof of concept, only storing the encryption key fingerprint
inside the media-set label.
This commit is contained in:
Dietmar Maurer 2021-01-18 13:36:11 +01:00
parent 84cbdb35c4
commit 8a0046f519
8 changed files with 145 additions and 88 deletions

View File

@ -406,7 +406,7 @@ fn write_media_label(
if let Some(ref pool) = pool { if let Some(ref pool) = pool {
// assign media to pool by writing special media set label // assign media to pool by writing special media set 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); 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)?;
media_set_label = Some(set); media_set_label = Some(set);
@ -493,6 +493,7 @@ pub async fn read_label(
ctime: media_id.label.ctime, ctime: media_id.label.ctime,
media_set_ctime: None, media_set_ctime: None,
media_set_uuid: None, media_set_uuid: None,
encryption_key_fingerprint: None,
pool: None, pool: None,
seq_nr: None, seq_nr: None,
}; };
@ -501,6 +502,10 @@ pub async fn read_label(
flat.seq_nr = Some(set.seq_nr); flat.seq_nr = Some(set.seq_nr);
flat.media_set_uuid = Some(set.uuid.to_string()); flat.media_set_uuid = Some(set.uuid.to_string());
flat.media_set_ctime = Some(set.ctime); flat.media_set_ctime = Some(set.ctime);
flat.encryption_key_fingerprint = set
.encryption_key_fingerprint
.as_ref()
.map(|fp| crate::tools::format::as_fingerprint(fp.bytes()));
} }
if let Some(true) = inventorize { if let Some(true) = inventorize {
@ -992,6 +997,9 @@ pub fn catalog_media(
MediaCatalog::destroy(status_path, &media_id.label.uuid)?; MediaCatalog::destroy(status_path, &media_id.label.uuid)?;
return Ok(()); return Ok(());
} }
let encrypt_fingerprint = set.encryption_key_fingerprint.clone();
drive.set_encryption(encrypt_fingerprint)?;
set.pool.clone() set.pool.clone()
} }
}; };

View File

@ -74,6 +74,9 @@ pub struct MediaIdFlat {
/// MediaSet Creation time stamp /// MediaSet Creation time stamp
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub media_set_ctime: Option<i64>, pub media_set_ctime: Option<i64>,
/// Encryption key fingerprint
#[serde(skip_serializing_if="Option::is_none")]
pub encryption_key_fingerprint: Option<String>,
} }
#[api()] #[api()]

View File

@ -422,6 +422,7 @@ async fn read_label(
.column(ColumnConfig::new("pool")) .column(ColumnConfig::new("pool"))
.column(ColumnConfig::new("media-set-uuid")) .column(ColumnConfig::new("media-set-uuid"))
.column(ColumnConfig::new("media-set-ctime").renderer(render_epoch)) .column(ColumnConfig::new("media-set-ctime").renderer(render_epoch))
.column(ColumnConfig::new("encryption-key-fingerprint"))
; ;
format_and_print_result_full(&mut data, &info.returns, &output_format, &options); format_and_print_result_full(&mut data, &info.returns, &output_format, &options);

View File

@ -8,7 +8,7 @@
use std::fs::File; use std::fs::File;
use std::os::unix::io::{AsRawFd, FromRawFd}; use std::os::unix::io::{AsRawFd, FromRawFd};
use anyhow::{bail, format_err, Error}; use anyhow::{bail, Error};
use serde_json::Value; use serde_json::Value;
use proxmox::{ use proxmox::{

View File

@ -7,6 +7,8 @@ use bitflags::bitflags;
use proxmox::tools::Uuid; use proxmox::tools::Uuid;
use crate::backup::Fingerprint;
/// We use 256KB blocksize (always) /// We use 256KB blocksize (always)
pub const PROXMOX_TAPE_BLOCK_SIZE: usize = 256*1024; pub const PROXMOX_TAPE_BLOCK_SIZE: usize = 256*1024;
@ -185,16 +187,26 @@ pub struct MediaSetLabel {
pub seq_nr: u64, pub seq_nr: u64,
/// Creation time stamp /// Creation time stamp
pub ctime: i64, pub ctime: i64,
/// Encryption key finkerprint (if encryped)
#[serde(skip_serializing_if="Option::is_none")]
pub encryption_key_fingerprint: Option<Fingerprint>,
} }
impl MediaSetLabel { impl MediaSetLabel {
pub fn with_data(pool: &str, uuid: Uuid, seq_nr: u64, ctime: i64) -> Self { pub fn with_data(
pool: &str,
uuid: Uuid,
seq_nr: u64,
ctime: i64,
encryption_key_fingerprint: Option<Fingerprint>,
) -> Self {
Self { Self {
pool: pool.to_string(), pool: pool.to_string(),
uuid, uuid,
seq_nr, seq_nr,
ctime, ctime,
encryption_key_fingerprint,
} }
} }
} }

View File

@ -565,7 +565,7 @@ impl Inventory {
let uuid = label.uuid.clone(); let uuid = label.uuid.clone();
let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime); let set = MediaSetLabel::with_data(pool, [0u8; 16].into(), 0, ctime, None);
self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap(); self.store(MediaId { label, media_set_label: Some(set) }, false).unwrap();

View File

@ -14,6 +14,7 @@ use ::serde::{Deserialize, Serialize};
use proxmox::tools::Uuid; use proxmox::tools::Uuid;
use crate::{ use crate::{
backup::Fingerprint,
api2::types::{ api2::types::{
MediaStatus, MediaStatus,
MediaLocation, MediaLocation,
@ -44,6 +45,7 @@ pub struct MediaPool {
media_set_policy: MediaSetPolicy, media_set_policy: MediaSetPolicy,
retention: RetentionPolicy, retention: RetentionPolicy,
use_offline_media: bool, use_offline_media: bool,
encrypt_fingerprint: Option<Fingerprint>,
inventory: Inventory, inventory: Inventory,
@ -59,6 +61,7 @@ impl MediaPool {
media_set_policy: MediaSetPolicy, media_set_policy: MediaSetPolicy,
retention: RetentionPolicy, retention: RetentionPolicy,
use_offline_media: bool, use_offline_media: bool,
encrypt_fingerprint: Option<Fingerprint>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let inventory = Inventory::load(state_path)?; let inventory = Inventory::load(state_path)?;
@ -75,6 +78,7 @@ impl MediaPool {
use_offline_media, use_offline_media,
inventory, inventory,
current_media_set, current_media_set,
encrypt_fingerprint,
}) })
} }
@ -89,7 +93,19 @@ impl MediaPool {
let retention = config.retention.clone().unwrap_or(String::from("keep")).parse()?; let retention = config.retention.clone().unwrap_or(String::from("keep")).parse()?;
MediaPool::new(&config.name, state_path, allocation, retention, use_offline_media) let encrypt_fingerprint = match config.encrypt {
Some(ref fingerprint) => Some(fingerprint.parse()?),
None => None,
};
MediaPool::new(
&config.name,
state_path,
allocation,
retention,
use_offline_media,
encrypt_fingerprint,
)
} }
/// Returns the pool name /// Returns the pool name
@ -97,6 +113,12 @@ impl MediaPool {
&self.name &self.name
} }
/// Retruns encryption settings
pub fn encrypt_fingerprint(&self) -> Option<Fingerprint> {
self.encrypt_fingerprint.clone()
}
fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) { fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) {
let (status, location) = self.inventory.status_and_location(&media_id.label.uuid); let (status, location) = self.inventory.status_and_location(&media_id.label.uuid);
@ -247,13 +269,49 @@ impl MediaPool {
current_time > expire_time current_time > expire_time
} }
// check if a location is considered on site
pub fn location_is_available(&self, location: &MediaLocation) -> bool {
match location {
MediaLocation::Online(_) => true,
MediaLocation::Offline => self.use_offline_media,
MediaLocation::Vault(_) => false,
}
}
fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> {
let seq_nr = self.current_media_set.media_list().len() as u64;
let pool = self.name.clone();
let encrypt_fingerprint = self.encrypt_fingerprint();
let set = MediaSetLabel::with_data(
&pool,
self.current_media_set.uuid().clone(),
seq_nr,
current_time,
encrypt_fingerprint,
);
media_id.media_set_label = Some(set);
let uuid = media_id.label.uuid.clone();
let clear_media_status = true; // remove Full status
self.inventory.store(media_id, clear_media_status)?; // store persistently
self.current_media_set.add_media(uuid);
Ok(())
}
/// Allocates a writable media to the current media set /// Allocates a writable media to the current media set
pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> { pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> {
let last_is_writable = self.current_set_usable()?; let last_is_writable = self.current_set_usable()?;
let pool = self.name.clone();
if last_is_writable { if last_is_writable {
let last_uuid = self.current_media_set.last_media_uuid().unwrap(); let last_uuid = self.current_media_set.last_media_uuid().unwrap();
let media = self.lookup_media(last_uuid)?; let media = self.lookup_media(last_uuid)?;
@ -262,88 +320,65 @@ impl MediaPool {
// try to find empty media in pool, add to media set // try to find empty media in pool, add to media set
let mut media_list = self.list_media(); let media_list = self.list_media();
let mut empty_media = Vec::new(); let mut empty_media = Vec::new();
for media in media_list.iter_mut() { let mut used_media = Vec::new();
// already part of a media set?
if media.media_set_label().is_some() { continue; }
// check if media is on site for media in media_list.into_iter() {
match media.location() { if !self.location_is_available(media.location()) {
MediaLocation::Online(_) => { /* OK */ },
MediaLocation::Offline => {
if self.use_offline_media {
/* OK */
} else {
continue; continue;
} }
}, // already part of a media set?
MediaLocation::Vault(_) => continue, if media.media_set_label().is_some() {
} used_media.push(media);
} else {
// only consider writable media // only consider writable empty media
if media.status() != &MediaStatus::Writable { continue; } if media.status() == &MediaStatus::Writable {
empty_media.push(media); empty_media.push(media);
} }
}
}
// sort empty_media, oldest media first // sort empty_media, newest first -> oldest last
empty_media.sort_unstable_by_key(|media| media.label().ctime); empty_media.sort_unstable_by(|a, b| b.label().ctime.cmp(&a.label().ctime));
if let Some(media) = empty_media.first_mut() { if let Some(media) = empty_media.pop() {
// found empty media, add to media set an use it // found empty media, add to media set an use it
let seq_nr = self.current_media_set.media_list().len() as u64; let uuid = media.uuid().clone();
self.add_media_to_current_set(media.into_id(), current_time)?;
let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); return Ok(uuid);
media.set_media_set_label(set);
self.inventory.store(media.id().clone(), true)?; // store persistently
self.current_media_set.add_media(media.uuid().clone());
return Ok(media.uuid().clone());
} }
println!("no empty media in pool, try to reuse expired media"); println!("no empty media in pool, try to reuse expired media");
let mut expired_media = Vec::new(); let mut expired_media = Vec::new();
for media in media_list.into_iter() { for media in used_media.into_iter() {
if let Some(set) = media.media_set_label() { if let Some(set) = media.media_set_label() {
if &set.uuid == self.current_media_set.uuid() { if &set.uuid == self.current_media_set.uuid() {
continue; continue;
} }
} else {
continue;
} }
if self.media_is_expired(&media, current_time) { if self.media_is_expired(&media, current_time) {
println!("found expired media on media '{}'", media.label_text()); println!("found expired media on media '{}'", media.label_text());
expired_media.push(media); expired_media.push(media);
} }
} }
// sort, oldest media first // sort expired_media, newest first -> oldest last
expired_media.sort_unstable_by_key(|media| { expired_media.sort_unstable_by(|a, b| {
match media.media_set_label() { b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime)
None => 0, // should not happen here
Some(set) => set.ctime,
}
}); });
if let Some(media) = expired_media.first_mut() { if let Some(media) = expired_media.pop() {
println!("reuse expired media '{}'", media.label_text()); println!("reuse expired media '{}'", media.label_text());
let uuid = media.uuid().clone();
let seq_nr = self.current_media_set.media_list().len() as u64; self.add_media_to_current_set(media.into_id(), current_time)?;
let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); return Ok(uuid);
media.set_media_set_label(set);
let clear_media_status = true; // remove Full status
self.inventory.store(media.id().clone(), clear_media_status)?; // store persistently
self.current_media_set.add_media(media.uuid().clone());
return Ok(media.uuid().clone());
} }
println!("no expired media in pool, try to find unassigned/free media"); println!("no expired media in pool, try to find unassigned/free media");
@ -357,18 +392,9 @@ impl MediaPool {
let (status, location) = self.compute_media_state(&media_id); let (status, location) = self.compute_media_state(&media_id);
if media_id.media_set_label.is_some() { continue; } // should not happen if media_id.media_set_label.is_some() { continue; } // should not happen
// check if media is on site if !self.location_is_available(&location) {
match location {
MediaLocation::Online(_) => { /* OK */ },
MediaLocation::Offline => {
if self.use_offline_media {
/* OK */
} else {
continue; continue;
} }
},
MediaLocation::Vault(_) => continue,
}
// only consider writable media // only consider writable media
if status != MediaStatus::Writable { continue; } if status != MediaStatus::Writable { continue; }
@ -376,23 +402,13 @@ impl MediaPool {
free_media.push(media_id); free_media.push(media_id);
} }
if let Some(media) = free_media.first_mut() { if let Some(media_id) = free_media.pop() {
println!("use free media '{}'", media.label.label_text); println!("use free media '{}'", media_id.label.label_text);
let uuid = media_id.label.uuid.clone();
let seq_nr = self.current_media_set.media_list().len() as u64; self.add_media_to_current_set(media_id, current_time)?;
let set = MediaSetLabel::with_data(&pool, self.current_media_set.uuid().clone(), seq_nr, current_time); return Ok(uuid);
media.media_set_label = Some(set);
let clear_media_status = true; // remove Full status
self.inventory.store(media.clone(), clear_media_status)?; // store persistently
self.current_media_set.add_media(media.label.uuid.clone());
return Ok(media.label.uuid.clone());
} }
bail!("alloc writable media in pool '{}' failed: no usable media found", self.name()); bail!("alloc writable media in pool '{}' failed: no usable media found", self.name());
} }
@ -537,6 +553,11 @@ impl BackupMedia {
&self.id &self.id
} }
/// Returns the media id, consumes self)
pub fn into_id(self) -> MediaId {
self.id
}
/// Returns the media label (Barcode) /// Returns the media label (Barcode)
pub fn label_text(&self) -> &str { pub fn label_text(&self) -> &str {
&self.id.label.label_text &self.id.label.label_text

View File

@ -230,6 +230,15 @@ impl PoolWriter {
media.id(), media.id(),
)?; )?;
let encrypt_fingerprint = media
.media_set_label()
.as_ref()
.unwrap()
.encryption_key_fingerprint
.clone();
drive.set_encryption(encrypt_fingerprint)?;
self.status = Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 }); self.status = Some(PoolWriterState { drive, catalog, at_eom: false, bytes_written: 0 });
Ok(media_uuid) Ok(media_uuid)
@ -457,6 +466,9 @@ fn update_media_set_label(
bail!("got media with wrong media sequence number ({} != {}", bail!("got media with wrong media sequence number ({} != {}",
new_set.seq_nr,media_set_label.seq_nr); new_set.seq_nr,media_set_label.seq_nr);
} }
if new_set.encryption_key_fingerprint != media_set_label.encryption_key_fingerprint {
bail!("detected changed encryption fingerprint - internal error");
}
media_catalog = MediaCatalog::open(status_path, &media_id.label.uuid, true, false)?; media_catalog = MediaCatalog::open(status_path, &media_id.label.uuid, true, false)?;
} else { } else {
worker.log( worker.log(