diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index 65a7fabc..abe5af95 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -52,14 +52,14 @@ fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result { eprintln!("error during snapshot file listing: '{}'", err); - info.files.iter().map(|x| BackupContent { filename: x.to_string(), size: None, encrypted: None }).collect() + info + .files + .iter() + .map(|x| BackupContent { + filename: x.to_string(), + size: None, + crypt_mode: None, + }) + .collect() }, }; @@ -902,7 +914,7 @@ fn download_file_decoded( let files = read_backup_index(&datastore, &backup_dir)?; for file in files { - if file.filename == file_name && file.encrypted == Some(true) { + if file.filename == file_name && file.crypt_mode == Some(CryptMode::Encrypt) { bail!("cannot decode '{}' - is encrypted", file_name); } } diff --git a/src/api2/types.rs b/src/api2/types.rs index 4c86b1ce..7b902702 100644 --- a/src/api2/types.rs +++ b/src/api2/types.rs @@ -5,6 +5,8 @@ use proxmox::api::{api, schema::*}; use proxmox::const_regex; use proxmox::{IPRE, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32}; +use crate::backup::CryptMode; + // File names: may not contain slashes, may not start with "." pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| { if name.starts_with('.') { @@ -496,6 +498,10 @@ pub const PRUNE_SCHEMA_KEEP_YEARLY: Schema = IntegerSchema::new( "filename": { schema: BACKUP_ARCHIVE_NAME_SCHEMA, }, + "crypt-mode": { + type: CryptMode, + optional: true, + }, }, )] #[derive(Serialize, Deserialize)] @@ -503,9 +509,9 @@ pub const PRUNE_SCHEMA_KEEP_YEARLY: Schema = IntegerSchema::new( /// Basic information about archive files inside a backup snapshot. pub struct BackupContent { pub filename: String, - /// Info if file is encrypted (or empty if we do not have that info) + /// Info if file is encrypted, signed, or neither. #[serde(skip_serializing_if="Option::is_none")] - pub encrypted: Option, + pub crypt_mode: Option, /// Archive size (from backup manifest). #[serde(skip_serializing_if="Option::is_none")] pub size: Option, diff --git a/src/backup/crypt_config.rs b/src/backup/crypt_config.rs index 771d41e0..90d99b4f 100644 --- a/src/backup/crypt_config.rs +++ b/src/backup/crypt_config.rs @@ -6,12 +6,40 @@ //! See the Wikipedia Artikel for [Authenticated //! encryption](https://en.wikipedia.org/wiki/Authenticated_encryption) //! for a short introduction. -use anyhow::{bail, Error}; -use openssl::pkcs5::pbkdf2_hmac; -use openssl::hash::MessageDigest; -use openssl::symm::{decrypt_aead, Cipher, Crypter, Mode}; + use std::io::Write; + +use anyhow::{bail, Error}; use chrono::{Local, TimeZone, DateTime}; +use openssl::hash::MessageDigest; +use openssl::pkcs5::pbkdf2_hmac; +use openssl::symm::{decrypt_aead, Cipher, Crypter, Mode}; +use serde::{Deserialize, Serialize}; + +use proxmox::api::api; + +#[api(default: "encrypt")] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +/// Defines whether data is encrypted (using an AEAD cipher), only signed, or neither. +pub enum CryptMode { + /// Don't encrypt. + None, + /// Encrypt. + Encrypt, + /// Only sign. + SignOnly, +} + +impl CryptMode { + /// Maps values other than `None` to `SignOnly`. + pub fn sign_only(self) -> Self { + match self { + CryptMode::None => CryptMode::None, + _ => CryptMode::SignOnly, + } + } +} /// Encryption Configuration with secret key /// @@ -26,7 +54,6 @@ pub struct CryptConfig { id_pkey: openssl::pkey::PKey, // The private key used by the cipher. enc_key: [u8; 32], - } impl CryptConfig { diff --git a/src/backup/manifest.rs b/src/backup/manifest.rs index c7941b15..7aacca12 100644 --- a/src/backup/manifest.rs +++ b/src/backup/manifest.rs @@ -4,14 +4,14 @@ use std::path::Path; use serde_json::{json, Value}; -use crate::backup::BackupDir; +use crate::backup::{BackupDir, CryptMode}; pub const MANIFEST_BLOB_NAME: &str = "index.json.blob"; pub const CLIENT_LOG_BLOB_NAME: &str = "client.log.blob"; pub struct FileInfo { pub filename: String, - pub encrypted: Option, + pub crypt_mode: CryptMode, pub size: u64, pub csum: [u8; 32], } @@ -49,9 +49,9 @@ impl BackupManifest { Self { files: Vec::new(), snapshot } } - pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], encrypted: Option) -> Result<(), Error> { + pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], crypt_mode: CryptMode) -> Result<(), Error> { let _archive_type = archive_type(&filename)?; // check type - self.files.push(FileInfo { filename, size, csum, encrypted }); + self.files.push(FileInfo { filename, size, csum, crypt_mode }); Ok(()) } @@ -91,18 +91,12 @@ impl BackupManifest { "backup-time": self.snapshot.backup_time().timestamp(), "files": self.files.iter() .fold(Vec::new(), |mut acc, info| { - let mut value = json!({ + acc.push(json!({ "filename": info.filename, - "encrypted": info.encrypted, + "crypt-mode": info.crypt_mode, "size": info.size, "csum": proxmox::tools::digest_to_hex(&info.csum), - }); - - if let Some(encrypted) = info.encrypted { - value["encrypted"] = encrypted.into(); - } - - acc.push(value); + })); acc }) }) @@ -142,8 +136,8 @@ impl TryFrom for BackupManifest { let csum = required_string_property(item, "csum")?; let csum = proxmox::tools::hex_to_digest(csum)?; let size = required_integer_property(item, "size")? as u64; - let encrypted = item["encrypted"].as_bool(); - manifest.add_file(filename, size, csum, encrypted)?; + let crypt_mode: CryptMode = serde_json::from_value(item["crypt-mode"].clone())?; + manifest.add_file(filename, size, csum, crypt_mode)?; } if manifest.files().is_empty() { diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index ee950aec..cc254f24 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -35,11 +35,12 @@ use proxmox_backup::backup::{ BackupGroup, BackupManifest, BufferedDynamicReader, + CATALOG_NAME, CatalogReader, CatalogWriter, - CATALOG_NAME, ChunkStream, CryptConfig, + CryptMode, DataBlob, DynamicIndexReader, FixedChunkStream, @@ -664,34 +665,41 @@ fn spawn_catalog_upload( Ok((catalog, catalog_result_rx)) } -fn keyfile_parameters(param: &Value) -> Result, Error> { - Ok(match (param.get("keyfile"), param.get("encryption")) { +fn keyfile_parameters(param: &Value) -> Result<(Option, CryptMode), Error> { + let keyfile = match param.get("keyfile") { + Some(Value::String(keyfile)) => Some(keyfile), + Some(_) => bail!("bad --keyfile parameter type"), + None => None, + }; + + let crypt_mode: Option = match param.get("crypt-mode") { + Some(mode) => Some(serde_json::from_value(mode.clone())?), + None => None, + }; + + Ok(match (keyfile, crypt_mode) { // no parameters: - (None, None) => key::optional_default_key_path()?, + (None, None) => (key::optional_default_key_path()?, CryptMode::Encrypt), - // just --encryption=false - (None, Some(Value::Bool(false))) => None, + // just --crypt-mode=none + (None, Some(CryptMode::None)) => (None, CryptMode::None), - // just --encryption=true - (None, Some(Value::Bool(true))) => match key::optional_default_key_path()? { - None => bail!("--encryption=false without --keyfile and no default key file available"), - Some(path) => Some(path), + // just --crypt-mode other than none + (None, Some(crypt_mode)) => match key::optional_default_key_path()? { + None => bail!("--crypt-mode without --keyfile and no default key file available"), + Some(path) => (Some(path), crypt_mode), } // just --keyfile - (Some(Value::String(keyfile)), None) => Some(PathBuf::from(keyfile)), + (Some(keyfile), None) => (Some(PathBuf::from(keyfile)), CryptMode::Encrypt), - // --keyfile and --encryption=false - (Some(Value::String(_)), Some(Value::Bool(false))) => { - bail!("--keyfile and --encryption=false are mutually exclusive"); + // --keyfile and --crypt-mode=none + (Some(_), Some(CryptMode::None)) => { + bail!("--keyfile and --crypt-mode=none are mutually exclusive"); } - // --keyfile and --encryption=true - (Some(Value::String(keyfile)), Some(Value::Bool(true))) => Some(PathBuf::from(keyfile)), - - // wrong value types: - (Some(_), _) => bail!("bad --keyfile parameter"), - (_, Some(_)) => bail!("bad --encryption parameter"), + // --keyfile and --crypt-mode other than none + (Some(keyfile), Some(crypt_mode)) => (Some(PathBuf::from(keyfile)), crypt_mode), }) } @@ -794,7 +802,7 @@ async fn create_backup( verify_chunk_size(size)?; } - let keyfile = keyfile_parameters(¶m)?; + let (keyfile, crypt_mode) = keyfile_parameters(¶m)?; let backup_id = param["backup-id"].as_str().unwrap_or(&proxmox::tools::nodename()); @@ -912,8 +920,6 @@ async fn create_backup( } }; - let is_encrypted = Some(crypt_config.is_some()); - let client = BackupWriter::start( client, crypt_config.clone(), @@ -941,16 +947,16 @@ async fn create_backup( BackupSpecificationType::CONFIG => { println!("Upload config file '{}' to '{:?}' as {}", filename, repo, target); let stats = client - .upload_blob_from_file(&filename, &target, true, Some(true)) + .upload_blob_from_file(&filename, &target, true, crypt_mode) .await?; - manifest.add_file(target, stats.size, stats.csum, is_encrypted)?; + manifest.add_file(target, stats.size, stats.csum, crypt_mode)?; } BackupSpecificationType::LOGFILE => { // fixme: remove - not needed anymore ? println!("Upload log file '{}' to '{:?}' as {}", filename, repo, target); let stats = client - .upload_blob_from_file(&filename, &target, true, Some(true)) + .upload_blob_from_file(&filename, &target, true, crypt_mode) .await?; - manifest.add_file(target, stats.size, stats.csum, is_encrypted)?; + manifest.add_file(target, stats.size, stats.csum, crypt_mode)?; } BackupSpecificationType::PXAR => { // start catalog upload on first use @@ -976,7 +982,7 @@ async fn create_backup( pattern_list.clone(), entries_max as usize, ).await?; - manifest.add_file(target, stats.size, stats.csum, is_encrypted)?; + manifest.add_file(target, stats.size, stats.csum, crypt_mode)?; catalog.lock().unwrap().end_directory()?; } BackupSpecificationType::IMAGE => { @@ -990,7 +996,7 @@ async fn create_backup( chunk_size_opt, verbose, ).await?; - manifest.add_file(target, stats.size, stats.csum, is_encrypted)?; + manifest.add_file(target, stats.size, stats.csum, crypt_mode)?; } } } @@ -1007,7 +1013,7 @@ async fn create_backup( if let Some(catalog_result_rx) = catalog_result_tx { let stats = catalog_result_rx.await??; - manifest.add_file(CATALOG_NAME.to_owned(), stats.size, stats.csum, is_encrypted)?; + manifest.add_file(CATALOG_NAME.to_owned(), stats.size, stats.csum, crypt_mode)?; } } @@ -1015,9 +1021,9 @@ async fn create_backup( let target = "rsa-encrypted.key"; println!("Upload RSA encoded key to '{:?}' as {}", repo, target); let stats = client - .upload_blob_from_data(rsa_encrypted_key, target, false, None) + .upload_blob_from_data(rsa_encrypted_key, target, false, CryptMode::None) .await?; - manifest.add_file(format!("{}.blob", target), stats.size, stats.csum, is_encrypted)?; + manifest.add_file(format!("{}.blob", target), stats.size, stats.csum, crypt_mode)?; // openssl rsautl -decrypt -inkey master-private.pem -in rsa-encrypted.key -out t /* @@ -1035,7 +1041,7 @@ async fn create_backup( println!("Upload index.json to '{:?}'", repo); let manifest = serde_json::to_string_pretty(&manifest)?.into(); client - .upload_blob_from_data(manifest, MANIFEST_BLOB_NAME, true, Some(true)) + .upload_blob_from_data(manifest, MANIFEST_BLOB_NAME, true, crypt_mode.sign_only()) .await?; client.finish().await?; @@ -1193,7 +1199,7 @@ async fn restore(param: Value) -> Result { let target = tools::required_string_param(¶m, "target")?; let target = if target == "-" { None } else { Some(target) }; - let keyfile = keyfile_parameters(¶m)?; + let (keyfile, _crypt_mode) = keyfile_parameters(¶m)?; let crypt_config = match keyfile { None => None, @@ -1341,7 +1347,7 @@ async fn upload_log(param: Value) -> Result { let mut client = connect(repo.host(), repo.user())?; - let keyfile = keyfile_parameters(¶m)?; + let (keyfile, crypt_mode) = keyfile_parameters(¶m)?; let crypt_config = match keyfile { None => None, @@ -1354,7 +1360,19 @@ async fn upload_log(param: Value) -> Result { let data = file_get_contents(logfile)?; - let blob = DataBlob::encode(&data, crypt_config.as_ref().map(Arc::as_ref), true)?; + let blob = match crypt_mode { + CryptMode::None => DataBlob::encode(&data, None, true)?, + CryptMode::Encrypt => { + DataBlob::encode(&data, crypt_config.as_ref().map(Arc::as_ref), true)? + } + CryptMode::SignOnly => DataBlob::create_signed( + &data, + crypt_config + .ok_or_else(|| format_err!("cannot sign without crypt config"))? + .as_ref(), + true, + )?, + }; let raw_data = blob.into_inner(); diff --git a/src/client/backup_writer.rs b/src/client/backup_writer.rs index f2675ea7..74157096 100644 --- a/src/client/backup_writer.rs +++ b/src/client/backup_writer.rs @@ -3,7 +3,7 @@ use std::os::unix::fs::OpenOptionsExt; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; -use anyhow::{format_err, Error}; +use anyhow::{bail, format_err, Error}; use chrono::{DateTime, Utc}; use futures::*; use futures::stream::Stream; @@ -163,21 +163,17 @@ impl BackupWriter { data: Vec, file_name: &str, compress: bool, - crypt_or_sign: Option, - ) -> Result { - - let blob = if let Some(ref crypt_config) = self.crypt_config { - if let Some(encrypt) = crypt_or_sign { - if encrypt { - DataBlob::encode(&data, Some(crypt_config), compress)? - } else { - DataBlob::create_signed(&data, crypt_config, compress)? - } - } else { - DataBlob::encode(&data, None, compress)? - } - } else { - DataBlob::encode(&data, None, compress)? + crypt_mode: CryptMode, + ) -> Result { + let blob = match (crypt_mode, &self.crypt_config) { + (CryptMode::None, _) => DataBlob::encode(&data, None, compress)?, + (_, None) => bail!("requested encryption/signing without a crypt config"), + (CryptMode::Encrypt, Some(crypt_config)) => { + DataBlob::encode(&data, Some(crypt_config), compress)? + } + (CryptMode::SignOnly, Some(crypt_config)) => { + DataBlob::create_signed(&data, crypt_config, compress)? + } }; let raw_data = blob.into_inner(); @@ -194,7 +190,7 @@ impl BackupWriter { src_path: P, file_name: &str, compress: bool, - crypt_or_sign: Option, + crypt_mode: CryptMode, ) -> Result { let src_path = src_path.as_ref(); @@ -209,7 +205,7 @@ impl BackupWriter { .await .map_err(|err| format_err!("unable to read file {:?} - {}", src_path, err))?; - self.upload_blob_from_data(contents, file_name, compress, crypt_or_sign).await + self.upload_blob_from_data(contents, file_name, compress, crypt_mode).await } pub async fn upload_stream(