From 9696f5193b5972230e311dcdba6c6bedd91105cc Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 6 Jul 2020 11:39:24 +0200 Subject: [PATCH] client: move key management into separate module and use api macro for methods and Kdf type Signed-off-by: Wolfgang Bumiller --- src/bin/proxmox-backup-client.rs | 246 +------------------ src/bin/proxmox_backup_client/benchmark.rs | 3 +- src/bin/proxmox_backup_client/catalog.rs | 3 +- src/bin/proxmox_backup_client/key.rs | 267 +++++++++++++++++++++ src/bin/proxmox_backup_client/mod.rs | 1 + src/bin/proxmox_backup_client/mount.rs | 3 +- 6 files changed, 275 insertions(+), 248 deletions(-) create mode 100644 src/bin/proxmox_backup_client/key.rs diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index 4cd59448..ff7325d6 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -14,9 +14,7 @@ use tokio::sync::mpsc; use xdg::BaseDirectories; use pathpatterns::{MatchEntry, MatchType, PatternFlag}; -use proxmox::{sortable, identity}; use proxmox::tools::fs::{file_get_contents, file_get_json, replace_file, CreateOptions, image_size}; -use proxmox::sys::linux::tty; use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment}; use proxmox::api::schema::*; use proxmox::api::cli::*; @@ -29,9 +27,7 @@ use proxmox_backup::client::*; use proxmox_backup::pxar::catalog::*; use proxmox_backup::backup::{ archive_type, - encrypt_key_with_passphrase, load_and_decrypt_key, - store_key_config, verify_chunk_size, ArchiveType, AsyncReadChunk, @@ -49,7 +45,6 @@ use proxmox_backup::backup::{ FixedChunkStream, FixedIndexReader, IndexFile, - KeyConfig, MANIFEST_BLOB_NAME, Shell, }; @@ -861,7 +856,7 @@ async fn create_backup( let (crypt_config, rsa_encrypted_key) = match keyfile { None => (None, None), Some(path) => { - let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?; + let (key, created) = load_and_decrypt_key(&path, &key::get_encryption_key_password)?; let crypt_config = CryptConfig::new(key)?; @@ -1159,7 +1154,7 @@ async fn restore(param: Value) -> Result { let crypt_config = match keyfile { None => None, Some(path) => { - let (key, _) = load_and_decrypt_key(&path, &get_encryption_key_password)?; + let (key, _) = load_and_decrypt_key(&path, &key::get_encryption_key_password)?; Some(Arc::new(CryptConfig::new(key)?)) } }; @@ -1303,7 +1298,7 @@ async fn upload_log(param: Value) -> Result { let crypt_config = match keyfile { None => None, Some(path) => { - let (key, _created) = load_and_decrypt_key(&path, &get_encryption_key_password)?; + let (key, _created) = load_and_decrypt_key(&path, &key::get_encryption_key_password)?; let crypt_config = CryptConfig::new(key)?; Some(Arc::new(crypt_config)) } @@ -1668,69 +1663,6 @@ fn complete_chunk_size(_arg: &str, _param: &HashMap) -> Vec 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"); -} - -fn key_create( - param: Value, - _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - let path = tools::required_string_param(¶m, "path")?; - let path = PathBuf::from(path); - - let kdf = param["kdf"].as_str().unwrap_or("scrypt"); - - let key = proxmox::sys::linux::random_data(32)?; - - if kdf == "scrypt" { - // always read passphrase from tty - if !tty::stdin_isatty() { - bail!("unable to read passphrase - no tty"); - } - - let password = tty::read_and_verify_password("Encryption Key Password: ")?; - - let key_config = encrypt_key_with_passphrase(&key, &password)?; - - store_key_config(&path, false, key_config)?; - - Ok(Value::Null) - } else if kdf == "none" { - let created = Local.timestamp(Local::now().timestamp(), 0); - - store_key_config(&path, false, KeyConfig { - kdf: None, - created, - modified: created, - data: key, - })?; - - Ok(Value::Null) - } else { - unreachable!(); - } -} - fn master_pubkey_path() -> Result { let base = BaseDirectories::with_prefix("proxmox-backup")?; @@ -1740,176 +1672,6 @@ fn master_pubkey_path() -> Result { Ok(path) } -fn key_import_master_pubkey( - param: Value, - _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - let path = tools::required_string_param(¶m, "path")?; - let path = PathBuf::from(path); - - let pem_data = file_get_contents(&path)?; - - if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) { - bail!("Unable to decode PEM data - {}", err); - } - - let target_path = master_pubkey_path()?; - - replace_file(&target_path, &pem_data, CreateOptions::new())?; - - println!("Imported public master key to {:?}", target_path); - - Ok(Value::Null) -} - -fn key_create_master_key( - _param: Value, - _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - // we need a TTY to query the new password - if !tty::stdin_isatty() { - bail!("unable to create master key - no tty"); - } - - let rsa = openssl::rsa::Rsa::generate(4096)?; - let pkey = openssl::pkey::PKey::from_rsa(rsa)?; - - - let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?; - - let pub_key: Vec = pkey.public_key_to_pem()?; - let filename_pub = "master-public.pem"; - println!("Writing public master key to {}", filename_pub); - replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?; - - let cipher = openssl::symm::Cipher::aes_256_cbc(); - let priv_key: Vec = pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?; - - let filename_priv = "master-private.pem"; - println!("Writing private master key to {}", filename_priv); - replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?; - - Ok(Value::Null) -} - -fn key_change_passphrase( - param: Value, - _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - let path = tools::required_string_param(¶m, "path")?; - let path = PathBuf::from(path); - - let kdf = param["kdf"].as_str().unwrap_or("scrypt"); - - // we need a TTY to query the new password - if !tty::stdin_isatty() { - bail!("unable to change passphrase - no tty"); - } - - let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?; - - if kdf == "scrypt" { - - let password = tty::read_and_verify_password("New Password: ")?; - - let mut new_key_config = encrypt_key_with_passphrase(&key, &password)?; - new_key_config.created = created; // keep original value - - store_key_config(&path, true, new_key_config)?; - - Ok(Value::Null) - } else if kdf == "none" { - let modified = Local.timestamp(Local::now().timestamp(), 0); - - store_key_config(&path, true, KeyConfig { - kdf: None, - created, // keep original value - modified, - data: key.to_vec(), - })?; - - Ok(Value::Null) - } else { - unreachable!(); - } -} - -fn key_mgmt_cli() -> CliCommandMap { - - const KDF_SCHEMA: Schema = - StringSchema::new("Key derivation function. Choose 'none' to store the key unecrypted.") - .format(&ApiStringFormat::Enum(&[ - EnumEntry::new("scrypt", "SCrypt"), - EnumEntry::new("none", "Do not encrypt the key")])) - .default("scrypt") - .schema(); - - #[sortable] - const API_METHOD_KEY_CREATE: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&key_create), - &ObjectSchema::new( - "Create a new encryption key.", - &sorted!([ - ("path", false, &StringSchema::new("File system path.").schema()), - ("kdf", true, &KDF_SCHEMA), - ]), - ) - ); - - let key_create_cmd_def = CliCommand::new(&API_METHOD_KEY_CREATE) - .arg_param(&["path"]) - .completion_cb("path", tools::complete_file_name); - - #[sortable] - const API_METHOD_KEY_CHANGE_PASSPHRASE: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&key_change_passphrase), - &ObjectSchema::new( - "Change the passphrase required to decrypt the key.", - &sorted!([ - ("path", false, &StringSchema::new("File system path.").schema()), - ("kdf", true, &KDF_SCHEMA), - ]), - ) - ); - - let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_KEY_CHANGE_PASSPHRASE) - .arg_param(&["path"]) - .completion_cb("path", tools::complete_file_name); - - const API_METHOD_KEY_CREATE_MASTER_KEY: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&key_create_master_key), - &ObjectSchema::new("Create a new 4096 bit RSA master pub/priv key pair.", &[]) - ); - - let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_KEY_CREATE_MASTER_KEY); - - #[sortable] - const API_METHOD_KEY_IMPORT_MASTER_PUBKEY: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&key_import_master_pubkey), - &ObjectSchema::new( - "Import a new RSA public key and use it as master key. The key is expected to be in '.pem' format.", - &sorted!([ ("path", false, &StringSchema::new("File system path.").schema()) ]), - ) - ); - - let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_KEY_IMPORT_MASTER_PUBKEY) - .arg_param(&["path"]) - .completion_cb("path", tools::complete_file_name); - - CliCommandMap::new() - .insert("create", key_create_cmd_def) - .insert("create-master-key", key_create_master_key_cmd_def) - .insert("import-master-pubkey", key_import_master_pubkey_cmd_def) - .insert("change-passphrase", key_change_passphrase_cmd_def) -} - - use proxmox_backup::client::RemoteChunkReader; /// This is a workaround until we have cleaned up the chunk/reader/... infrastructure for better /// async use! @@ -2027,7 +1789,7 @@ fn main() { .insert("snapshots", snapshots_cmd_def) .insert("files", files_cmd_def) .insert("status", status_cmd_def) - .insert("key", key_mgmt_cli()) + .insert("key", key::cli()) .insert("mount", mount_cmd_def()) .insert("catalog", catalog_mgmt_cli()) .insert("task", task_mgmt_cli()) diff --git a/src/bin/proxmox_backup_client/benchmark.rs b/src/bin/proxmox_backup_client/benchmark.rs index 4b1c8d4c..7ed95a7d 100644 --- a/src/bin/proxmox_backup_client/benchmark.rs +++ b/src/bin/proxmox_backup_client/benchmark.rs @@ -19,7 +19,6 @@ use proxmox_backup::client::*; use crate::{ KEYFILE_SCHEMA, REPO_URL_SCHEMA, extract_repository_from_value, - get_encryption_key_password, record_repository, connect, }; @@ -52,7 +51,7 @@ pub async fn benchmark( let crypt_config = match keyfile { None => None, Some(path) => { - let (key, _) = load_and_decrypt_key(&path, &get_encryption_key_password)?; + let (key, _) = load_and_decrypt_key(&path, &crate::key::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 0a0eaeff..595b5bab 100644 --- a/src/bin/proxmox_backup_client/catalog.rs +++ b/src/bin/proxmox_backup_client/catalog.rs @@ -16,7 +16,6 @@ use crate::{ REPO_URL_SCHEMA, extract_repository_from_value, record_repository, - get_encryption_key_password, load_and_decrypt_key, api_datastore_latest_snapshot, complete_repository, @@ -36,7 +35,7 @@ use crate::{ Shell, }; - +use crate::key::get_encryption_key_password; #[api( input: { diff --git a/src/bin/proxmox_backup_client/key.rs b/src/bin/proxmox_backup_client/key.rs new file mode 100644 index 00000000..290af5f9 --- /dev/null +++ b/src/bin/proxmox_backup_client/key.rs @@ -0,0 +1,267 @@ +use std::path::PathBuf; + +use anyhow::{bail, Error}; +use chrono::{Local, TimeZone}; +use serde::{Deserialize, Serialize}; +use xdg::BaseDirectories; + +use proxmox::api::api; +use proxmox::api::cli::{CliCommand, CliCommandMap}; +use proxmox::sys::linux::tty; +use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions}; + +use proxmox_backup::backup::{ + encrypt_key_with_passphrase, load_and_decrypt_key, store_key_config, KeyConfig, +}; +use proxmox_backup::tools; + +pub fn master_pubkey_path() -> Result { + let base = BaseDirectories::with_prefix("proxmox-backup")?; + + // usually $HOME/.config/proxmox-backup/master-public.pem + let path = base.place_config_file("master-public.pem")?; + + Ok(path) +} + +pub fn default_encryption_key_path() -> Result { + let base = BaseDirectories::with_prefix("proxmox-backup")?; + + // usually $HOME/.config/proxmox-backup/encryption-key.json + let path = base.place_config_file("encryption-key.json")?; + + Ok(path) +} + +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"); +} + +#[api( + default: "scrypt", +)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +/// Key derivation function for password protected encryption keys. +pub enum Kdf { + /// Do not encrypt the key. + None, + + /// Encrypt they key with a password using SCrypt. + Scrypt, +} + +impl Default for Kdf { + #[inline] + fn default() -> Self { + Kdf::Scrypt + } +} + +#[api( + input: { + properties: { + kdf: { + type: Kdf, + optional: true, + }, + path: { + description: + "Output file. Without this the key will become the new default encryption key.", + optional: true, + } + }, + }, +)] +/// Create a new encryption key. +fn create(kdf: Option, path: Option) -> Result<(), Error> { + let path = match path { + Some(path) => PathBuf::from(path), + None => default_encryption_key_path()?, + }; + + let kdf = kdf.unwrap_or_default(); + + let key = proxmox::sys::linux::random_data(32)?; + + match kdf { + Kdf::None => { + let created = Local.timestamp(Local::now().timestamp(), 0); + + store_key_config( + &path, + false, + KeyConfig { + kdf: None, + created, + modified: created, + data: key, + }, + )?; + } + Kdf::Scrypt => { + // always read passphrase from tty + if !tty::stdin_isatty() { + bail!("unable to read passphrase - no tty"); + } + + let password = tty::read_and_verify_password("Encryption Key Password: ")?; + + let key_config = encrypt_key_with_passphrase(&key, &password)?; + + store_key_config(&path, false, key_config)?; + } + } + + Ok(()) +} + +#[api( + input: { + properties: { + kdf: { + type: Kdf, + optional: true, + }, + path: { + description: "Key file. Without this the default key's password will be changed.", + optional: true, + } + }, + }, +)] +/// Change the encryption key's password. +fn change_passphrase(kdf: Option, path: Option) -> Result<(), Error> { + let path = match path { + Some(path) => PathBuf::from(path), + None => default_encryption_key_path()?, + }; + + let kdf = kdf.unwrap_or_default(); + + if !tty::stdin_isatty() { + bail!("unable to change passphrase - no tty"); + } + + let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?; + + match kdf { + Kdf::None => { + let modified = Local.timestamp(Local::now().timestamp(), 0); + + store_key_config( + &path, + true, + KeyConfig { + kdf: None, + created, // keep original value + modified, + data: key.to_vec(), + }, + )?; + } + Kdf::Scrypt => { + let password = tty::read_and_verify_password("New Password: ")?; + + let mut new_key_config = encrypt_key_with_passphrase(&key, &password)?; + new_key_config.created = created; // keep original value + + store_key_config(&path, true, new_key_config)?; + } + } + + Ok(()) +} + +#[api( + input: { + properties: { + path: { + description: "Path to the PEM formatted RSA public key.", + }, + }, + }, +)] +/// Import an RSA public key used to put an encrypted version of the symmetric backup encryption +/// key onto the backup server along with each backup. +fn import_master_pubkey(path: String) -> Result<(), Error> { + let pem_data = file_get_contents(&path)?; + + if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) { + bail!("Unable to decode PEM data - {}", err); + } + + let target_path = master_pubkey_path()?; + + replace_file(&target_path, &pem_data, CreateOptions::new())?; + + println!("Imported public master key to {:?}", target_path); + + Ok(()) +} + +#[api] +/// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup +/// encryption key onto the backup server along with each backup. +fn create_master_key() -> Result<(), Error> { + // we need a TTY to query the new password + if !tty::stdin_isatty() { + bail!("unable to create master key - no tty"); + } + + let rsa = openssl::rsa::Rsa::generate(4096)?; + let pkey = openssl::pkey::PKey::from_rsa(rsa)?; + + let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?; + + let pub_key: Vec = pkey.public_key_to_pem()?; + let filename_pub = "master-public.pem"; + println!("Writing public master key to {}", filename_pub); + replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?; + + let cipher = openssl::symm::Cipher::aes_256_cbc(); + let priv_key: Vec = pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?; + + let filename_priv = "master-private.pem"; + println!("Writing private master key to {}", filename_priv); + replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?; + + Ok(()) +} + +pub fn cli() -> CliCommandMap { + let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE) + .arg_param(&["path"]) + .completion_cb("path", tools::complete_file_name); + + let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE) + .arg_param(&["path"]) + .completion_cb("path", tools::complete_file_name); + + let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY); + let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY) + .arg_param(&["path"]) + .completion_cb("path", tools::complete_file_name); + + CliCommandMap::new() + .insert("create", key_create_cmd_def) + .insert("create-master-key", key_create_master_key_cmd_def) + .insert("import-master-pubkey", key_import_master_pubkey_cmd_def) + .insert("change-passphrase", key_change_passphrase_cmd_def) +} diff --git a/src/bin/proxmox_backup_client/mod.rs b/src/bin/proxmox_backup_client/mod.rs index a6715a6d..054aff5b 100644 --- a/src/bin/proxmox_backup_client/mod.rs +++ b/src/bin/proxmox_backup_client/mod.rs @@ -7,3 +7,4 @@ pub use task::*; mod catalog; pub use catalog::*; +pub mod key; diff --git a/src/bin/proxmox_backup_client/mount.rs b/src/bin/proxmox_backup_client/mount.rs index 19772b6c..15cd663c 100644 --- a/src/bin/proxmox_backup_client/mount.rs +++ b/src/bin/proxmox_backup_client/mount.rs @@ -30,7 +30,6 @@ use proxmox_backup::client::*; use crate::{ REPO_URL_SCHEMA, extract_repository_from_value, - get_encryption_key_password, complete_pxar_archive_name, complete_group_or_snapshot, complete_repository, @@ -119,7 +118,7 @@ async fn mount_do(param: Value, pipe: Option) -> Result { let crypt_config = match keyfile { None => None, Some(path) => { - let (key, _) = load_and_decrypt_key(&path, &get_encryption_key_password)?; + let (key, _) = load_and_decrypt_key(&path, &crate::key::get_encryption_key_password)?; Some(Arc::new(CryptConfig::new(key)?)) } };