1 use std
::convert
::TryFrom
;
2 use std
::path
::PathBuf
;
4 use anyhow
::{bail, format_err, Error}
;
8 use proxmox
::api
::cli
::{
9 format_and_print_result_full
, get_output_format
, CliCommand
, CliCommandMap
, ColumnConfig
,
12 use proxmox
::api
::router
::ReturnType
;
13 use proxmox
::sys
::linux
::tty
;
14 use proxmox
::tools
::fs
::{file_get_contents, replace_file, CreateOptions}
;
17 api2
::types
::{Kdf, KeyInfo, RsaPubKeyInfo, PASSWORD_HINT_SCHEMA}
,
18 backup
::{rsa_decrypt_key_config, KeyConfig}
,
20 tools
::paperkey
::{generate_paper_key, PaperkeyFormat}
,
23 use crate::KeyWithSource
;
25 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME
: &str = "encryption-key.json";
26 pub const DEFAULT_MASTER_PUBKEY_FILE_NAME
: &str = "master-public.pem";
28 pub fn find_default_master_pubkey() -> Result
<Option
<PathBuf
>, Error
> {
30 DEFAULT_MASTER_PUBKEY_FILE_NAME
,
31 "default master public key file",
35 pub fn place_default_master_pubkey() -> Result
<PathBuf
, Error
> {
36 super::place_xdg_file(
37 DEFAULT_MASTER_PUBKEY_FILE_NAME
,
38 "default master public key file",
42 pub fn find_default_encryption_key() -> Result
<Option
<PathBuf
>, Error
> {
44 DEFAULT_ENCRYPTION_KEY_FILE_NAME
,
45 "default encryption key file",
49 pub fn place_default_encryption_key() -> Result
<PathBuf
, Error
> {
50 super::place_xdg_file(
51 DEFAULT_ENCRYPTION_KEY_FILE_NAME
,
52 "default encryption key file",
57 pub(crate) fn read_optional_default_encryption_key() -> Result
<Option
<KeyWithSource
>, Error
> {
58 find_default_encryption_key()?
59 .map(|path
| file_get_contents(path
).map(KeyWithSource
::from_default
))
64 pub(crate) fn read_optional_default_master_pubkey() -> Result
<Option
<KeyWithSource
>, Error
> {
65 find_default_master_pubkey()?
66 .map(|path
| file_get_contents(path
).map(KeyWithSource
::from_default
))
71 static mut TEST_DEFAULT_ENCRYPTION_KEY
: Result
<Option
<Vec
<u8>>, Error
> = Ok(None
);
74 pub(crate) fn read_optional_default_encryption_key() -> Result
<Option
<KeyWithSource
>, Error
> {
75 // not safe when multiple concurrent test cases end up here!
77 match &TEST_DEFAULT_ENCRYPTION_KEY
{
78 Ok(Some(key
)) => Ok(Some(KeyWithSource
::from_default(key
.clone()))),
80 Err(_
) => bail
!("test error"),
86 // not safe when multiple concurrent test cases end up here!
87 pub(crate) unsafe fn set_test_encryption_key(value
: Result
<Option
<Vec
<u8>>, Error
>) {
88 TEST_DEFAULT_ENCRYPTION_KEY
= value
;
92 static mut TEST_DEFAULT_MASTER_PUBKEY
: Result
<Option
<Vec
<u8>>, Error
> = Ok(None
);
95 pub(crate) fn read_optional_default_master_pubkey() -> Result
<Option
<KeyWithSource
>, Error
> {
96 // not safe when multiple concurrent test cases end up here!
98 match &TEST_DEFAULT_MASTER_PUBKEY
{
99 Ok(Some(key
)) => Ok(Some(KeyWithSource
::from_default(key
.clone()))),
100 Ok(None
) => Ok(None
),
101 Err(_
) => bail
!("test error"),
107 // not safe when multiple concurrent test cases end up here!
108 pub(crate) unsafe fn set_test_default_master_pubkey(value
: Result
<Option
<Vec
<u8>>, Error
>) {
109 TEST_DEFAULT_MASTER_PUBKEY
= value
;
112 pub fn get_encryption_key_password() -> Result
<Vec
<u8>, Error
> {
113 // fixme: implement other input methods
115 use std
::env
::VarError
::*;
116 match std
::env
::var("PBS_ENCRYPTION_PASSWORD") {
117 Ok(p
) => return Ok(p
.as_bytes().to_vec()),
118 Err(NotUnicode(_
)) => bail
!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
120 // Try another method
124 // If we're on a TTY, query the user for a password
125 if tty
::stdin_isatty() {
126 return Ok(tty
::read_password("Encryption Key Password: ")?
);
129 bail
!("no password input mechanism available");
141 "Output file. Without this the key will become the new default encryption key.",
145 schema
: PASSWORD_HINT_SCHEMA
,
151 /// Create a new encryption key.
152 fn create(kdf
: Option
<Kdf
>, path
: Option
<String
>, hint
: Option
<String
>) -> Result
<(), Error
> {
153 let path
= match path
{
154 Some(path
) => PathBuf
::from(path
),
156 let path
= place_default_encryption_key()?
;
157 println
!("creating default key at: {:?}", path
);
162 let kdf
= kdf
.unwrap_or_default();
164 let mut key
= [0u8; 32];
165 proxmox
::sys
::linux
::fill_with_random_data(&mut key
)?
;
170 bail
!("password hint not allowed for Kdf::None");
173 let key_config
= KeyConfig
::without_password(key
)?
;
175 key_config
.store(path
, false)?
;
177 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
178 // always read passphrase from tty
179 if !tty
::stdin_isatty() {
180 bail
!("unable to read passphrase - no tty");
183 let password
= tty
::read_and_verify_password("Encryption Key Password: ")?
;
185 let mut key_config
= KeyConfig
::with_key(&key
, &password
, kdf
)?
;
186 key_config
.hint
= hint
;
188 key_config
.store(&path
, false)?
;
199 description
: "(Private) master key to use.",
201 "encrypted-keyfile": {
202 description
: "RSA-encrypted keyfile to import.",
210 "Output file. Without this the key will become the new default encryption key.",
214 schema
: PASSWORD_HINT_SCHEMA
,
220 /// Import an encrypted backup of an encryption key using a (private) master key.
221 async
fn import_with_master_key(
222 master_keyfile
: String
,
223 encrypted_keyfile
: String
,
225 path
: Option
<String
>,
226 hint
: Option
<String
>,
227 ) -> Result
<(), Error
> {
228 let path
= match path
{
229 Some(path
) => PathBuf
::from(path
),
231 let path
= place_default_encryption_key()?
;
233 bail
!("Please remove default encryption key at {:?} before importing to default location (or choose a non-default one).", path
);
235 println
!("Importing key to default location at: {:?}", path
);
240 let encrypted_key
= file_get_contents(&encrypted_keyfile
)?
;
241 let master_key
= file_get_contents(&master_keyfile
)?
;
242 let password
= tty
::read_password("Master Key Password: ")?
;
244 let master_key
= openssl
::pkey
::PKey
::private_key_from_pem_passphrase(&master_key
, &password
)
245 .map_err(|err
| format_err
!("failed to read PEM-formatted private key - {}", err
))?
247 .map_err(|err
| format_err
!("not a valid private RSA key - {}", err
))?
;
249 let (key
, created
, _fingerprint
) =
250 rsa_decrypt_key_config(master_key
, &encrypted_key
, &get_encryption_key_password
)?
;
252 let kdf
= kdf
.unwrap_or_default();
256 bail
!("password hint not allowed for Kdf::None");
259 let mut key_config
= KeyConfig
::without_password(key
)?
;
260 key_config
.created
= created
; // keep original value
262 key_config
.store(path
, true)?
;
264 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
265 let password
= tty
::read_and_verify_password("New Password: ")?
;
267 let mut new_key_config
= KeyConfig
::with_key(&key
, &password
, kdf
)?
;
268 new_key_config
.created
= created
; // keep original value
269 new_key_config
.hint
= hint
;
271 new_key_config
.store(path
, true)?
;
286 description
: "Key file. Without this the default key's password will be changed.",
290 schema
: PASSWORD_HINT_SCHEMA
,
296 /// Change the encryption key's password.
297 fn change_passphrase(
299 path
: Option
<String
>,
300 hint
: Option
<String
>,
301 ) -> Result
<(), Error
> {
302 let path
= match path
{
303 Some(path
) => PathBuf
::from(path
),
305 let path
= find_default_encryption_key()?
.ok_or_else(|| {
306 format_err
!("no encryption file provided and no default file found")
308 println
!("updating default key at: {:?}", path
);
313 let kdf
= kdf
.unwrap_or_default();
315 if !tty
::stdin_isatty() {
316 bail
!("unable to change passphrase - no tty");
319 let key_config
= KeyConfig
::load(&path
)?
;
320 let (key
, created
, _fingerprint
) = key_config
.decrypt(&get_encryption_key_password
)?
;
325 bail
!("password hint not allowed for Kdf::None");
328 let mut key_config
= KeyConfig
::without_password(key
)?
;
329 key_config
.created
= created
; // keep original value
331 key_config
.store(&path
, true)?
;
333 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
334 let password
= tty
::read_and_verify_password("New Password: ")?
;
336 let mut new_key_config
= KeyConfig
::with_key(&key
, &password
, kdf
)?
;
337 new_key_config
.created
= created
; // keep original value
338 new_key_config
.hint
= hint
;
340 new_key_config
.store(&path
, true)?
;
351 description
: "Key file. Without this the default key's metadata will be shown.",
355 schema
: OUTPUT_FORMAT
,
361 /// Print the encryption key's metadata.
362 fn show_key(path
: Option
<String
>, param
: Value
) -> Result
<(), Error
> {
363 let path
= match path
{
364 Some(path
) => PathBuf
::from(path
),
365 None
=> find_default_encryption_key()?
366 .ok_or_else(|| format_err
!("no encryption file provided and no default file found"))?
,
369 let config
: KeyConfig
= serde_json
::from_slice(&file_get_contents(path
.clone())?
)?
;
371 let output_format
= get_output_format(¶m
);
373 let mut info
: KeyInfo
= (&config
).into();
374 info
.path
= Some(format
!("{:?}", path
));
376 let options
= proxmox
::api
::cli
::default_table_format_options()
377 .column(ColumnConfig
::new("path"))
378 .column(ColumnConfig
::new("kdf"))
379 .column(ColumnConfig
::new("created").renderer(tools
::format
::render_epoch
))
380 .column(ColumnConfig
::new("modified").renderer(tools
::format
::render_epoch
))
381 .column(ColumnConfig
::new("fingerprint"))
382 .column(ColumnConfig
::new("hint"));
384 let return_type
= ReturnType
::new(false, &KeyInfo
::API_SCHEMA
);
386 format_and_print_result_full(
387 &mut serde_json
::to_value(info
)?
,
400 description
: "Path to the PEM formatted RSA public key.",
405 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
406 /// key onto the backup server along with each backup.
408 /// The imported key will be used as default master key for future invocations by the same local
410 fn import_master_pubkey(path
: String
) -> Result
<(), Error
> {
411 let pem_data
= file_get_contents(&path
)?
;
413 match openssl
::pkey
::PKey
::public_key_from_pem(&pem_data
) {
415 let info
= RsaPubKeyInfo
::try_from(key
.rsa()?
)?
;
416 println
!("Found following key at {:?}", path
);
417 println
!("Modulus: {}", info
.modulus
);
418 println
!("Exponent: {}", info
.exponent
);
419 println
!("Length: {}", info
.length
);
421 Err(err
) => bail
!("Unable to decode PEM data - {}", err
),
424 let target_path
= place_default_master_pubkey()?
;
426 replace_file(&target_path
, &pem_data
, CreateOptions
::new())?
;
428 println
!("Imported public master key to {:?}", target_path
);
434 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
435 /// encryption key onto the backup server along with each backup.
436 fn create_master_key() -> Result
<(), Error
> {
437 // we need a TTY to query the new password
438 if !tty
::stdin_isatty() {
439 bail
!("unable to create master key - no tty");
443 println
!("Generating {}-bit RSA key..", bits
);
444 let rsa
= openssl
::rsa
::Rsa
::generate(bits
)?
;
446 openssl
::rsa
::Rsa
::from_public_components(rsa
.n().to_owned()?
, rsa
.e().to_owned()?
)?
;
447 let info
= RsaPubKeyInfo
::try_from(public
)?
;
448 println
!("Modulus: {}", info
.modulus
);
449 println
!("Exponent: {}", info
.exponent
);
452 let pkey
= openssl
::pkey
::PKey
::from_rsa(rsa
)?
;
454 let password
= String
::from_utf8(tty
::read_and_verify_password("Master Key Password: ")?
)?
;
456 let pub_key
: Vec
<u8> = pkey
.public_key_to_pem()?
;
457 let filename_pub
= "master-public.pem";
458 println
!("Writing public master key to {}", filename_pub
);
459 replace_file(filename_pub
, pub_key
.as_slice(), CreateOptions
::new())?
;
461 let cipher
= openssl
::symm
::Cipher
::aes_256_cbc();
462 let priv_key
: Vec
<u8> =
463 pkey
.private_key_to_pem_pkcs8_passphrase(cipher
, password
.as_bytes())?
;
465 let filename_priv
= "master-private.pem";
466 println
!("Writing private master key to {}", filename_priv
);
467 replace_file(filename_priv
, priv_key
.as_slice(), CreateOptions
::new())?
;
476 description
: "Path to the PEM formatted RSA public key. Default location will be used if not specified.",
480 schema
: OUTPUT_FORMAT
,
486 /// List information about master key
487 fn show_master_pubkey(path
: Option
<String
>, param
: Value
) -> Result
<(), Error
> {
488 let path
= match path
{
489 Some(path
) => PathBuf
::from(path
),
490 None
=> find_default_master_pubkey()?
491 .ok_or_else(|| format_err
!("No path specified and no default master key available."))?
,
494 let path
= path
.canonicalize()?
;
496 let output_format
= get_output_format(¶m
);
498 let pem_data
= file_get_contents(path
.clone())?
;
499 let rsa
= openssl
::rsa
::Rsa
::public_key_from_pem(&pem_data
)?
;
501 let mut info
= RsaPubKeyInfo
::try_from(rsa
)?
;
502 info
.path
= Some(path
.display().to_string());
504 let options
= proxmox
::api
::cli
::default_table_format_options()
505 .column(ColumnConfig
::new("path"))
506 .column(ColumnConfig
::new("modulus"))
507 .column(ColumnConfig
::new("exponent"))
508 .column(ColumnConfig
::new("length"));
510 let return_type
= ReturnType
::new(false, &RsaPubKeyInfo
::API_SCHEMA
);
512 format_and_print_result_full(
513 &mut serde_json
::to_value(info
)?
,
526 description
: "Key file. Without this the default key's will be used.",
530 description
: "Include the specified subject as title text.",
534 type: PaperkeyFormat
,
540 /// Generate a printable, human readable text file containing the encryption key.
542 /// This also includes a scanable QR code for fast key restore.
544 path
: Option
<String
>,
545 subject
: Option
<String
>,
546 output_format
: Option
<PaperkeyFormat
>,
547 ) -> Result
<(), Error
> {
548 let path
= match path
{
549 Some(path
) => PathBuf
::from(path
),
550 None
=> find_default_encryption_key()?
551 .ok_or_else(|| format_err
!("no encryption file provided and no default file found"))?
,
554 let data
= file_get_contents(&path
)?
;
555 let data
= String
::from_utf8(data
)?
;
557 generate_paper_key(std
::io
::stdout(), &data
, subject
, output_format
)
560 pub fn cli() -> CliCommandMap
{
561 let key_create_cmd_def
= CliCommand
::new(&API_METHOD_CREATE
)
562 .arg_param(&["path"])
563 .completion_cb("path", tools
::complete_file_name
);
565 let key_import_with_master_key_cmd_def
= CliCommand
::new(&API_METHOD_IMPORT_WITH_MASTER_KEY
)
566 .arg_param(&["master-keyfile"])
567 .completion_cb("master-keyfile", tools
::complete_file_name
)
568 .arg_param(&["encrypted-keyfile"])
569 .completion_cb("encrypted-keyfile", tools
::complete_file_name
)
570 .arg_param(&["path"])
571 .completion_cb("path", tools
::complete_file_name
);
573 let key_change_passphrase_cmd_def
= CliCommand
::new(&API_METHOD_CHANGE_PASSPHRASE
)
574 .arg_param(&["path"])
575 .completion_cb("path", tools
::complete_file_name
);
577 let key_create_master_key_cmd_def
= CliCommand
::new(&API_METHOD_CREATE_MASTER_KEY
);
578 let key_import_master_pubkey_cmd_def
= CliCommand
::new(&API_METHOD_IMPORT_MASTER_PUBKEY
)
579 .arg_param(&["path"])
580 .completion_cb("path", tools
::complete_file_name
);
581 let key_show_master_pubkey_cmd_def
= CliCommand
::new(&API_METHOD_SHOW_MASTER_PUBKEY
)
582 .arg_param(&["path"])
583 .completion_cb("path", tools
::complete_file_name
);
585 let key_show_cmd_def
= CliCommand
::new(&API_METHOD_SHOW_KEY
)
586 .arg_param(&["path"])
587 .completion_cb("path", tools
::complete_file_name
);
589 let paper_key_cmd_def
= CliCommand
::new(&API_METHOD_PAPER_KEY
)
590 .arg_param(&["path"])
591 .completion_cb("path", tools
::complete_file_name
);
594 .insert("create", key_create_cmd_def
)
595 .insert("import-with-master-key", key_import_with_master_key_cmd_def
)
596 .insert("create-master-key", key_create_master_key_cmd_def
)
597 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def
)
598 .insert("change-passphrase", key_change_passphrase_cmd_def
)
599 .insert("show", key_show_cmd_def
)
600 .insert("show-master-pubkey", key_show_master_pubkey_cmd_def
)
601 .insert("paperkey", paper_key_cmd_def
)