src/bin/proxmox-backup-client.rs: start key management
This commit is contained in:
parent
a7dd483097
commit
f2401311b0
@ -26,6 +26,13 @@ pub struct CryptConfig {
|
|||||||
enc_key: [u8; 32],
|
enc_key: [u8; 32],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SCryptConfig {
|
||||||
|
pub n: u64,
|
||||||
|
pub r: u64,
|
||||||
|
pub p: u64,
|
||||||
|
pub salt: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
impl CryptConfig {
|
impl CryptConfig {
|
||||||
|
|
||||||
/// Create a new instance.
|
/// Create a new instance.
|
||||||
@ -47,34 +54,21 @@ impl CryptConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A simple key derivation function using scrypt
|
/// A simple key derivation function using scrypt
|
||||||
fn derive_key_from_password(password: &[u8]) -> Result<[u8; 32], Error> {
|
pub fn derive_key_from_password(password: &[u8], scrypt_config: &SCryptConfig) -> Result<[u8; 32], Error> {
|
||||||
|
|
||||||
let mut key = [0u8; 32];
|
let mut key = [0u8; 32];
|
||||||
|
|
||||||
// estimated scrypt memory usage is N*2r*64
|
// estimated scrypt memory usage is 128*r*n*p
|
||||||
let n = 65536;
|
|
||||||
let r = 8;
|
|
||||||
let p = 1;
|
|
||||||
|
|
||||||
let salt = b""; // Salt??
|
|
||||||
|
|
||||||
scrypt(
|
scrypt(
|
||||||
password,
|
password,
|
||||||
salt,
|
&scrypt_config.salt,
|
||||||
n, r, p, 128*1024*1024,
|
scrypt_config.n, scrypt_config.r, scrypt_config.p, 1025*1024*1024,
|
||||||
&mut key)?;
|
&mut key)?;
|
||||||
|
|
||||||
Ok(key)
|
Ok(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new instance, but derive key from password using scrypt.
|
|
||||||
pub fn with_password(password: &[u8]) -> Result<Self, Error> {
|
|
||||||
|
|
||||||
let enc_key = Self::derive_key_from_password(password)?;
|
|
||||||
|
|
||||||
Self::new(enc_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute a chunk digest using a secret name space.
|
/// Compute a chunk digest using a secret name space.
|
||||||
///
|
///
|
||||||
/// Computes an SHA256 checksum over some secret data (derived
|
/// Computes an SHA256 checksum over some secret data (derived
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#[macro_use]
|
||||||
extern crate proxmox_backup;
|
extern crate proxmox_backup;
|
||||||
|
|
||||||
use failure::*;
|
use failure::*;
|
||||||
@ -29,6 +30,13 @@ use tokio::sync::mpsc;
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref BACKUPSPEC_REGEX: Regex = Regex::new(r"^([a-zA-Z0-9_-]+\.(?:pxar|img|conf)):(.+)$").unwrap();
|
static ref BACKUPSPEC_REGEX: Regex = Regex::new(r"^([a-zA-Z0-9_-]+\.(?:pxar|img|conf)):(.+)$").unwrap();
|
||||||
|
|
||||||
|
static ref REPO_URL_SCHEMA: Arc<Schema> = Arc::new(
|
||||||
|
StringSchema::new("Repository URL.")
|
||||||
|
.format(BACKUP_REPO_URL.clone())
|
||||||
|
.max_length(256)
|
||||||
|
.into()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -774,14 +782,119 @@ fn complete_chunk_size(_arg: &str, _param: &HashMap<String, String>) -> Vec<Stri
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn get_encryption_key_password() -> Result<String, Error> {
|
||||||
|
|
||||||
let repo_url_schema: Arc<Schema> = Arc::new(
|
// fixme: implement other input methods
|
||||||
StringSchema::new("Repository URL.")
|
|
||||||
.format(BACKUP_REPO_URL.clone())
|
use std::env::VarError::*;
|
||||||
.max_length(256)
|
match std::env::var("PBS_ENCRYPTION_PASSWORD") {
|
||||||
.into()
|
Ok(p) => return Ok(p),
|
||||||
);
|
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 crate::tools::tty::stdin_isatty() {
|
||||||
|
return Ok(String::from_utf8(crate::tools::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 repo_url = tools::required_string_param(¶m, "repository")?;
|
||||||
|
let repo: BackupRepository = repo_url.parse()?;
|
||||||
|
|
||||||
|
let base = BaseDirectories::with_prefix("proxmox-backup")?;
|
||||||
|
|
||||||
|
let repo = repo.to_string();
|
||||||
|
|
||||||
|
// usually $HOME/.config/proxmox-backup/xxx.enc_key
|
||||||
|
let path = base.place_config_file(&format!("{}.enc_key", repo))?;
|
||||||
|
|
||||||
|
let password = get_encryption_key_password()?;
|
||||||
|
|
||||||
|
let raw_key = proxmox::sys::linux::random_data(32)?;
|
||||||
|
|
||||||
|
let salt = proxmox::sys::linux::random_data(32)?;
|
||||||
|
|
||||||
|
let scrypt_config = SCryptConfig { n: 65536, r: 8, p: 1, salt };
|
||||||
|
|
||||||
|
let derived_key = CryptConfig::derive_key_from_password(password.as_bytes(), &scrypt_config)?;
|
||||||
|
|
||||||
|
let cipher = openssl::symm::Cipher::aes_256_gcm();
|
||||||
|
|
||||||
|
let iv = proxmox::sys::linux::random_data(16)?;
|
||||||
|
let mut tag = [0u8; 16];
|
||||||
|
|
||||||
|
let encrypted_key = openssl::symm::encrypt_aead(
|
||||||
|
cipher,
|
||||||
|
&derived_key,
|
||||||
|
Some(&iv),
|
||||||
|
b"",
|
||||||
|
&raw_key,
|
||||||
|
&mut tag,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let created = Local.timestamp(Local::now().timestamp(), 0);
|
||||||
|
|
||||||
|
let result = json!({
|
||||||
|
"kdf": "scrypt",
|
||||||
|
"created": created.to_rfc3339(),
|
||||||
|
"n": scrypt_config.n,
|
||||||
|
"r": scrypt_config.r,
|
||||||
|
"p": scrypt_config.p,
|
||||||
|
"salt": proxmox::tools::digest_to_hex(&scrypt_config.salt),
|
||||||
|
"iv": proxmox::tools::digest_to_hex(&iv),
|
||||||
|
"tag": proxmox::tools::digest_to_hex(&tag),
|
||||||
|
"data": proxmox::tools::digest_to_hex(&encrypted_key),
|
||||||
|
});
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let data = result.to_string();
|
||||||
|
|
||||||
|
try_block!({
|
||||||
|
let mut file = std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(&path)?;
|
||||||
|
|
||||||
|
file.write_all(data.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}).map_err(|err: Error| format_err!("Unable to create file {:?} - {}", path, err))?;
|
||||||
|
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_mgmt_cli() -> CliCommandMap {
|
||||||
|
|
||||||
|
// fixme: change-passphrase, import, export, list
|
||||||
|
let key_create_cmd_def = CliCommand::new(
|
||||||
|
ApiMethod::new(
|
||||||
|
key_create,
|
||||||
|
ObjectSchema::new("Create a new encryption key.")
|
||||||
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
|
))
|
||||||
|
.arg_param(vec!["repository"])
|
||||||
|
.completion_cb("repository", complete_repository);
|
||||||
|
|
||||||
|
let cmd_def = CliCommandMap::new()
|
||||||
|
.insert("create".to_owned(), key_create_cmd_def.into());
|
||||||
|
|
||||||
|
cmd_def
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
|
||||||
let backup_source_schema: Arc<Schema> = Arc::new(
|
let backup_source_schema: Arc<Schema> = Arc::new(
|
||||||
StringSchema::new("Backup source specification ([<label>:<path>]).")
|
StringSchema::new("Backup source specification ([<label>:<path>]).")
|
||||||
@ -793,7 +906,7 @@ fn main() {
|
|||||||
ApiMethod::new(
|
ApiMethod::new(
|
||||||
create_backup,
|
create_backup,
|
||||||
ObjectSchema::new("Create (host) backup.")
|
ObjectSchema::new("Create (host) backup.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
.required(
|
.required(
|
||||||
"backupspec",
|
"backupspec",
|
||||||
ArraySchema::new(
|
ArraySchema::new(
|
||||||
@ -824,7 +937,7 @@ fn main() {
|
|||||||
ApiMethod::new(
|
ApiMethod::new(
|
||||||
list_backup_groups,
|
list_backup_groups,
|
||||||
ObjectSchema::new("List backup groups.")
|
ObjectSchema::new("List backup groups.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
))
|
))
|
||||||
.arg_param(vec!["repository"])
|
.arg_param(vec!["repository"])
|
||||||
.completion_cb("repository", complete_repository);
|
.completion_cb("repository", complete_repository);
|
||||||
@ -833,7 +946,7 @@ fn main() {
|
|||||||
ApiMethod::new(
|
ApiMethod::new(
|
||||||
list_snapshots,
|
list_snapshots,
|
||||||
ObjectSchema::new("List backup snapshots.")
|
ObjectSchema::new("List backup snapshots.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
.required("group", StringSchema::new("Backup group."))
|
.required("group", StringSchema::new("Backup group."))
|
||||||
))
|
))
|
||||||
.arg_param(vec!["repository", "group"])
|
.arg_param(vec!["repository", "group"])
|
||||||
@ -844,7 +957,7 @@ fn main() {
|
|||||||
ApiMethod::new(
|
ApiMethod::new(
|
||||||
forget_snapshots,
|
forget_snapshots,
|
||||||
ObjectSchema::new("Forget (remove) backup snapshots.")
|
ObjectSchema::new("Forget (remove) backup snapshots.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
.required("snapshot", StringSchema::new("Snapshot path."))
|
.required("snapshot", StringSchema::new("Snapshot path."))
|
||||||
))
|
))
|
||||||
.arg_param(vec!["repository", "snapshot"])
|
.arg_param(vec!["repository", "snapshot"])
|
||||||
@ -855,7 +968,7 @@ fn main() {
|
|||||||
ApiMethod::new(
|
ApiMethod::new(
|
||||||
start_garbage_collection,
|
start_garbage_collection,
|
||||||
ObjectSchema::new("Start garbage collection for a specific repository.")
|
ObjectSchema::new("Start garbage collection for a specific repository.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
))
|
))
|
||||||
.arg_param(vec!["repository"])
|
.arg_param(vec!["repository"])
|
||||||
.completion_cb("repository", complete_repository);
|
.completion_cb("repository", complete_repository);
|
||||||
@ -864,7 +977,7 @@ fn main() {
|
|||||||
ApiMethod::new(
|
ApiMethod::new(
|
||||||
restore,
|
restore,
|
||||||
ObjectSchema::new("Restore backup repository.")
|
ObjectSchema::new("Restore backup repository.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
.required("snapshot", StringSchema::new("Group/Snapshot path."))
|
.required("snapshot", StringSchema::new("Group/Snapshot path."))
|
||||||
.required("archive-name", StringSchema::new("Backup archive name."))
|
.required("archive-name", StringSchema::new("Backup archive name."))
|
||||||
.required("target", StringSchema::new("Target directory path."))
|
.required("target", StringSchema::new("Target directory path."))
|
||||||
@ -880,7 +993,7 @@ fn main() {
|
|||||||
prune,
|
prune,
|
||||||
proxmox_backup::api2::admin::datastore::add_common_prune_prameters(
|
proxmox_backup::api2::admin::datastore::add_common_prune_prameters(
|
||||||
ObjectSchema::new("Prune backup repository.")
|
ObjectSchema::new("Prune backup repository.")
|
||||||
.required("repository", repo_url_schema.clone())
|
.required("repository", REPO_URL_SCHEMA.clone())
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
.arg_param(vec!["repository"])
|
.arg_param(vec!["repository"])
|
||||||
@ -893,7 +1006,8 @@ fn main() {
|
|||||||
.insert("list".to_owned(), list_cmd_def.into())
|
.insert("list".to_owned(), list_cmd_def.into())
|
||||||
.insert("prune".to_owned(), prune_cmd_def.into())
|
.insert("prune".to_owned(), prune_cmd_def.into())
|
||||||
.insert("restore".to_owned(), restore_cmd_def.into())
|
.insert("restore".to_owned(), restore_cmd_def.into())
|
||||||
.insert("snapshots".to_owned(), snapshots_cmd_def.into());
|
.insert("snapshots".to_owned(), snapshots_cmd_def.into())
|
||||||
|
.insert("key".to_owned(), key_mgmt_cli().into());
|
||||||
|
|
||||||
hyper::rt::run(futures::future::lazy(move || {
|
hyper::rt::run(futures::future::lazy(move || {
|
||||||
run_cli_command(cmd_def.into());
|
run_cli_command(cmd_def.into());
|
||||||
|
Loading…
Reference in New Issue
Block a user