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 8132E6A1DC for ; Wed, 24 Mar 2021 16:21:24 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7ECE3C618 for ; Wed, 24 Mar 2021 16:21:24 +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 975D0C2E3 for ; Wed, 24 Mar 2021 16:21:07 +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 5F5AC41904 for ; Wed, 24 Mar 2021 16:21:07 +0100 (CET) From: Stefan Reiter To: pbs-devel@lists.proxmox.com Date: Wed, 24 Mar 2021 16:18:12 +0100 Message-Id: <20210324151827.26200-6-s.reiter@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210324151827.26200-1-s.reiter@proxmox.com> References: <20210324151827.26200-1-s.reiter@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.019 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. [catalog.rs, proxmox-backup-client.rs, mod.rs, key.rs, benchmark.rs, mount.rs, snapshot.rs] Subject: [pbs-devel] [PATCH v2 proxmox-backup 05/20] proxmox_client_tools: move common key related functions to key_source.rs 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: Wed, 24 Mar 2021 15:21:24 -0000 Add a new module containing key-related functions and schemata from all over, code moved is not changed as much as possible. Requires adapting some 'use' statements across proxmox-backup-client and putting the XDG helpers quite cozily into proxmox_client_tools/mod.rs Signed-off-by: Stefan Reiter --- v2: * don't move entire key.rs, just what is necessary src/bin/proxmox-backup-client.rs | 453 +--------------- src/bin/proxmox_backup_client/benchmark.rs | 4 +- src/bin/proxmox_backup_client/catalog.rs | 3 +- src/bin/proxmox_backup_client/key.rs | 112 +--- src/bin/proxmox_backup_client/mod.rs | 28 - src/bin/proxmox_backup_client/mount.rs | 4 +- src/bin/proxmox_backup_client/snapshot.rs | 4 +- src/bin/proxmox_client_tools/key_source.rs | 573 +++++++++++++++++++++ src/bin/proxmox_client_tools/mod.rs | 48 +- 9 files changed, 631 insertions(+), 598 deletions(-) create mode 100644 src/bin/proxmox_client_tools/key_source.rs diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index 45b26c7a..50703dcb 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, @@ -71,8 +69,18 @@ use proxmox_backup::backup::{ mod proxmox_backup_client; use proxmox_backup_client::*; -mod proxmox_client_tools; -use proxmox_client_tools::*; +pub mod proxmox_client_tools; +use proxmox_client_tools::{ + complete_archive_name, complete_auth_id, complete_backup_group, complete_backup_snapshot, + complete_backup_source, complete_chunk_size, complete_group_or_snapshot, + complete_img_archive_name, complete_pxar_archive_name, complete_repository, connect, + extract_repository_from_value, + key_source::{ + crypto_parameters, format_key_source, get_encryption_key_password, KEYFD_SCHEMA, + KEYFILE_SCHEMA, MASTER_PUBKEY_FD_SCHEMA, MASTER_PUBKEY_FILE_SCHEMA, + }, + CHUNK_SIZE_SCHEMA, REPO_URL_SCHEMA, +}; fn record_repository(repo: &BackupRepository) { @@ -503,437 +511,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: { @@ -1164,7 +741,7 @@ async fn create_backup( ); let (key, created, fingerprint) = - decrypt_key(&key_with_source.key, &key::get_encryption_key_password)?; + decrypt_key(&key_with_source.key, &get_encryption_key_password)?; println!("Encryption key fingerprint: {}", fingerprint); let crypt_config = CryptConfig::new(key)?; @@ -1514,7 +1091,7 @@ async fn restore(param: Value) -> Result { None => None, Some(ref key) => { let (key, _, _) = - decrypt_key(&key.key, &key::get_encryption_key_password).map_err(|err| { + decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| { eprintln!("{}", format_key_source(&key.source, "encryption")); err })?; diff --git a/src/bin/proxmox_backup_client/benchmark.rs b/src/bin/proxmox_backup_client/benchmark.rs index 1076dc19..c1673701 100644 --- a/src/bin/proxmox_backup_client/benchmark.rs +++ b/src/bin/proxmox_backup_client/benchmark.rs @@ -34,6 +34,8 @@ use crate::{ connect, }; +use crate::proxmox_client_tools::key_source::get_encryption_key_password; + #[api()] #[derive(Copy, Clone, Serialize)] /// Speed test result @@ -152,7 +154,7 @@ pub async fn benchmark( let crypt_config = match keyfile { None => None, Some(path) => { - let (key, _, _) = load_and_decrypt_key(&path, &crate::key::get_encryption_key_password)?; + let (key, _, _) = load_and_decrypt_key(&path, &get_encryption_key_password)?; let crypt_config = CryptConfig::new(key)?; Some(Arc::new(crypt_config)) } diff --git a/src/bin/proxmox_backup_client/catalog.rs b/src/bin/proxmox_backup_client/catalog.rs index 659200ff..f4b0a1d5 100644 --- a/src/bin/proxmox_backup_client/catalog.rs +++ b/src/bin/proxmox_backup_client/catalog.rs @@ -17,7 +17,6 @@ use crate::{ extract_repository_from_value, format_key_source, record_repository, - key::get_encryption_key_password, decrypt_key, api_datastore_latest_snapshot, complete_repository, @@ -38,6 +37,8 @@ use crate::{ Shell, }; +use crate::proxmox_client_tools::key_source::get_encryption_key_password; + #[api( input: { properties: { diff --git a/src/bin/proxmox_backup_client/key.rs b/src/bin/proxmox_backup_client/key.rs index 76b135a2..c442fad9 100644 --- a/src/bin/proxmox_backup_client/key.rs +++ b/src/bin/proxmox_backup_client/key.rs @@ -20,114 +20,10 @@ use proxmox_backup::{ 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"; - -pub fn find_default_master_pubkey() -> Result, Error> { - super::find_xdg_file( - DEFAULT_MASTER_PUBKEY_FILE_NAME, - "default master public key file", - ) -} - -pub fn place_default_master_pubkey() -> Result { - super::place_xdg_file( - DEFAULT_MASTER_PUBKEY_FILE_NAME, - "default master public key file", - ) -} - -pub fn find_default_encryption_key() -> Result, Error> { - super::find_xdg_file( - DEFAULT_ENCRYPTION_KEY_FILE_NAME, - "default encryption key file", - ) -} - -pub fn place_default_encryption_key() -> Result { - super::place_xdg_file( - DEFAULT_ENCRYPTION_KEY_FILE_NAME, - "default encryption key file", - ) -} - -#[cfg(not(test))] -pub(crate) fn read_optional_default_encryption_key() -> Result, Error> { - find_default_encryption_key()? - .map(|path| file_get_contents(path).map(KeyWithSource::from_default)) - .transpose() -} - -#[cfg(not(test))] -pub(crate) fn read_optional_default_master_pubkey() -> Result, Error> { - find_default_master_pubkey()? - .map(|path| file_get_contents(path).map(KeyWithSource::from_default)) - .transpose() -} - -#[cfg(test)] -static mut TEST_DEFAULT_ENCRYPTION_KEY: Result>, Error> = Ok(None); - -#[cfg(test)] -pub(crate) fn read_optional_default_encryption_key() -> Result, Error> { - // not safe when multiple concurrent test cases end up here! - unsafe { - match &TEST_DEFAULT_ENCRYPTION_KEY { - Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))), - Ok(None) => Ok(None), - Err(_) => bail!("test error"), - } - } -} - -#[cfg(test)] -// not safe when multiple concurrent test cases end up here! -pub(crate) unsafe fn set_test_encryption_key(value: Result>, Error>) { - TEST_DEFAULT_ENCRYPTION_KEY = value; -} - -#[cfg(test)] -static mut TEST_DEFAULT_MASTER_PUBKEY: Result>, Error> = Ok(None); - -#[cfg(test)] -pub(crate) fn read_optional_default_master_pubkey() -> Result, Error> { - // not safe when multiple concurrent test cases end up here! - unsafe { - match &TEST_DEFAULT_MASTER_PUBKEY { - Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))), - Ok(None) => Ok(None), - Err(_) => bail!("test error"), - } - } -} - -#[cfg(test)] -// not safe when multiple concurrent test cases end up here! -pub(crate) unsafe fn set_test_default_master_pubkey(value: Result>, Error>) { - TEST_DEFAULT_MASTER_PUBKEY = value; -} - -pub fn get_encryption_key_password() -> Result, Error> { - // fixme: implement other input methods - - use std::env::VarError::*; - match std::env::var("PBS_ENCRYPTION_PASSWORD") { - Ok(p) => return Ok(p.as_bytes().to_vec()), - Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"), - Err(NotPresent) => { - // Try another method - } - } - - // If we're on a TTY, query the user for a password - if tty::stdin_isatty() { - return Ok(tty::read_password("Encryption Key Password: ")?); - } - - bail!("no password input mechanism available"); -} +use crate::proxmox_client_tools::key_source::{ + find_default_encryption_key, find_default_master_pubkey, get_encryption_key_password, + place_default_encryption_key, place_default_master_pubkey, +}; #[api( input: { diff --git a/src/bin/proxmox_backup_client/mod.rs b/src/bin/proxmox_backup_client/mod.rs index a14b0dc1..d272dc8f 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; @@ -13,29 +11,3 @@ 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/mount.rs b/src/bin/proxmox_backup_client/mount.rs index be6aca05..f3498e35 100644 --- a/src/bin/proxmox_backup_client/mount.rs +++ b/src/bin/proxmox_backup_client/mount.rs @@ -43,6 +43,8 @@ use crate::{ BufferedDynamicReadAt, }; +use crate::proxmox_client_tools::key_source::get_encryption_key_password; + #[sortable] const API_METHOD_MOUNT: ApiMethod = ApiMethod::new( &ApiHandler::Sync(&mount), @@ -182,7 +184,7 @@ async fn mount_do(param: Value, pipe: Option) -> Result { None => None, Some(path) => { println!("Encryption key file: '{:?}'", path); - let (key, _, fingerprint) = load_and_decrypt_key(&path, &crate::key::get_encryption_key_password)?; + let (key, _, fingerprint) = load_and_decrypt_key(&path, &get_encryption_key_password)?; println!("Encryption key fingerprint: '{}'", fingerprint); Some(Arc::new(CryptConfig::new(key)?)) } diff --git a/src/bin/proxmox_backup_client/snapshot.rs b/src/bin/proxmox_backup_client/snapshot.rs index 5988ebf6..a98b1ca2 100644 --- a/src/bin/proxmox_backup_client/snapshot.rs +++ b/src/bin/proxmox_backup_client/snapshot.rs @@ -35,6 +35,8 @@ use crate::{ record_repository, }; +use crate::proxmox_client_tools::key_source::get_encryption_key_password; + #[api( input: { properties: { @@ -239,7 +241,7 @@ async fn upload_log(param: Value) -> Result { let crypt_config = match crypto.enc_key { None => None, Some(key) => { - let (key, _created, _) = decrypt_key(&key.key, &crate::key::get_encryption_key_password)?; + let (key, _created, _) = decrypt_key(&key.key, &get_encryption_key_password)?; let crypt_config = CryptConfig::new(key)?; Some(Arc::new(crypt_config)) } diff --git a/src/bin/proxmox_client_tools/key_source.rs b/src/bin/proxmox_client_tools/key_source.rs new file mode 100644 index 00000000..92132ba5 --- /dev/null +++ b/src/bin/proxmox_client_tools/key_source.rs @@ -0,0 +1,573 @@ +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; + +use proxmox::api::schema::*; +use proxmox::sys::linux::tty; +use proxmox::tools::fs::file_get_contents; + +use proxmox_backup::backup::CryptMode; + +pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json"; +pub const DEFAULT_MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem"; + +pub const KEYFILE_SCHEMA: Schema = + StringSchema::new("Path to encryption key. All data will be encrypted using this key.") + .schema(); + +pub const KEYFD_SCHEMA: Schema = + IntegerSchema::new("Pass an encryption key via an already opened file descriptor.") + .minimum(0) + .schema(); + +pub const MASTER_PUBKEY_FILE_SCHEMA: Schema = StringSchema::new( + "Path to master public key. The encryption key used for a backup will be encrypted using this key and appended to the backup.") + .schema(); + +pub const MASTER_PUBKEY_FD_SCHEMA: Schema = + IntegerSchema::new("Pass a master public key via an already opened file descriptor.") + .minimum(0) + .schema(); + +#[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, + "default master public key file", + ) +} + +pub fn place_default_master_pubkey() -> Result { + super::place_xdg_file( + DEFAULT_MASTER_PUBKEY_FILE_NAME, + "default master public key file", + ) +} + +pub fn find_default_encryption_key() -> Result, Error> { + super::find_xdg_file( + DEFAULT_ENCRYPTION_KEY_FILE_NAME, + "default encryption key file", + ) +} + +pub fn place_default_encryption_key() -> Result { + super::place_xdg_file( + DEFAULT_ENCRYPTION_KEY_FILE_NAME, + "default encryption key file", + ) +} + +#[cfg(not(test))] +pub(crate) fn read_optional_default_encryption_key() -> Result, Error> { + find_default_encryption_key()? + .map(|path| file_get_contents(path).map(KeyWithSource::from_default)) + .transpose() +} + +#[cfg(not(test))] +pub(crate) fn read_optional_default_master_pubkey() -> Result, Error> { + find_default_master_pubkey()? + .map(|path| file_get_contents(path).map(KeyWithSource::from_default)) + .transpose() +} + +#[cfg(test)] +static mut TEST_DEFAULT_ENCRYPTION_KEY: Result>, Error> = Ok(None); + +#[cfg(test)] +pub(crate) fn read_optional_default_encryption_key() -> Result, Error> { + // not safe when multiple concurrent test cases end up here! + unsafe { + match &TEST_DEFAULT_ENCRYPTION_KEY { + Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))), + Ok(None) => Ok(None), + Err(_) => bail!("test error"), + } + } +} + +#[cfg(test)] +// not safe when multiple concurrent test cases end up here! +pub(crate) unsafe fn set_test_encryption_key(value: Result>, Error>) { + TEST_DEFAULT_ENCRYPTION_KEY = value; +} + +#[cfg(test)] +static mut TEST_DEFAULT_MASTER_PUBKEY: Result>, Error> = Ok(None); + +#[cfg(test)] +pub(crate) fn read_optional_default_master_pubkey() -> Result, Error> { + // not safe when multiple concurrent test cases end up here! + unsafe { + match &TEST_DEFAULT_MASTER_PUBKEY { + Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))), + Ok(None) => Ok(None), + Err(_) => bail!("test error"), + } + } +} + +#[cfg(test)] +// not safe when multiple concurrent test cases end up here! +pub(crate) unsafe fn set_test_default_master_pubkey(value: Result>, Error>) { + TEST_DEFAULT_MASTER_PUBKEY = value; +} + +pub fn get_encryption_key_password() -> Result, Error> { + // fixme: implement other input methods + + use std::env::VarError::*; + match std::env::var("PBS_ENCRYPTION_PASSWORD") { + Ok(p) => return Ok(p.as_bytes().to_vec()), + Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"), + Err(NotPresent) => { + // Try another method + } + } + + // If we're on a TTY, query the user for a password + if tty::stdin_isatty() { + return Ok(tty::read_password("Encryption Key Password: ")?); + } + + bail!("no password input mechanism available"); +} + +#[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; + use proxmox::tools::fs::{replace_file, CreateOptions}; + + 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..73744ba2 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_source; + const ENV_VAR_PBS_FINGERPRINT: &str = "PBS_FINGERPRINT"; const ENV_VAR_PBS_PASSWORD: &str = "PBS_PASSWORD"; @@ -25,24 +26,6 @@ pub const REPO_URL_SCHEMA: Schema = StringSchema::new("Repository URL.") .max_length(256) .schema(); -pub const KEYFILE_SCHEMA: Schema = - StringSchema::new("Path to encryption key. All data will be encrypted using this key.") - .schema(); - -pub const KEYFD_SCHEMA: Schema = - IntegerSchema::new("Pass an encryption key via an already opened file descriptor.") - .minimum(0) - .schema(); - -pub const MASTER_PUBKEY_FILE_SCHEMA: Schema = StringSchema::new( - "Path to master public key. The encryption key used for a backup will be encrypted using this key and appended to the backup.") - .schema(); - -pub const MASTER_PUBKEY_FD_SCHEMA: Schema = - IntegerSchema::new("Pass a master public key via an already opened file descriptor.") - .minimum(0) - .schema(); - pub const CHUNK_SIZE_SCHEMA: Schema = IntegerSchema::new("Chunk size in KB. Must be a power of 2.") .minimum(64) .maximum(4096) @@ -364,3 +347,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