]> git.proxmox.com Git - proxmox-backup.git/commitdiff
client: move key management into separate module
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 6 Jul 2020 09:39:24 +0000 (11:39 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 6 Jul 2020 12:36:04 +0000 (14:36 +0200)
and use api macro for methods and Kdf type

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
src/bin/proxmox-backup-client.rs
src/bin/proxmox_backup_client/benchmark.rs
src/bin/proxmox_backup_client/catalog.rs
src/bin/proxmox_backup_client/key.rs [new file with mode: 0644]
src/bin/proxmox_backup_client/mod.rs
src/bin/proxmox_backup_client/mount.rs

index 4cd5944836e752b61135f1e4f55dd3a6006c5574..ff7325d6770f53b3ab59af513a7fe4271ce77848 100644 (file)
@@ -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<Value, Error> {
     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<Value, Error> {
     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<String, String>) -> Vec<Stri
     result
 }
 
-fn get_encryption_key_password() -> Result<Vec<u8>, 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<Value, Error> {
-
-    let path = tools::required_string_param(&param, "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<PathBuf, Error> {
     let base = BaseDirectories::with_prefix("proxmox-backup")?;
 
@@ -1740,176 +1672,6 @@ fn master_pubkey_path() -> Result<PathBuf, Error> {
     Ok(path)
 }
 
-fn key_import_master_pubkey(
-    param: Value,
-    _info: &ApiMethod,
-    _rpcenv: &mut dyn RpcEnvironment,
-) -> Result<Value, Error> {
-
-    let path = tools::required_string_param(&param, "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<Value, 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<u8> = 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<u8> = 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<Value, Error> {
-
-    let path = tools::required_string_param(&param, "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())
index 4b1c8d4ca0cad7867c0813fa3c351f931a17347c..7ed95a7d5b973da985910768ef6a782f17732e4f 100644 (file)
@@ -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))
         }
index 0a0eaefff56485f5520d9ced3ae788120c48a9db..595b5bab08bf0e6297a85f75fad51a7436b0e0af 100644 (file)
@@ -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 (file)
index 0000000..290af5f
--- /dev/null
@@ -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<PathBuf, Error> {
+    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<PathBuf, Error> {
+    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<Vec<u8>, 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<Kdf>, path: Option<String>) -> 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<Kdf>, path: Option<String>) -> 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<u8> = 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<u8> = 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)
+}
index a6715a6d8ad428ca34b83c8b1553a9f1d48b212f..054aff5bd254ad871e2ce5c6225c98dd69c45ff2 100644 (file)
@@ -7,3 +7,4 @@ pub use task::*;
 mod catalog;
 pub use catalog::*;
 
+pub mod key;
index 19772b6c097d384f564dbbd338e71e19f2b21ae7..15cd663c89511be5fadbf8698c8153057ecf749a 100644 (file)
@@ -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<RawFd>) -> Result<Value, Error> {
     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)?))
         }
     };