1 use std
::path
::PathBuf
;
3 use std
::process
::{Stdio, Command}
;
5 use anyhow
::{bail, format_err, Error}
;
6 use serde
::{Deserialize, Serialize}
;
10 use proxmox
::api
::cli
::{
14 format_and_print_result_full
,
18 use proxmox
::api
::router
::ReturnType
;
19 use proxmox
::sys
::linux
::tty
;
20 use proxmox
::tools
::fs
::{file_get_contents, replace_file, CreateOptions}
;
27 rsa_decrypt_key_config
,
37 #[derive(Debug, Serialize, Deserialize)]
38 #[serde(rename_all = "lowercase")]
39 /// Paperkey output format
40 pub enum PaperkeyFormat
{
41 /// Format as Utf8 text. Includes QR codes as ascii-art.
43 /// Format as Html. Includes QR codes as png images.
47 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME
: &str = "encryption-key.json";
48 pub const MASTER_PUBKEY_FILE_NAME
: &str = "master-public.pem";
50 pub fn find_master_pubkey() -> Result
<Option
<PathBuf
>, Error
> {
51 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME
, "main public key file")
54 pub fn place_master_pubkey() -> Result
<PathBuf
, Error
> {
55 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME
, "main public key file")
58 pub fn find_default_encryption_key() -> Result
<Option
<PathBuf
>, Error
> {
59 super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME
, "default encryption key file")
62 pub fn place_default_encryption_key() -> Result
<PathBuf
, Error
> {
63 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME
, "default encryption key file")
66 pub fn read_optional_default_encryption_key() -> Result
<Option
<Vec
<u8>>, Error
> {
67 find_default_encryption_key()?
68 .map(file_get_contents
)
72 pub fn get_encryption_key_password() -> Result
<Vec
<u8>, Error
> {
73 // fixme: implement other input methods
75 use std
::env
::VarError
::*;
76 match std
::env
::var("PBS_ENCRYPTION_PASSWORD") {
77 Ok(p
) => return Ok(p
.as_bytes().to_vec()),
78 Err(NotUnicode(_
)) => bail
!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
84 // If we're on a TTY, query the user for a password
85 if tty
::stdin_isatty() {
86 return Ok(tty
::read_password("Encryption Key Password: ")?
);
89 bail
!("no password input mechanism available");
101 "Output file. Without this the key will become the new default encryption key.",
105 schema
: PASSWORD_HINT_SCHEMA
,
111 /// Create a new encryption key.
114 path
: Option
<String
>,
116 ) -> Result
<(), Error
> {
117 let path
= match path
{
118 Some(path
) => PathBuf
::from(path
),
120 let path
= place_default_encryption_key()?
;
121 println
!("creating default key at: {:?}", path
);
126 let kdf
= kdf
.unwrap_or_default();
128 let mut key
= [0u8; 32];
129 proxmox
::sys
::linux
::fill_with_random_data(&mut key
)?
;
130 let crypt_config
= CryptConfig
::new(key
.clone())?
;
135 bail
!("password hint not allowed for Kdf::None");
138 let mut key_config
= KeyConfig
::without_password(key
);
139 key_config
.fingerprint
= Some(crypt_config
.fingerprint());
141 key_config
.store(path
, false)?
;
143 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
144 // always read passphrase from tty
145 if !tty
::stdin_isatty() {
146 bail
!("unable to read passphrase - no tty");
149 let password
= tty
::read_and_verify_password("Encryption Key Password: ")?
;
151 let mut key_config
= KeyConfig
::with_key(&key
, &password
, kdf
)?
;
152 key_config
.fingerprint
= Some(crypt_config
.fingerprint());
153 key_config
.hint
= hint
;
155 key_config
.store(&path
, false)?
;
166 description
: "(Private) master key to use.",
168 "encrypted-keyfile": {
169 description
: "RSA-encrypted keyfile to import.",
177 "Output file. Without this the key will become the new default encryption key.",
181 schema
: PASSWORD_HINT_SCHEMA
,
187 /// Import an encrypted backup of an encryption key using a (private) master key.
188 async
fn import_with_master_key(
189 master_keyfile
: String
,
190 encrypted_keyfile
: String
,
192 path
: Option
<String
>,
193 hint
: Option
<String
>,
194 ) -> Result
<(), Error
> {
195 let path
= match path
{
196 Some(path
) => PathBuf
::from(path
),
198 let path
= place_default_encryption_key()?
;
200 bail
!("Please remove default encryption key at {:?} before importing to default location (or choose a non-default one).", path
);
202 println
!("Importing key to default location at: {:?}", path
);
207 let encrypted_key
= file_get_contents(&encrypted_keyfile
)?
;
208 let master_key
= file_get_contents(&master_keyfile
)?
;
209 let password
= tty
::read_password("Master Key Password: ")?
;
212 openssl
::pkey
::PKey
::private_key_from_pem_passphrase(&master_key
, &password
)
213 .map_err(|err
| format_err
!("failed to read PEM-formatted private key - {}", err
))?
215 .map_err(|err
| format_err
!("not a valid private RSA key - {}", err
))?
;
217 let (key
, created
, fingerprint
) =
218 rsa_decrypt_key_config(master_key
, &encrypted_key
, &get_encryption_key_password
)?
;
220 let kdf
= kdf
.unwrap_or_default();
224 bail
!("password hint not allowed for Kdf::None");
227 let mut key_config
= KeyConfig
::without_password(key
);
228 key_config
.created
= created
; // keep original value
229 key_config
.fingerprint
= Some(fingerprint
);
231 key_config
.store(path
, true)?
;
234 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
235 let password
= tty
::read_and_verify_password("New Password: ")?
;
237 let mut new_key_config
= KeyConfig
::with_key(&key
, &password
, kdf
)?
;
238 new_key_config
.created
= created
; // keep original value
239 new_key_config
.fingerprint
= Some(fingerprint
);
240 new_key_config
.hint
= hint
;
242 new_key_config
.store(path
, true)?
;
257 description
: "Key file. Without this the default key's password will be changed.",
261 schema
: PASSWORD_HINT_SCHEMA
,
267 /// Change the encryption key's password.
268 fn change_passphrase(
270 path
: Option
<String
>,
271 hint
: Option
<String
>,
272 ) -> Result
<(), Error
> {
273 let path
= match path
{
274 Some(path
) => PathBuf
::from(path
),
276 let path
= find_default_encryption_key()?
278 format_err
!("no encryption file provided and no default file found")
280 println
!("updating default key at: {:?}", path
);
285 let kdf
= kdf
.unwrap_or_default();
287 if !tty
::stdin_isatty() {
288 bail
!("unable to change passphrase - no tty");
291 let key_config
= KeyConfig
::load(&path
)?
;
292 let (key
, created
, fingerprint
) = key_config
.decrypt(&get_encryption_key_password
)?
;
297 bail
!("password hint not allowed for Kdf::None");
300 let mut key_config
= KeyConfig
::without_password(key
);
301 key_config
.created
= created
; // keep original value
302 key_config
.fingerprint
= Some(fingerprint
);
304 key_config
.store(&path
, true)?
;
306 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
307 let password
= tty
::read_and_verify_password("New Password: ")?
;
309 let mut new_key_config
= KeyConfig
::with_key(&key
, &password
, kdf
)?
;
310 new_key_config
.created
= created
; // keep original value
311 new_key_config
.fingerprint
= Some(fingerprint
);
312 new_key_config
.hint
= hint
;
314 new_key_config
.store(&path
, true)?
;
328 #[derive(Deserialize, Serialize)]
329 /// Encryption Key Information
334 /// Key creation time
336 /// Key modification time
339 #[serde(skip_serializing_if="Option::is_none")]
340 pub fingerprint
: Option
<String
>,
342 #[serde(skip_serializing_if="Option::is_none")]
343 pub hint
: Option
<String
>,
350 description
: "Key file. Without this the default key's metadata will be shown.",
354 schema
: OUTPUT_FORMAT
,
360 /// Print the encryption key's metadata.
362 path
: Option
<String
>,
364 ) -> Result
<(), Error
> {
365 let path
= match path
{
366 Some(path
) => PathBuf
::from(path
),
368 let path
= find_default_encryption_key()?
370 format_err
!("no encryption file provided and no default file found")
377 let config
: KeyConfig
= serde_json
::from_slice(&file_get_contents(path
.clone())?
)?
;
379 let output_format
= get_output_format(¶m
);
382 path
: format
!("{:?}", path
),
383 kdf
: match config
.kdf
{
384 Some(KeyDerivationConfig
::PBKDF2 { .. }
) => Kdf
::PBKDF2
,
385 Some(KeyDerivationConfig
::Scrypt { .. }
) => Kdf
::Scrypt
,
388 created
: config
.created
,
389 modified
: config
.modified
,
390 fingerprint
: match config
.fingerprint
{
391 Some(ref fp
) => Some(format
!("{}", fp
)),
397 let options
= proxmox
::api
::cli
::default_table_format_options()
398 .column(ColumnConfig
::new("path"))
399 .column(ColumnConfig
::new("kdf"))
400 .column(ColumnConfig
::new("created").renderer(tools
::format
::render_epoch
))
401 .column(ColumnConfig
::new("modified").renderer(tools
::format
::render_epoch
))
402 .column(ColumnConfig
::new("fingerprint"))
403 .column(ColumnConfig
::new("hint"));
405 let return_type
= ReturnType
::new(false, &KeyInfo
::API_SCHEMA
);
407 format_and_print_result_full(
408 &mut serde_json
::to_value(info
)?
,
421 description
: "Path to the PEM formatted RSA public key.",
426 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
427 /// key onto the backup server along with each backup.
428 fn import_master_pubkey(path
: String
) -> Result
<(), Error
> {
429 let pem_data
= file_get_contents(&path
)?
;
431 if let Err(err
) = openssl
::pkey
::PKey
::public_key_from_pem(&pem_data
) {
432 bail
!("Unable to decode PEM data - {}", err
);
435 let target_path
= place_master_pubkey()?
;
437 replace_file(&target_path
, &pem_data
, CreateOptions
::new())?
;
439 println
!("Imported public master key to {:?}", target_path
);
445 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
446 /// encryption key onto the backup server along with each backup.
447 fn create_master_key() -> Result
<(), Error
> {
448 // we need a TTY to query the new password
449 if !tty
::stdin_isatty() {
450 bail
!("unable to create master key - no tty");
453 let rsa
= openssl
::rsa
::Rsa
::generate(4096)?
;
454 let pkey
= openssl
::pkey
::PKey
::from_rsa(rsa
)?
;
456 let password
= String
::from_utf8(tty
::read_and_verify_password("Master Key Password: ")?
)?
;
458 let pub_key
: Vec
<u8> = pkey
.public_key_to_pem()?
;
459 let filename_pub
= "master-public.pem";
460 println
!("Writing public master key to {}", filename_pub
);
461 replace_file(filename_pub
, pub_key
.as_slice(), CreateOptions
::new())?
;
463 let cipher
= openssl
::symm
::Cipher
::aes_256_cbc();
464 let priv_key
: Vec
<u8> = pkey
.private_key_to_pem_pkcs8_passphrase(cipher
, password
.as_bytes())?
;
466 let filename_priv
= "master-private.pem";
467 println
!("Writing private master key to {}", filename_priv
);
468 replace_file(filename_priv
, priv_key
.as_slice(), CreateOptions
::new())?
;
477 description
: "Key file. Without this the default key's will be used.",
481 description
: "Include the specified subject as titel text.",
485 type: PaperkeyFormat
,
491 /// Generate a printable, human readable text file containing the encryption key.
493 /// This also includes a scanable QR code for fast key restore.
495 path
: Option
<String
>,
496 subject
: Option
<String
>,
497 output_format
: Option
<PaperkeyFormat
>,
498 ) -> Result
<(), Error
> {
499 let path
= match path
{
500 Some(path
) => PathBuf
::from(path
),
502 let path
= find_default_encryption_key()?
504 format_err
!("no encryption file provided and no default file found")
510 let data
= file_get_contents(&path
)?
;
511 let data
= String
::from_utf8(data
)?
;
513 let (data
, is_private_key
) = if data
.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
514 let lines
: Vec
<String
> = data
516 .map(|s
| s
.trim_end())
517 .filter(|s
| !s
.is_empty())
521 if !lines
[lines
.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
522 bail
!("unexpected key format");
525 if lines
.len() < 20 {
526 bail
!("unexpected key format");
531 match serde_json
::from_str
::<KeyConfig
>(&data
) {
533 let lines
= serde_json
::to_string_pretty(&key_config
)?
541 eprintln
!("Couldn't parse '{:?}' as KeyConfig - {}", path
, err
);
542 bail
!("Neither a PEM-formatted private key, nor a PBS key file.");
547 let format
= output_format
.unwrap_or(PaperkeyFormat
::Html
);
550 PaperkeyFormat
::Html
=> paperkey_html(&data
, subject
, is_private_key
),
551 PaperkeyFormat
::Text
=> paperkey_text(&data
, subject
, is_private_key
),
555 pub fn cli() -> CliCommandMap
{
556 let key_create_cmd_def
= CliCommand
::new(&API_METHOD_CREATE
)
557 .arg_param(&["path"])
558 .completion_cb("path", tools
::complete_file_name
);
560 let key_import_with_master_key_cmd_def
= CliCommand
::new(&API_METHOD_IMPORT_WITH_MASTER_KEY
)
561 .arg_param(&["master-keyfile"])
562 .completion_cb("master-keyfile", tools
::complete_file_name
)
563 .arg_param(&["encrypted-keyfile"])
564 .completion_cb("encrypted-keyfile", tools
::complete_file_name
)
565 .arg_param(&["path"])
566 .completion_cb("path", tools
::complete_file_name
);
568 let key_change_passphrase_cmd_def
= CliCommand
::new(&API_METHOD_CHANGE_PASSPHRASE
)
569 .arg_param(&["path"])
570 .completion_cb("path", tools
::complete_file_name
);
572 let key_create_master_key_cmd_def
= CliCommand
::new(&API_METHOD_CREATE_MASTER_KEY
);
573 let key_import_master_pubkey_cmd_def
= CliCommand
::new(&API_METHOD_IMPORT_MASTER_PUBKEY
)
574 .arg_param(&["path"])
575 .completion_cb("path", tools
::complete_file_name
);
577 let key_show_cmd_def
= CliCommand
::new(&API_METHOD_SHOW_KEY
)
578 .arg_param(&["path"])
579 .completion_cb("path", tools
::complete_file_name
);
581 let paper_key_cmd_def
= CliCommand
::new(&API_METHOD_PAPER_KEY
)
582 .arg_param(&["path"])
583 .completion_cb("path", tools
::complete_file_name
);
586 .insert("create", key_create_cmd_def
)
587 .insert("import-with-master-key", key_import_with_master_key_cmd_def
)
588 .insert("create-master-key", key_create_master_key_cmd_def
)
589 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def
)
590 .insert("change-passphrase", key_change_passphrase_cmd_def
)
591 .insert("show", key_show_cmd_def
)
592 .insert("paperkey", paper_key_cmd_def
)
595 fn paperkey_html(lines
: &[String
], subject
: Option
<String
>, is_private
: bool
) -> Result
<(), Error
> {
597 let img_size_pt
= 500;
599 println
!("<!DOCTYPE html>");
600 println
!("<html lang=\"en\">");
602 println
!("<meta charset=\"utf-8\">");
603 println
!("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
604 println
!("<title>Proxmox Backup Paperkey</title>");
605 println
!("<style type=\"text/css\">");
608 println
!(" font-size: 12pt;");
609 println
!(" font-family: monospace;");
610 println
!(" white-space: pre-wrap;");
611 println
!(" line-break: anywhere;");
614 println
!("</style>");
620 if let Some(subject
) = subject
{
621 println
!("<p>Subject: {}</p>", subject
);
625 const BLOCK_SIZE
: usize = 20;
626 let blocks
= (lines
.len() + BLOCK_SIZE
-1)/BLOCK_SIZE
;
629 let start
= i
*BLOCK_SIZE
;
630 let mut end
= start
+ BLOCK_SIZE
;
631 if end
> lines
.len() {
634 let data
= &lines
[start
..end
];
636 println
!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
639 for l
in start
..end
{
640 println
!("{:02}: {}", l
, lines
[l
]);
645 let qr_code
= generate_qr_code("svg", data
)?
;
646 let qr_code
= base64
::encode_config(&qr_code
, base64
::STANDARD_NO_PAD
);
648 println
!("<center>");
650 println
!("width=\"{}pt\" height=\"{}pt\"", img_size_pt
, img_size_pt
);
651 println
!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code
);
652 println
!("</center>");
661 println
!("<div style=\"page-break-inside: avoid\">");
665 println
!("-----BEGIN PROXMOX BACKUP KEY-----");
668 println
!("{}", line
);
671 println
!("-----END PROXMOX BACKUP KEY-----");
675 let qr_code
= generate_qr_code("svg", lines
)?
;
676 let qr_code
= base64
::encode_config(&qr_code
, base64
::STANDARD_NO_PAD
);
678 println
!("<center>");
680 println
!("width=\"{}pt\" height=\"{}pt\"", img_size_pt
, img_size_pt
);
681 println
!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code
);
682 println
!("</center>");
692 fn paperkey_text(lines
: &[String
], subject
: Option
<String
>, is_private
: bool
) -> Result
<(), Error
> {
694 if let Some(subject
) = subject
{
695 println
!("Subject: {}\n", subject
);
699 const BLOCK_SIZE
: usize = 5;
700 let blocks
= (lines
.len() + BLOCK_SIZE
-1)/BLOCK_SIZE
;
703 let start
= i
*BLOCK_SIZE
;
704 let mut end
= start
+ BLOCK_SIZE
;
705 if end
> lines
.len() {
708 let data
= &lines
[start
..end
];
710 for l
in start
..end
{
711 println
!("{:-2}: {}", l
, lines
[l
]);
713 let qr_code
= generate_qr_code("utf8i", data
)?
;
714 let qr_code
= String
::from_utf8(qr_code
)
715 .map_err(|_
| format_err
!("Failed to read qr code (got non-utf8 data)"))?
;
716 println
!("{}", qr_code
);
717 println
!("{}", char::from(12u8)); // page break
723 println
!("-----BEGIN PROXMOX BACKUP KEY-----");
725 println
!("{}", line
);
727 println
!("-----END PROXMOX BACKUP KEY-----");
729 let qr_code
= generate_qr_code("utf8i", &lines
)?
;
730 let qr_code
= String
::from_utf8(qr_code
)
731 .map_err(|_
| format_err
!("Failed to read qr code (got non-utf8 data)"))?
;
733 println
!("{}", qr_code
);
738 fn generate_qr_code(output_type
: &str, lines
: &[String
]) -> Result
<Vec
<u8>, Error
> {
739 let mut child
= Command
::new("qrencode")
740 .args(&["-t", output_type
, "-m0", "-s1", "-lm", "--output", "-"])
741 .stdin(Stdio
::piped())
742 .stdout(Stdio
::piped())
746 let stdin
= child
.stdin
.as_mut()
747 .ok_or_else(|| format_err
!("Failed to open stdin"))?
;
748 let data
= lines
.join("\n");
749 stdin
.write_all(data
.as_bytes())
750 .map_err(|_
| format_err
!("Failed to write to stdin"))?
;
753 let output
= child
.wait_with_output()
754 .map_err(|_
| format_err
!("Failed to read stdout"))?
;
756 let output
= crate::tools
::command_output(output
, None
)?
;