From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id A6C7C6A5B5 for ; Tue, 16 Feb 2021 18:07:59 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id A4757193C7 for ; Tue, 16 Feb 2021 18:07:59 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 7024D18EAE for ; Tue, 16 Feb 2021 18:07:35 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 3D566461C0 for ; Tue, 16 Feb 2021 18:07:35 +0100 (CET) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Tue, 16 Feb 2021 18:06:58 +0100 Message-Id: <20210216170710.31767-11-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210216170710.31767-1-s.reiter@proxmox.com> References: <20210216170710.31767-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.026 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [key.rs, proxmox-backup-client.rs, snapshot.rs, mod.rs, catalog.rs] Subject: [pbs-devel] [PATCH proxmox-backup 10/22] proxmox_client_tools: extract 'key' from client module X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 16 Feb 2021 17:07:59 -0000 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 --- 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, -} - -impl KeyWithSource { - pub fn from_fd(key: Vec) -> Self { - Self { - source: KeySource::Fd, - key, - } - } - - pub fn from_default(key: Vec) -> Self { - Self { - source: KeySource::DefaultKey, - key, - } - } - - pub fn from_path(path: String, key: Vec) -> Self { - Self { - source: KeySource::Path(path), - key, - } - } -} - -#[derive(Debug, Eq, PartialEq)] -struct CryptoParams { - mode: CryptMode, - enc_key: Option, - // FIXME switch to openssl::rsa::rsa once that is Eq? - master_pubkey: Option, -} - -fn crypto_parameters(param: &Value) -> Result { - 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 = 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::with_prefix("proxmox-backup").map_err(Error::from) -} - -/// Convenience helper for better error messages: -pub fn find_xdg_file( - file_name: impl AsRef, - description: &'static str, -) -> Result, 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, - description: &'static str, -) -> Result { - 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, +} + +impl KeyWithSource { + pub fn from_fd(key: Vec) -> Self { + Self { + source: KeySource::Fd, + key, + } + } + + pub fn from_default(key: Vec) -> Self { + Self { + source: KeySource::DefaultKey, + key, + } + } + + pub fn from_path(path: String, key: Vec) -> Self { + Self { + source: KeySource::Path(path), + key, + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct CryptoParams { + pub mode: CryptMode, + pub enc_key: Option, + // FIXME switch to openssl::rsa::rsa once that is Eq? + pub master_pubkey: Option, +} + +pub fn crypto_parameters(param: &Value) -> Result { + 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 = 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, 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) -> Vec result } + +pub fn base_directories() -> Result { + 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, + description: &'static str, +) -> Result, 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, + description: &'static str, +) -> Result { + 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