introduce a CryptMode enum
This also replaces the recently introduced --encryption
parameter on the client with a --crypt-mode parameter.
This can be "none", "encrypt" or "sign-only".
Note that this introduces various changes in the API types
which previously did not take the above distinction into
account properly:
Both `BackupContent` and the manifest's `FileInfo`:
    lose `encryption: Option<bool>`
    gain `crypt_mode: Option<CryptMode>`
Within the backup manifest itself, the "crypt-mode" property
will always be set.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
			
			
This commit is contained in:
		| @ -52,14 +52,14 @@ fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result<Vec<Ba | ||||
|     for item in manifest.files() { | ||||
|         result.push(BackupContent { | ||||
|             filename: item.filename.clone(), | ||||
|             encrypted: item.encrypted, | ||||
|             crypt_mode: Some(item.crypt_mode), | ||||
|             size: Some(item.size), | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     result.push(BackupContent { | ||||
|         filename: MANIFEST_BLOB_NAME.to_string(), | ||||
|         encrypted: Some(false), | ||||
|         crypt_mode: None, | ||||
|         size: Some(index_size), | ||||
|     }); | ||||
|  | ||||
| @ -79,7 +79,11 @@ fn get_all_snapshot_files( | ||||
|  | ||||
|     for file in &info.files { | ||||
|         if file_set.contains(file) { continue; } | ||||
|         files.push(BackupContent { filename: file.to_string(), size: None, encrypted: None }); | ||||
|         files.push(BackupContent { | ||||
|             filename: file.to_string(), | ||||
|             size: None, | ||||
|             crypt_mode: None, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     Ok(files) | ||||
| @ -350,7 +354,15 @@ pub fn list_snapshots ( | ||||
|             }, | ||||
|             Err(err) => { | ||||
|                 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); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -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<bool>, | ||||
|     pub crypt_mode: Option<CryptMode>, | ||||
|     /// Archive size (from backup manifest). | ||||
|     #[serde(skip_serializing_if="Option::is_none")] | ||||
|     pub size: Option<u64>, | ||||
|  | ||||
| @ -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<openssl::pkey::Private>, | ||||
|     // The private key used by the cipher. | ||||
|     enc_key: [u8; 32], | ||||
|  | ||||
| } | ||||
|  | ||||
| impl CryptConfig { | ||||
|  | ||||
| @ -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<bool>, | ||||
|     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<bool>) -> 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<Value> 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() { | ||||
|  | ||||
| @ -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<Option<PathBuf>, Error> { | ||||
|     Ok(match (param.get("keyfile"), param.get("encryption")) { | ||||
| fn keyfile_parameters(param: &Value) -> Result<(Option<PathBuf>, 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<CryptMode> = 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<Value, Error> { | ||||
|     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<Value, Error> { | ||||
|  | ||||
|     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<Value, Error> { | ||||
|  | ||||
|     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(); | ||||
|  | ||||
|  | ||||
| @ -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<u8>, | ||||
|         file_name: &str, | ||||
|         compress: bool, | ||||
|         crypt_or_sign: Option<bool>, | ||||
|      ) -> Result<BackupStats, Error> { | ||||
|  | ||||
|         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<BackupStats, Error> { | ||||
|         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<bool>, | ||||
|         crypt_mode: CryptMode, | ||||
|      ) -> Result<BackupStats, Error> { | ||||
|  | ||||
|         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( | ||||
|  | ||||
		Reference in New Issue
	
	Block a user