all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Reiter <s.reiter@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup 10/22] proxmox_client_tools: extract 'key' from client module
Date: Tue, 16 Feb 2021 18:06:58 +0100	[thread overview]
Message-ID: <20210216170710.31767-11-s.reiter@proxmox.com> (raw)
In-Reply-To: <20210216170710.31767-1-s.reiter@proxmox.com>

To be used by other command line tools. Requires moving XDG helpers as
well, which find their place in the tools module quite cozily IMHO.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
---
 src/bin/proxmox-backup-client.rs              | 440 +-----------------
 src/bin/proxmox_backup_client/catalog.rs      |   4 +-
 src/bin/proxmox_backup_client/mod.rs          |  30 --
 src/bin/proxmox_backup_client/snapshot.rs     |   3 +-
 .../key.rs                                    | 440 +++++++++++++++++-
 src/bin/proxmox_client_tools/mod.rs           |  30 +-
 6 files changed, 474 insertions(+), 473 deletions(-)
 rename src/bin/{proxmox_backup_client => proxmox_client_tools}/key.rs (52%)

diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs
index 1b8b5bec..794f783c 100644
--- a/src/bin/proxmox-backup-client.rs
+++ b/src/bin/proxmox-backup-client.rs
@@ -1,7 +1,5 @@
 use std::collections::HashSet;
-use std::convert::TryFrom;
 use std::io::{self, Read, Write, Seek, SeekFrom};
-use std::os::unix::io::{FromRawFd, RawFd};
 use std::path::{Path, PathBuf};
 use std::pin::Pin;
 use std::sync::{Arc, Mutex};
@@ -19,7 +17,7 @@ use pathpatterns::{MatchEntry, MatchType, PatternFlag};
 use proxmox::{
     tools::{
         time::{strftime_local, epoch_i64},
-        fs::{file_get_contents, file_get_json, replace_file, CreateOptions, image_size},
+        fs::{file_get_json, replace_file, CreateOptions, image_size},
     },
     api::{
         api,
@@ -68,7 +66,10 @@ mod proxmox_backup_client;
 use proxmox_backup_client::*;
 
 mod proxmox_client_tools;
-use proxmox_client_tools::*;
+use proxmox_client_tools::{
+    *,
+    key::{format_key_source, crypto_parameters},
+};
 
 fn record_repository(repo: &BackupRepository) {
 
@@ -499,437 +500,6 @@ fn spawn_catalog_upload(
     Ok(CatalogUploadResult { catalog_writer, result: catalog_result_rx })
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
-enum KeySource {
-    DefaultKey,
-    Fd,
-    Path(String),
-}
-
-fn format_key_source(source: &KeySource, key_type: &str) -> String {
-    match source {
-        KeySource::DefaultKey => format!("Using default {} key..", key_type),
-        KeySource::Fd => format!("Using {} key from file descriptor..", key_type),
-        KeySource::Path(path) => format!("Using {} key from '{}'..", key_type, path),
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct KeyWithSource {
-    pub source: KeySource,
-    pub key: Vec<u8>,
-}
-
-impl KeyWithSource {
-    pub fn from_fd(key: Vec<u8>) -> Self {
-        Self {
-            source: KeySource::Fd,
-            key,
-        }
-    }
-
-    pub fn from_default(key: Vec<u8>) -> Self {
-        Self {
-            source: KeySource::DefaultKey,
-            key,
-        }
-    }
-
-    pub fn from_path(path: String, key: Vec<u8>) -> Self {
-        Self {
-            source: KeySource::Path(path),
-            key,
-        }
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-struct CryptoParams {
-    mode: CryptMode,
-    enc_key: Option<KeyWithSource>,
-    // FIXME switch to openssl::rsa::rsa<openssl::pkey::Public> once that is Eq?
-    master_pubkey: Option<KeyWithSource>,
-}
-
-fn crypto_parameters(param: &Value) -> Result<CryptoParams, Error> {
-    let keyfile = match param.get("keyfile") {
-        Some(Value::String(keyfile)) => Some(keyfile),
-        Some(_) => bail!("bad --keyfile parameter type"),
-        None => None,
-    };
-
-    let key_fd = match param.get("keyfd") {
-        Some(Value::Number(key_fd)) => Some(
-            RawFd::try_from(key_fd
-                .as_i64()
-                .ok_or_else(|| format_err!("bad key fd: {:?}", key_fd))?
-            )
-            .map_err(|err| format_err!("bad key fd: {:?}: {}", key_fd, err))?
-        ),
-        Some(_) => bail!("bad --keyfd parameter type"),
-        None => None,
-    };
-
-    let master_pubkey_file = match param.get("master-pubkey-file") {
-        Some(Value::String(keyfile)) => Some(keyfile),
-        Some(_) => bail!("bad --master-pubkey-file parameter type"),
-        None => None,
-    };
-
-    let master_pubkey_fd = match param.get("master-pubkey-fd") {
-        Some(Value::Number(key_fd)) => Some(
-            RawFd::try_from(key_fd
-                .as_i64()
-                .ok_or_else(|| format_err!("bad master public key fd: {:?}", key_fd))?
-            )
-            .map_err(|err| format_err!("bad public master key fd: {:?}: {}", key_fd, err))?
-        ),
-        Some(_) => bail!("bad --master-pubkey-fd parameter type"),
-        None => None,
-    };
-
-    let mode: Option<CryptMode> = match param.get("crypt-mode") {
-        Some(mode) => Some(serde_json::from_value(mode.clone())?),
-        None => None,
-    };
-
-    let key = match (keyfile, key_fd) {
-        (None, None) => None,
-        (Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
-        (Some(keyfile), None) => Some(KeyWithSource::from_path(
-            keyfile.clone(),
-            file_get_contents(keyfile)?,
-        )),
-        (None, Some(fd)) => {
-            let input = unsafe { std::fs::File::from_raw_fd(fd) };
-            let mut data = Vec::new();
-            let _len: usize = { input }.read_to_end(&mut data).map_err(|err| {
-                format_err!("error reading encryption key from fd {}: {}", fd, err)
-            })?;
-            Some(KeyWithSource::from_fd(data))
-        }
-    };
-
-    let master_pubkey = match (master_pubkey_file, master_pubkey_fd) {
-        (None, None) => None,
-        (Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
-        (Some(keyfile), None) => Some(KeyWithSource::from_path(
-            keyfile.clone(),
-            file_get_contents(keyfile)?,
-        )),
-        (None, Some(fd)) => {
-            let input = unsafe { std::fs::File::from_raw_fd(fd) };
-            let mut data = Vec::new();
-            let _len: usize = { input }
-                .read_to_end(&mut data)
-                .map_err(|err| format_err!("error reading master key from fd {}: {}", fd, err))?;
-            Some(KeyWithSource::from_fd(data))
-        }
-    };
-
-    let res = match mode {
-        // no crypt mode, enable encryption if keys are available
-        None => match (key, master_pubkey) {
-            // only default keys if available
-            (None, None) => match key::read_optional_default_encryption_key()? {
-                None => CryptoParams { mode: CryptMode::None, enc_key: None, master_pubkey: None },
-                enc_key => {
-                    let master_pubkey = key::read_optional_default_master_pubkey()?;
-                    CryptoParams {
-                        mode: CryptMode::Encrypt,
-                        enc_key,
-                        master_pubkey,
-                    }
-                },
-            },
-
-            // explicit master key, default enc key needed
-            (None, master_pubkey) => match key::read_optional_default_encryption_key()? {
-                None => bail!("--master-pubkey-file/--master-pubkey-fd specified, but no key available"),
-                enc_key => {
-                    CryptoParams {
-                        mode: CryptMode::Encrypt,
-                        enc_key,
-                        master_pubkey,
-                    }
-                },
-            },
-
-            // explicit keyfile, maybe default master key
-            (enc_key, None) => CryptoParams { mode: CryptMode::Encrypt, enc_key, master_pubkey: key::read_optional_default_master_pubkey()? },
-
-            // explicit keyfile and master key
-            (enc_key, master_pubkey) => CryptoParams { mode: CryptMode::Encrypt, enc_key, master_pubkey },
-        },
-
-        // explicitly disabled encryption
-        Some(CryptMode::None) => match (key, master_pubkey) {
-            // no keys => OK, no encryption
-            (None, None) => CryptoParams { mode: CryptMode::None, enc_key: None, master_pubkey: None },
-
-            // --keyfile and --crypt-mode=none
-            (Some(_), _) => bail!("--keyfile/--keyfd and --crypt-mode=none are mutually exclusive"),
-
-            // --master-pubkey-file and --crypt-mode=none
-            (_, Some(_)) => bail!("--master-pubkey-file/--master-pubkey-fd and --crypt-mode=none are mutually exclusive"),
-        },
-
-        // explicitly enabled encryption
-        Some(mode) => match (key, master_pubkey) {
-            // no key, maybe master key
-            (None, master_pubkey) => match key::read_optional_default_encryption_key()? {
-                None => bail!("--crypt-mode without --keyfile and no default key file available"),
-                enc_key => {
-                    eprintln!("Encrypting with default encryption key!");
-                    let master_pubkey = match master_pubkey {
-                        None => key::read_optional_default_master_pubkey()?,
-                        master_pubkey => master_pubkey,
-                    };
-
-                    CryptoParams {
-                        mode,
-                        enc_key,
-                        master_pubkey,
-                    }
-                },
-            },
-
-            // --keyfile and --crypt-mode other than none
-            (enc_key, master_pubkey) => {
-                let master_pubkey = match master_pubkey {
-                    None => key::read_optional_default_master_pubkey()?,
-                    master_pubkey => master_pubkey,
-                };
-
-                CryptoParams { mode, enc_key, master_pubkey }
-            },
-        },
-    };
-
-    Ok(res)
-}
-
-#[test]
-// WARNING: there must only be one test for crypto_parameters as the default key handling is not
-// safe w.r.t. concurrency
-fn test_crypto_parameters_handling() -> Result<(), Error> {
-    let some_key = vec![1;1];
-    let default_key = vec![2;1];
-
-    let some_master_key = vec![3;1];
-    let default_master_key = vec![4;1];
-
-    let keypath = "./target/testout/keyfile.test";
-    let master_keypath = "./target/testout/masterkeyfile.test";
-    let invalid_keypath = "./target/testout/invalid_keyfile.test";
-
-    let no_key_res = CryptoParams {
-        enc_key: None,
-        master_pubkey: None,
-        mode: CryptMode::None,
-    };
-    let some_key_res = CryptoParams {
-        enc_key: Some(KeyWithSource::from_path(
-            keypath.to_string(),
-            some_key.clone(),
-        )),
-        master_pubkey: None,
-        mode: CryptMode::Encrypt,
-    };
-    let some_key_some_master_res = CryptoParams {
-        enc_key: Some(KeyWithSource::from_path(
-            keypath.to_string(),
-            some_key.clone(),
-        )),
-        master_pubkey: Some(KeyWithSource::from_path(
-            master_keypath.to_string(),
-            some_master_key.clone(),
-        )),
-        mode: CryptMode::Encrypt,
-    };
-    let some_key_default_master_res = CryptoParams {
-        enc_key: Some(KeyWithSource::from_path(
-            keypath.to_string(),
-            some_key.clone(),
-        )),
-        master_pubkey: Some(KeyWithSource::from_default(default_master_key.clone())),
-        mode: CryptMode::Encrypt,
-    };
-
-    let some_key_sign_res = CryptoParams {
-        enc_key: Some(KeyWithSource::from_path(
-            keypath.to_string(),
-            some_key.clone(),
-        )),
-        master_pubkey: None,
-        mode: CryptMode::SignOnly,
-    };
-    let default_key_res = CryptoParams {
-        enc_key: Some(KeyWithSource::from_default(default_key.clone())),
-        master_pubkey: None,
-        mode: CryptMode::Encrypt,
-    };
-    let default_key_sign_res = CryptoParams {
-        enc_key: Some(KeyWithSource::from_default(default_key.clone())),
-        master_pubkey: None,
-        mode: CryptMode::SignOnly,
-    };
-
-    replace_file(&keypath, &some_key, CreateOptions::default())?;
-    replace_file(&master_keypath, &some_master_key, CreateOptions::default())?;
-
-    // no params, no default key == no key
-    let res = crypto_parameters(&json!({}));
-    assert_eq!(res.unwrap(), no_key_res);
-
-    // keyfile param == key from keyfile
-    let res = crypto_parameters(&json!({"keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_res);
-
-    // crypt mode none == no key
-    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
-    assert_eq!(res.unwrap(), no_key_res);
-
-    // crypt mode encrypt/sign-only, no keyfile, no default key == Error
-    assert!(crypto_parameters(&json!({"crypt-mode": "sign-only"})).is_err());
-    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
-
-    // crypt mode none with explicit key == Error
-    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
-    // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
-    let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_sign_res);
-    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_res);
-
-    // invalid keyfile parameter always errors
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
-
-    // now set a default key
-    unsafe { key::set_test_encryption_key(Ok(Some(default_key.clone()))); }
-
-    // and repeat
-
-    // no params but default key == default key
-    let res = crypto_parameters(&json!({}));
-    assert_eq!(res.unwrap(), default_key_res);
-
-    // keyfile param == key from keyfile
-    let res = crypto_parameters(&json!({"keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_res);
-
-    // crypt mode none == no key
-    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
-    assert_eq!(res.unwrap(), no_key_res);
-
-    // crypt mode encrypt/sign-only, no keyfile, default key == default key with correct mode
-    let res = crypto_parameters(&json!({"crypt-mode": "sign-only"}));
-    assert_eq!(res.unwrap(), default_key_sign_res);
-    let res = crypto_parameters(&json!({"crypt-mode": "encrypt"}));
-    assert_eq!(res.unwrap(), default_key_res);
-
-    // crypt mode none with explicit key == Error
-    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
-    // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
-    let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_sign_res);
-    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_res);
-
-    // invalid keyfile parameter always errors
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
-
-    // now make default key retrieval error
-    unsafe { key::set_test_encryption_key(Err(format_err!("test error"))); }
-
-    // and repeat
-
-    // no params, default key retrieval errors == Error
-    assert!(crypto_parameters(&json!({})).is_err());
-
-    // keyfile param == key from keyfile
-    let res = crypto_parameters(&json!({"keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_res);
-
-    // crypt mode none == no key
-    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
-    assert_eq!(res.unwrap(), no_key_res);
-
-    // crypt mode encrypt/sign-only, no keyfile, default key error == Error
-    assert!(crypto_parameters(&json!({"crypt-mode": "sign-only"})).is_err());
-    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
-
-    // crypt mode none with explicit key == Error
-    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
-    // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
-    let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_sign_res);
-    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_res);
-
-    // invalid keyfile parameter always errors
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
-
-    // now remove default key again
-    unsafe { key::set_test_encryption_key(Ok(None)); }
-    // set a default master key
-    unsafe { key::set_test_default_master_pubkey(Ok(Some(default_master_key.clone()))); }
-
-    // and use an explicit master key
-    assert!(crypto_parameters(&json!({"master-pubkey-file": master_keypath})).is_err());
-    // just a default == no key
-    let res = crypto_parameters(&json!({}));
-    assert_eq!(res.unwrap(), no_key_res);
-
-    // keyfile param == key from keyfile
-    let res = crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": master_keypath}));
-    assert_eq!(res.unwrap(), some_key_some_master_res);
-    // same with fallback to default master key
-    let res = crypto_parameters(&json!({"keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_default_master_res);
-
-    // crypt mode none == error
-    assert!(crypto_parameters(&json!({"crypt-mode": "none", "master-pubkey-file": master_keypath})).is_err());
-    // with just default master key == no key
-    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
-    assert_eq!(res.unwrap(), no_key_res);
-
-    // crypt mode encrypt without enc key == error
-    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt", "master-pubkey-file": master_keypath})).is_err());
-    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
-
-    // crypt mode none with explicit key == Error
-    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath, "master-pubkey-file": master_keypath})).is_err());
-    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
-
-    // crypt mode encrypt with keyfile == key from keyfile with correct mode
-    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath, "master-pubkey-file": master_keypath}));
-    assert_eq!(res.unwrap(), some_key_some_master_res);
-    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
-    assert_eq!(res.unwrap(), some_key_default_master_res);
-
-    // invalid master keyfile parameter always errors when a key is passed, even with a valid
-    // default master key
-    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "none"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "sign-only"})).is_err());
-    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "encrypt"})).is_err());
-
-    Ok(())
-}
-
 #[api(
    input: {
        properties: {
diff --git a/src/bin/proxmox_backup_client/catalog.rs b/src/bin/proxmox_backup_client/catalog.rs
index 659200ff..80d72a55 100644
--- a/src/bin/proxmox_backup_client/catalog.rs
+++ b/src/bin/proxmox_backup_client/catalog.rs
@@ -15,7 +15,6 @@ use crate::{
     REPO_URL_SCHEMA,
     KEYFD_SCHEMA,
     extract_repository_from_value,
-    format_key_source,
     record_repository,
     key::get_encryption_key_password,
     decrypt_key,
@@ -25,7 +24,6 @@ use crate::{
     complete_group_or_snapshot,
     complete_pxar_archive_name,
     connect,
-    crypto_parameters,
     BackupDir,
     BackupGroup,
     BufferedDynamicReader,
@@ -38,6 +36,8 @@ use crate::{
     Shell,
 };
 
+use crate::proxmox_client_tools::key::{format_key_source, crypto_parameters};
+
 #[api(
    input: {
         properties: {
diff --git a/src/bin/proxmox_backup_client/mod.rs b/src/bin/proxmox_backup_client/mod.rs
index a14b0dc1..bc03f243 100644
--- a/src/bin/proxmox_backup_client/mod.rs
+++ b/src/bin/proxmox_backup_client/mod.rs
@@ -1,5 +1,3 @@
-use anyhow::{Context, Error};
-
 mod benchmark;
 pub use benchmark::*;
 mod mount;
@@ -11,31 +9,3 @@ pub use catalog::*;
 mod snapshot;
 pub use snapshot::*;
 
-pub mod key;
-
-pub fn base_directories() -> Result<xdg::BaseDirectories, Error> {
-    xdg::BaseDirectories::with_prefix("proxmox-backup").map_err(Error::from)
-}
-
-/// Convenience helper for better error messages:
-pub fn find_xdg_file(
-    file_name: impl AsRef<std::path::Path>,
-    description: &'static str,
-) -> Result<Option<std::path::PathBuf>, Error> {
-    let file_name = file_name.as_ref();
-    base_directories()
-        .map(|base| base.find_config_file(file_name))
-        .with_context(|| format!("error searching for {}", description))
-}
-
-pub fn place_xdg_file(
-    file_name: impl AsRef<std::path::Path>,
-    description: &'static str,
-) -> Result<std::path::PathBuf, Error> {
-    let file_name = file_name.as_ref();
-    base_directories()
-        .and_then(|base| {
-            base.place_config_file(file_name).map_err(Error::from)
-        })
-        .with_context(|| format!("failed to place {} in xdg home", description))
-}
diff --git a/src/bin/proxmox_backup_client/snapshot.rs b/src/bin/proxmox_backup_client/snapshot.rs
index 5988ebf6..45ae63b3 100644
--- a/src/bin/proxmox_backup_client/snapshot.rs
+++ b/src/bin/proxmox_backup_client/snapshot.rs
@@ -30,11 +30,12 @@ use crate::{
     complete_backup_group,
     complete_repository,
     connect,
-    crypto_parameters,
     extract_repository_from_value,
     record_repository,
 };
 
+use crate::proxmox_client_tools::key::crypto_parameters;
+
 #[api(
    input: {
         properties: {
diff --git a/src/bin/proxmox_backup_client/key.rs b/src/bin/proxmox_client_tools/key.rs
similarity index 52%
rename from src/bin/proxmox_backup_client/key.rs
rename to src/bin/proxmox_client_tools/key.rs
index 6e18a026..11cf01e6 100644
--- a/src/bin/proxmox_backup_client/key.rs
+++ b/src/bin/proxmox_client_tools/key.rs
@@ -1,5 +1,7 @@
 use std::convert::TryFrom;
 use std::path::PathBuf;
+use std::os::unix::io::{FromRawFd, RawFd};
+use std::io::Read;
 
 use anyhow::{bail, format_err, Error};
 use serde_json::Value;
@@ -15,16 +17,224 @@ use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
 
 use proxmox_backup::{
     api2::types::{Kdf, KeyInfo, RsaPubKeyInfo, PASSWORD_HINT_SCHEMA},
-    backup::{rsa_decrypt_key_config, KeyConfig},
+    backup::{rsa_decrypt_key_config, CryptMode, KeyConfig},
     tools,
     tools::paperkey::{generate_paper_key, PaperkeyFormat},
 };
 
-use crate::KeyWithSource;
-
 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
 pub const DEFAULT_MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
 
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum KeySource {
+    DefaultKey,
+    Fd,
+    Path(String),
+}
+
+pub fn format_key_source(source: &KeySource, key_type: &str) -> String {
+    match source {
+        KeySource::DefaultKey => format!("Using default {} key..", key_type),
+        KeySource::Fd => format!("Using {} key from file descriptor..", key_type),
+        KeySource::Path(path) => format!("Using {} key from '{}'..", key_type, path),
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct KeyWithSource {
+    pub source: KeySource,
+    pub key: Vec<u8>,
+}
+
+impl KeyWithSource {
+    pub fn from_fd(key: Vec<u8>) -> Self {
+        Self {
+            source: KeySource::Fd,
+            key,
+        }
+    }
+
+    pub fn from_default(key: Vec<u8>) -> Self {
+        Self {
+            source: KeySource::DefaultKey,
+            key,
+        }
+    }
+
+    pub fn from_path(path: String, key: Vec<u8>) -> Self {
+        Self {
+            source: KeySource::Path(path),
+            key,
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct CryptoParams {
+    pub mode: CryptMode,
+    pub enc_key: Option<KeyWithSource>,
+    // FIXME switch to openssl::rsa::rsa<openssl::pkey::Public> once that is Eq?
+    pub master_pubkey: Option<KeyWithSource>,
+}
+
+pub fn crypto_parameters(param: &Value) -> Result<CryptoParams, Error> {
+    let keyfile = match param.get("keyfile") {
+        Some(Value::String(keyfile)) => Some(keyfile),
+        Some(_) => bail!("bad --keyfile parameter type"),
+        None => None,
+    };
+
+    let key_fd = match param.get("keyfd") {
+        Some(Value::Number(key_fd)) => Some(
+            RawFd::try_from(key_fd
+                .as_i64()
+                .ok_or_else(|| format_err!("bad key fd: {:?}", key_fd))?
+            )
+            .map_err(|err| format_err!("bad key fd: {:?}: {}", key_fd, err))?
+        ),
+        Some(_) => bail!("bad --keyfd parameter type"),
+        None => None,
+    };
+
+    let master_pubkey_file = match param.get("master-pubkey-file") {
+        Some(Value::String(keyfile)) => Some(keyfile),
+        Some(_) => bail!("bad --master-pubkey-file parameter type"),
+        None => None,
+    };
+
+    let master_pubkey_fd = match param.get("master-pubkey-fd") {
+        Some(Value::Number(key_fd)) => Some(
+            RawFd::try_from(key_fd
+                .as_i64()
+                .ok_or_else(|| format_err!("bad master public key fd: {:?}", key_fd))?
+            )
+            .map_err(|err| format_err!("bad public master key fd: {:?}: {}", key_fd, err))?
+        ),
+        Some(_) => bail!("bad --master-pubkey-fd parameter type"),
+        None => None,
+    };
+
+    let mode: Option<CryptMode> = match param.get("crypt-mode") {
+        Some(mode) => Some(serde_json::from_value(mode.clone())?),
+        None => None,
+    };
+
+    let key = match (keyfile, key_fd) {
+        (None, None) => None,
+        (Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
+        (Some(keyfile), None) => Some(KeyWithSource::from_path(
+            keyfile.clone(),
+            file_get_contents(keyfile)?,
+        )),
+        (None, Some(fd)) => {
+            let input = unsafe { std::fs::File::from_raw_fd(fd) };
+            let mut data = Vec::new();
+            let _len: usize = { input }.read_to_end(&mut data).map_err(|err| {
+                format_err!("error reading encryption key from fd {}: {}", fd, err)
+            })?;
+            Some(KeyWithSource::from_fd(data))
+        }
+    };
+
+    let master_pubkey = match (master_pubkey_file, master_pubkey_fd) {
+        (None, None) => None,
+        (Some(_), Some(_)) => bail!("--keyfile and --keyfd are mutually exclusive"),
+        (Some(keyfile), None) => Some(KeyWithSource::from_path(
+            keyfile.clone(),
+            file_get_contents(keyfile)?,
+        )),
+        (None, Some(fd)) => {
+            let input = unsafe { std::fs::File::from_raw_fd(fd) };
+            let mut data = Vec::new();
+            let _len: usize = { input }
+                .read_to_end(&mut data)
+                .map_err(|err| format_err!("error reading master key from fd {}: {}", fd, err))?;
+            Some(KeyWithSource::from_fd(data))
+        }
+    };
+
+    let res = match mode {
+        // no crypt mode, enable encryption if keys are available
+        None => match (key, master_pubkey) {
+            // only default keys if available
+            (None, None) => match read_optional_default_encryption_key()? {
+                None => CryptoParams { mode: CryptMode::None, enc_key: None, master_pubkey: None },
+                enc_key => {
+                    let master_pubkey = read_optional_default_master_pubkey()?;
+                    CryptoParams {
+                        mode: CryptMode::Encrypt,
+                        enc_key,
+                        master_pubkey,
+                    }
+                },
+            },
+
+            // explicit master key, default enc key needed
+            (None, master_pubkey) => match read_optional_default_encryption_key()? {
+                None => bail!("--master-pubkey-file/--master-pubkey-fd specified, but no key available"),
+                enc_key => {
+                    CryptoParams {
+                        mode: CryptMode::Encrypt,
+                        enc_key,
+                        master_pubkey,
+                    }
+                },
+            },
+
+            // explicit keyfile, maybe default master key
+            (enc_key, None) => CryptoParams { mode: CryptMode::Encrypt, enc_key, master_pubkey: read_optional_default_master_pubkey()? },
+
+            // explicit keyfile and master key
+            (enc_key, master_pubkey) => CryptoParams { mode: CryptMode::Encrypt, enc_key, master_pubkey },
+        },
+
+        // explicitly disabled encryption
+        Some(CryptMode::None) => match (key, master_pubkey) {
+            // no keys => OK, no encryption
+            (None, None) => CryptoParams { mode: CryptMode::None, enc_key: None, master_pubkey: None },
+
+            // --keyfile and --crypt-mode=none
+            (Some(_), _) => bail!("--keyfile/--keyfd and --crypt-mode=none are mutually exclusive"),
+
+            // --master-pubkey-file and --crypt-mode=none
+            (_, Some(_)) => bail!("--master-pubkey-file/--master-pubkey-fd and --crypt-mode=none are mutually exclusive"),
+        },
+
+        // explicitly enabled encryption
+        Some(mode) => match (key, master_pubkey) {
+            // no key, maybe master key
+            (None, master_pubkey) => match read_optional_default_encryption_key()? {
+                None => bail!("--crypt-mode without --keyfile and no default key file available"),
+                enc_key => {
+                    eprintln!("Encrypting with default encryption key!");
+                    let master_pubkey = match master_pubkey {
+                        None => read_optional_default_master_pubkey()?,
+                        master_pubkey => master_pubkey,
+                    };
+
+                    CryptoParams {
+                        mode,
+                        enc_key,
+                        master_pubkey,
+                    }
+                },
+            },
+
+            // --keyfile and --crypt-mode other than none
+            (enc_key, master_pubkey) => {
+                let master_pubkey = match master_pubkey {
+                    None => read_optional_default_master_pubkey()?,
+                    master_pubkey => master_pubkey,
+                };
+
+                CryptoParams { mode, enc_key, master_pubkey }
+            },
+        },
+    };
+
+    Ok(res)
+}
+
 pub fn find_default_master_pubkey() -> Result<Option<PathBuf>, Error> {
     super::find_xdg_file(
         DEFAULT_MASTER_PUBKEY_FILE_NAME,
@@ -600,3 +810,227 @@ pub fn cli() -> CliCommandMap {
         .insert("show-master-pubkey", key_show_master_pubkey_cmd_def)
         .insert("paperkey", paper_key_cmd_def)
 }
+
+#[test]
+// WARNING: there must only be one test for crypto_parameters as the default key handling is not
+// safe w.r.t. concurrency
+fn test_crypto_parameters_handling() -> Result<(), Error> {
+    use serde_json::json;
+
+    let some_key = vec![1;1];
+    let default_key = vec![2;1];
+
+    let some_master_key = vec![3;1];
+    let default_master_key = vec![4;1];
+
+    let keypath = "./target/testout/keyfile.test";
+    let master_keypath = "./target/testout/masterkeyfile.test";
+    let invalid_keypath = "./target/testout/invalid_keyfile.test";
+
+    let no_key_res = CryptoParams {
+        enc_key: None,
+        master_pubkey: None,
+        mode: CryptMode::None,
+    };
+    let some_key_res = CryptoParams {
+        enc_key: Some(KeyWithSource::from_path(
+            keypath.to_string(),
+            some_key.clone(),
+        )),
+        master_pubkey: None,
+        mode: CryptMode::Encrypt,
+    };
+    let some_key_some_master_res = CryptoParams {
+        enc_key: Some(KeyWithSource::from_path(
+            keypath.to_string(),
+            some_key.clone(),
+        )),
+        master_pubkey: Some(KeyWithSource::from_path(
+            master_keypath.to_string(),
+            some_master_key.clone(),
+        )),
+        mode: CryptMode::Encrypt,
+    };
+    let some_key_default_master_res = CryptoParams {
+        enc_key: Some(KeyWithSource::from_path(
+            keypath.to_string(),
+            some_key.clone(),
+        )),
+        master_pubkey: Some(KeyWithSource::from_default(default_master_key.clone())),
+        mode: CryptMode::Encrypt,
+    };
+
+    let some_key_sign_res = CryptoParams {
+        enc_key: Some(KeyWithSource::from_path(
+            keypath.to_string(),
+            some_key.clone(),
+        )),
+        master_pubkey: None,
+        mode: CryptMode::SignOnly,
+    };
+    let default_key_res = CryptoParams {
+        enc_key: Some(KeyWithSource::from_default(default_key.clone())),
+        master_pubkey: None,
+        mode: CryptMode::Encrypt,
+    };
+    let default_key_sign_res = CryptoParams {
+        enc_key: Some(KeyWithSource::from_default(default_key.clone())),
+        master_pubkey: None,
+        mode: CryptMode::SignOnly,
+    };
+
+    replace_file(&keypath, &some_key, CreateOptions::default())?;
+    replace_file(&master_keypath, &some_master_key, CreateOptions::default())?;
+
+    // no params, no default key == no key
+    let res = crypto_parameters(&json!({}));
+    assert_eq!(res.unwrap(), no_key_res);
+
+    // keyfile param == key from keyfile
+    let res = crypto_parameters(&json!({"keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_res);
+
+    // crypt mode none == no key
+    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
+    assert_eq!(res.unwrap(), no_key_res);
+
+    // crypt mode encrypt/sign-only, no keyfile, no default key == Error
+    assert!(crypto_parameters(&json!({"crypt-mode": "sign-only"})).is_err());
+    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
+
+    // crypt mode none with explicit key == Error
+    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
+
+    // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
+    let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_sign_res);
+    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_res);
+
+    // invalid keyfile parameter always errors
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
+
+    // now set a default key
+    unsafe { set_test_encryption_key(Ok(Some(default_key.clone()))); }
+
+    // and repeat
+
+    // no params but default key == default key
+    let res = crypto_parameters(&json!({}));
+    assert_eq!(res.unwrap(), default_key_res);
+
+    // keyfile param == key from keyfile
+    let res = crypto_parameters(&json!({"keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_res);
+
+    // crypt mode none == no key
+    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
+    assert_eq!(res.unwrap(), no_key_res);
+
+    // crypt mode encrypt/sign-only, no keyfile, default key == default key with correct mode
+    let res = crypto_parameters(&json!({"crypt-mode": "sign-only"}));
+    assert_eq!(res.unwrap(), default_key_sign_res);
+    let res = crypto_parameters(&json!({"crypt-mode": "encrypt"}));
+    assert_eq!(res.unwrap(), default_key_res);
+
+    // crypt mode none with explicit key == Error
+    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
+
+    // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
+    let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_sign_res);
+    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_res);
+
+    // invalid keyfile parameter always errors
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
+
+    // now make default key retrieval error
+    unsafe { set_test_encryption_key(Err(format_err!("test error"))); }
+
+    // and repeat
+
+    // no params, default key retrieval errors == Error
+    assert!(crypto_parameters(&json!({})).is_err());
+
+    // keyfile param == key from keyfile
+    let res = crypto_parameters(&json!({"keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_res);
+
+    // crypt mode none == no key
+    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
+    assert_eq!(res.unwrap(), no_key_res);
+
+    // crypt mode encrypt/sign-only, no keyfile, default key error == Error
+    assert!(crypto_parameters(&json!({"crypt-mode": "sign-only"})).is_err());
+    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
+
+    // crypt mode none with explicit key == Error
+    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
+
+    // crypt mode sign-only/encrypt with keyfile == key from keyfile with correct mode
+    let res = crypto_parameters(&json!({"crypt-mode": "sign-only", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_sign_res);
+    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_res);
+
+    // invalid keyfile parameter always errors
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "none"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "sign-only"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": invalid_keypath, "crypt-mode": "encrypt"})).is_err());
+
+    // now remove default key again
+    unsafe { set_test_encryption_key(Ok(None)); }
+    // set a default master key
+    unsafe { set_test_default_master_pubkey(Ok(Some(default_master_key.clone()))); }
+
+    // and use an explicit master key
+    assert!(crypto_parameters(&json!({"master-pubkey-file": master_keypath})).is_err());
+    // just a default == no key
+    let res = crypto_parameters(&json!({}));
+    assert_eq!(res.unwrap(), no_key_res);
+
+    // keyfile param == key from keyfile
+    let res = crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": master_keypath}));
+    assert_eq!(res.unwrap(), some_key_some_master_res);
+    // same with fallback to default master key
+    let res = crypto_parameters(&json!({"keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_default_master_res);
+
+    // crypt mode none == error
+    assert!(crypto_parameters(&json!({"crypt-mode": "none", "master-pubkey-file": master_keypath})).is_err());
+    // with just default master key == no key
+    let res = crypto_parameters(&json!({"crypt-mode": "none"}));
+    assert_eq!(res.unwrap(), no_key_res);
+
+    // crypt mode encrypt without enc key == error
+    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt", "master-pubkey-file": master_keypath})).is_err());
+    assert!(crypto_parameters(&json!({"crypt-mode": "encrypt"})).is_err());
+
+    // crypt mode none with explicit key == Error
+    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath, "master-pubkey-file": master_keypath})).is_err());
+    assert!(crypto_parameters(&json!({"crypt-mode": "none", "keyfile": keypath})).is_err());
+
+    // crypt mode encrypt with keyfile == key from keyfile with correct mode
+    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath, "master-pubkey-file": master_keypath}));
+    assert_eq!(res.unwrap(), some_key_some_master_res);
+    let res = crypto_parameters(&json!({"crypt-mode": "encrypt", "keyfile": keypath}));
+    assert_eq!(res.unwrap(), some_key_default_master_res);
+
+    // invalid master keyfile parameter always errors when a key is passed, even with a valid
+    // default master key
+    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "none"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "sign-only"})).is_err());
+    assert!(crypto_parameters(&json!({"keyfile": keypath, "master-pubkey-file": invalid_keypath,"crypt-mode": "encrypt"})).is_err());
+
+    Ok(())
+}
+
diff --git a/src/bin/proxmox_client_tools/mod.rs b/src/bin/proxmox_client_tools/mod.rs
index 7b69e8cb..40698f1d 100644
--- a/src/bin/proxmox_client_tools/mod.rs
+++ b/src/bin/proxmox_client_tools/mod.rs
@@ -1,8 +1,7 @@
 //! Shared tools useful for common CLI clients.
-
 use std::collections::HashMap;
 
-use anyhow::{bail, format_err, Error};
+use anyhow::{bail, format_err, Context, Error};
 use serde_json::{json, Value};
 use xdg::BaseDirectories;
 
@@ -17,6 +16,8 @@ use proxmox_backup::backup::BackupDir;
 use proxmox_backup::client::*;
 use proxmox_backup::tools;
 
+pub mod key;
+
 const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT";
 const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD";
 
@@ -364,3 +365,28 @@ pub fn complete_backup_source(arg: &str, param: &HashMap<String, String>) -> Vec
 
     result
 }
+
+pub fn base_directories() -> Result<xdg::BaseDirectories, Error> {
+    xdg::BaseDirectories::with_prefix("proxmox-backup").map_err(Error::from)
+}
+
+/// Convenience helper for better error messages:
+pub fn find_xdg_file(
+    file_name: impl AsRef<std::path::Path>,
+    description: &'static str,
+) -> Result<Option<std::path::PathBuf>, Error> {
+    let file_name = file_name.as_ref();
+    base_directories()
+        .map(|base| base.find_config_file(file_name))
+        .with_context(|| format!("error searching for {}", description))
+}
+
+pub fn place_xdg_file(
+    file_name: impl AsRef<std::path::Path>,
+    description: &'static str,
+) -> Result<std::path::PathBuf, Error> {
+    let file_name = file_name.as_ref();
+    base_directories()
+        .and_then(|base| base.place_config_file(file_name).map_err(Error::from))
+        .with_context(|| format!("failed to place {} in xdg home", description))
+}
-- 
2.20.1





  parent reply	other threads:[~2021-02-16 17:07 UTC|newest]

Thread overview: 50+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-02-16 17:06 [pbs-devel] [PATCH 00/22] Single file restore for VM images Stefan Reiter
2021-02-16 17:06 ` [pbs-devel] [PATCH pxar 01/22] decoder/aio: add contents() and content_size() calls Stefan Reiter
2021-02-17  7:56   ` Wolfgang Bumiller
2021-02-16 17:06 ` [pbs-devel] [PATCH pxar 02/22] decoder: add peek() Stefan Reiter
2021-02-17  8:20   ` Wolfgang Bumiller
2021-02-17  8:38     ` Stefan Reiter
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-restore-vm-data 03/22] initial commit Stefan Reiter
2021-03-15 18:35   ` [pbs-devel] applied: " Thomas Lamprecht
2021-03-16 15:33     ` Stefan Reiter
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 04/22] api2/admin/datastore: refactor list_dir_content in catalog_reader Stefan Reiter
2021-02-17  7:50   ` [pbs-devel] applied: " Thomas Lamprecht
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 05/22] api2/admin/datastore: accept "/" as path for root Stefan Reiter
2021-02-17  7:50   ` [pbs-devel] applied: " Thomas Lamprecht
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 06/22] api2/admin/datastore: refactor create_zip into pxar/extract Stefan Reiter
2021-02-17  7:50   ` [pbs-devel] applied: " Thomas Lamprecht
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 07/22] pxar/extract: add extract_sub_dir Stefan Reiter
2021-02-17  7:51   ` [pbs-devel] applied: " Thomas Lamprecht
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 08/22] pxar/extract: add sequential variants to create_zip, extract_sub_dir Stefan Reiter
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 09/22] client: extract common functions to proxmox_client_tools module Stefan Reiter
2021-02-17  6:49   ` Dietmar Maurer
2021-02-17  7:58     ` Stefan Reiter
2021-02-17  8:50       ` Dietmar Maurer
2021-02-17  9:47         ` Stefan Reiter
2021-02-17 10:12           ` Dietmar Maurer
2021-02-17  9:13   ` [pbs-devel] applied: " Dietmar Maurer
2021-02-16 17:06 ` Stefan Reiter [this message]
2021-02-17  9:11   ` [pbs-devel] [PATCH proxmox-backup 10/22] proxmox_client_tools: extract 'key' from client module Dietmar Maurer
2021-02-16 17:06 ` [pbs-devel] [PATCH proxmox-backup 11/22] file-restore: add binary and basic commands Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 12/22] file-restore: allow specifying output-format Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 13/22] rest: implement tower service for UnixStream Stefan Reiter
2021-02-17  6:52   ` [pbs-devel] applied: " Dietmar Maurer
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 14/22] client: add VsockClient to connect to virtio-vsock VMs Stefan Reiter
2021-02-17  7:24   ` [pbs-devel] applied: " Dietmar Maurer
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 15/22] file-restore-daemon: add binary with virtio-vsock API server Stefan Reiter
2021-02-17 10:17   ` Dietmar Maurer
2021-02-17 10:25   ` Dietmar Maurer
2021-02-17 10:30     ` Stefan Reiter
2021-02-17 11:13       ` Dietmar Maurer
2021-02-17 11:26   ` Dietmar Maurer
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 16/22] file-restore-daemon: add watchdog module Stefan Reiter
2021-02-17 10:52   ` Wolfgang Bumiller
2021-02-17 11:14     ` Stefan Reiter
2021-02-17 11:29       ` Wolfgang Bumiller
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 17/22] file-restore-daemon: add disk module Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 18/22] file-restore: add basic VM/block device support Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 19/22] file-restore: improve logging of VM with logrotate Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 20/22] debian/client: add postinst hook to rebuild file-restore initramfs Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 21/22] file-restore(-daemon): implement list API Stefan Reiter
2021-02-16 17:07 ` [pbs-devel] [PATCH proxmox-backup 22/22] file-restore: add 'extract' command for VM file restore Stefan Reiter
2021-02-16 17:11 ` [pbs-devel] [PATCH 00/22] Single file restore for VM images Stefan Reiter

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210216170710.31767-11-s.reiter@proxmox.com \
    --to=s.reiter@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal