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
::sys
::linux
::tty
;
19 use proxmox
::tools
::fs
::{file_get_contents, replace_file, CreateOptions}
;
21 use proxmox_backup
::backup
::{
22 encrypt_key_with_passphrase
,
30 use proxmox_backup
::tools
;
33 #[derive(Debug, Serialize, Deserialize)]
34 #[serde(rename_all = "lowercase")]
35 /// Paperkey output format
36 pub enum PaperkeyFormat
{
37 /// Format as Utf8 text. Includes QR codes as ascii-art.
39 /// Format as Html. Includes QR codes as png images.
43 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME
: &str = "encryption-key.json";
44 pub const MASTER_PUBKEY_FILE_NAME
: &str = "master-public.pem";
46 pub fn find_master_pubkey() -> Result
<Option
<PathBuf
>, Error
> {
47 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME
, "main public key file")
50 pub fn place_master_pubkey() -> Result
<PathBuf
, Error
> {
51 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME
, "main public key file")
54 pub fn find_default_encryption_key() -> Result
<Option
<PathBuf
>, Error
> {
55 super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME
, "default encryption key file")
58 pub fn place_default_encryption_key() -> Result
<PathBuf
, Error
> {
59 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME
, "default encryption key file")
62 pub fn read_optional_default_encryption_key() -> Result
<Option
<Vec
<u8>>, Error
> {
63 find_default_encryption_key()?
64 .map(file_get_contents
)
68 pub fn get_encryption_key_password() -> Result
<Vec
<u8>, Error
> {
69 // fixme: implement other input methods
71 use std
::env
::VarError
::*;
72 match std
::env
::var("PBS_ENCRYPTION_PASSWORD") {
73 Ok(p
) => return Ok(p
.as_bytes().to_vec()),
74 Err(NotUnicode(_
)) => bail
!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
80 // If we're on a TTY, query the user for a password
81 if tty
::stdin_isatty() {
82 return Ok(tty
::read_password("Encryption Key Password: ")?
);
85 bail
!("no password input mechanism available");
97 "Output file. Without this the key will become the new default encryption key.",
103 /// Create a new encryption key.
104 fn create(kdf
: Option
<Kdf
>, path
: Option
<String
>) -> Result
<(), Error
> {
105 let path
= match path
{
106 Some(path
) => PathBuf
::from(path
),
108 let path
= place_default_encryption_key()?
;
109 println
!("creating default key at: {:?}", path
);
114 let kdf
= kdf
.unwrap_or_default();
116 let mut key_array
= [0u8; 32];
117 proxmox
::sys
::linux
::fill_with_random_data(&mut key_array
)?
;
118 let crypt_config
= CryptConfig
::new(key_array
.clone())?
;
119 let key
= key_array
.to_vec();
123 let created
= proxmox
::tools
::time
::epoch_i64();
133 fingerprint
: Some(crypt_config
.fingerprint()),
137 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
138 // always read passphrase from tty
139 if !tty
::stdin_isatty() {
140 bail
!("unable to read passphrase - no tty");
143 let password
= tty
::read_and_verify_password("Encryption Key Password: ")?
;
145 let mut key_config
= encrypt_key_with_passphrase(&key
, &password
, kdf
)?
;
146 key_config
.fingerprint
= Some(crypt_config
.fingerprint());
148 store_key_config(&path
, false, key_config
)?
;
163 description
: "Key file. Without this the default key's password will be changed.",
169 /// Change the encryption key's password.
170 fn change_passphrase(kdf
: Option
<Kdf
>, path
: Option
<String
>) -> Result
<(), Error
> {
171 let path
= match path
{
172 Some(path
) => PathBuf
::from(path
),
174 let path
= find_default_encryption_key()?
176 format_err
!("no encryption file provided and no default file found")
178 println
!("updating default key at: {:?}", path
);
183 let kdf
= kdf
.unwrap_or_default();
185 if !tty
::stdin_isatty() {
186 bail
!("unable to change passphrase - no tty");
189 let (key
, created
, fingerprint
) = load_and_decrypt_key(&path
, &get_encryption_key_password
)?
;
193 let modified
= proxmox
::tools
::time
::epoch_i64();
200 created
, // keep original value
203 fingerprint
: Some(fingerprint
),
207 Kdf
::Scrypt
| Kdf
::PBKDF2
=> {
208 let password
= tty
::read_and_verify_password("New Password: ")?
;
210 let mut new_key_config
= encrypt_key_with_passphrase(&key
, &password
, kdf
)?
;
211 new_key_config
.created
= created
; // keep original value
212 new_key_config
.fingerprint
= Some(fingerprint
);
214 store_key_config(&path
, true, new_key_config
)?
;
228 #[derive(Deserialize, Serialize)]
229 /// Encryption Key Information
234 /// Key creation time
236 /// Key modification time
239 pub fingerprint
: Option
<String
>,
246 description
: "Key file. Without this the default key's metadata will be shown.",
250 schema
: OUTPUT_FORMAT
,
256 /// Print the encryption key's metadata.
258 path
: Option
<String
>,
260 ) -> Result
<(), Error
> {
261 let path
= match path
{
262 Some(path
) => PathBuf
::from(path
),
264 let path
= find_default_encryption_key()?
266 format_err
!("no encryption file provided and no default file found")
273 let config
: KeyConfig
= serde_json
::from_slice(&file_get_contents(path
.clone())?
)?
;
275 let output_format
= get_output_format(¶m
);
278 path
: format
!("{:?}", path
),
279 kdf
: match config
.kdf
{
280 Some(KeyDerivationConfig
::PBKDF2 { .. }
) => Kdf
::PBKDF2
,
281 Some(KeyDerivationConfig
::Scrypt { .. }
) => Kdf
::Scrypt
,
284 created
: config
.created
,
285 modified
: config
.modified
,
286 fingerprint
: match config
.fingerprint
{
287 Some(ref fp
) => Some(format
!("{}", fp
)),
292 let options
= proxmox
::api
::cli
::default_table_format_options()
293 .column(ColumnConfig
::new("path"))
294 .column(ColumnConfig
::new("kdf"))
295 .column(ColumnConfig
::new("created").renderer(tools
::format
::render_epoch
))
296 .column(ColumnConfig
::new("modified").renderer(tools
::format
::render_epoch
))
297 .column(ColumnConfig
::new("fingerprint"));
299 let schema
= &KeyInfo
::API_SCHEMA
;
301 format_and_print_result_full(&mut serde_json
::to_value(info
)?
, schema
, &output_format
, &options
);
310 description
: "Path to the PEM formatted RSA public key.",
315 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
316 /// key onto the backup server along with each backup.
317 fn import_master_pubkey(path
: String
) -> Result
<(), Error
> {
318 let pem_data
= file_get_contents(&path
)?
;
320 if let Err(err
) = openssl
::pkey
::PKey
::public_key_from_pem(&pem_data
) {
321 bail
!("Unable to decode PEM data - {}", err
);
324 let target_path
= place_master_pubkey()?
;
326 replace_file(&target_path
, &pem_data
, CreateOptions
::new())?
;
328 println
!("Imported public master key to {:?}", target_path
);
334 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
335 /// encryption key onto the backup server along with each backup.
336 fn create_master_key() -> Result
<(), Error
> {
337 // we need a TTY to query the new password
338 if !tty
::stdin_isatty() {
339 bail
!("unable to create master key - no tty");
342 let rsa
= openssl
::rsa
::Rsa
::generate(4096)?
;
343 let pkey
= openssl
::pkey
::PKey
::from_rsa(rsa
)?
;
345 let password
= String
::from_utf8(tty
::read_and_verify_password("Master Key Password: ")?
)?
;
347 let pub_key
: Vec
<u8> = pkey
.public_key_to_pem()?
;
348 let filename_pub
= "master-public.pem";
349 println
!("Writing public master key to {}", filename_pub
);
350 replace_file(filename_pub
, pub_key
.as_slice(), CreateOptions
::new())?
;
352 let cipher
= openssl
::symm
::Cipher
::aes_256_cbc();
353 let priv_key
: Vec
<u8> = pkey
.private_key_to_pem_pkcs8_passphrase(cipher
, password
.as_bytes())?
;
355 let filename_priv
= "master-private.pem";
356 println
!("Writing private master key to {}", filename_priv
);
357 replace_file(filename_priv
, priv_key
.as_slice(), CreateOptions
::new())?
;
366 description
: "Key file. Without this the default key's will be used.",
370 description
: "Include the specified subject as titel text.",
374 type: PaperkeyFormat
,
380 /// Generate a printable, human readable text file containing the encryption key.
382 /// This also includes a scanable QR code for fast key restore.
384 path
: Option
<String
>,
385 subject
: Option
<String
>,
386 output_format
: Option
<PaperkeyFormat
>,
387 ) -> Result
<(), Error
> {
388 let path
= match path
{
389 Some(path
) => PathBuf
::from(path
),
391 let path
= find_default_encryption_key()?
393 format_err
!("no encryption file provided and no default file found")
399 let data
= file_get_contents(&path
)?
;
400 let data
= String
::from_utf8(data
)?
;
402 let (data
, is_private_key
) = if data
.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
403 let lines
: Vec
<String
> = data
405 .map(|s
| s
.trim_end())
406 .filter(|s
| !s
.is_empty())
410 if !lines
[lines
.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
411 bail
!("unexpected key format");
414 if lines
.len() < 20 {
415 bail
!("unexpected key format");
420 match serde_json
::from_str
::<KeyConfig
>(&data
) {
422 let lines
= serde_json
::to_string_pretty(&key_config
)?
430 eprintln
!("Couldn't parse '{:?}' as KeyConfig - {}", path
, err
);
431 bail
!("Neither a PEM-formatted private key, nor a PBS key file.");
436 let format
= output_format
.unwrap_or(PaperkeyFormat
::Html
);
439 PaperkeyFormat
::Html
=> paperkey_html(&data
, subject
, is_private_key
),
440 PaperkeyFormat
::Text
=> paperkey_text(&data
, subject
, is_private_key
),
444 pub fn cli() -> CliCommandMap
{
445 let key_create_cmd_def
= CliCommand
::new(&API_METHOD_CREATE
)
446 .arg_param(&["path"])
447 .completion_cb("path", tools
::complete_file_name
);
449 let key_change_passphrase_cmd_def
= CliCommand
::new(&API_METHOD_CHANGE_PASSPHRASE
)
450 .arg_param(&["path"])
451 .completion_cb("path", tools
::complete_file_name
);
453 let key_create_master_key_cmd_def
= CliCommand
::new(&API_METHOD_CREATE_MASTER_KEY
);
454 let key_import_master_pubkey_cmd_def
= CliCommand
::new(&API_METHOD_IMPORT_MASTER_PUBKEY
)
455 .arg_param(&["path"])
456 .completion_cb("path", tools
::complete_file_name
);
458 let key_show_cmd_def
= CliCommand
::new(&API_METHOD_SHOW_KEY
)
459 .arg_param(&["path"])
460 .completion_cb("path", tools
::complete_file_name
);
462 let paper_key_cmd_def
= CliCommand
::new(&API_METHOD_PAPER_KEY
)
463 .arg_param(&["path"])
464 .completion_cb("path", tools
::complete_file_name
);
467 .insert("create", key_create_cmd_def
)
468 .insert("create-master-key", key_create_master_key_cmd_def
)
469 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def
)
470 .insert("change-passphrase", key_change_passphrase_cmd_def
)
471 .insert("show", key_show_cmd_def
)
472 .insert("paperkey", paper_key_cmd_def
)
475 fn paperkey_html(lines
: &[String
], subject
: Option
<String
>, is_private
: bool
) -> Result
<(), Error
> {
477 let img_size_pt
= 500;
479 println
!("<!DOCTYPE html>");
480 println
!("<html lang=\"en\">");
482 println
!("<meta charset=\"utf-8\">");
483 println
!("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
484 println
!("<title>Proxmox Backup Paperkey</title>");
485 println
!("<style type=\"text/css\">");
488 println
!(" font-size: 12pt;");
489 println
!(" font-family: monospace;");
490 println
!(" white-space: pre-wrap;");
491 println
!(" line-break: anywhere;");
494 println
!("</style>");
500 if let Some(subject
) = subject
{
501 println
!("<p>Subject: {}</p>", subject
);
505 const BLOCK_SIZE
: usize = 20;
506 let blocks
= (lines
.len() + BLOCK_SIZE
-1)/BLOCK_SIZE
;
509 let start
= i
*BLOCK_SIZE
;
510 let mut end
= start
+ BLOCK_SIZE
;
511 if end
> lines
.len() {
514 let data
= &lines
[start
..end
];
516 println
!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
519 for l
in start
..end
{
520 println
!("{:02}: {}", l
, lines
[l
]);
525 let qr_code
= generate_qr_code("svg", data
)?
;
526 let qr_code
= base64
::encode_config(&qr_code
, base64
::STANDARD_NO_PAD
);
528 println
!("<center>");
530 println
!("width=\"{}pt\" height=\"{}pt\"", img_size_pt
, img_size_pt
);
531 println
!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code
);
532 println
!("</center>");
541 println
!("<div style=\"page-break-inside: avoid\">");
545 println
!("-----BEGIN PROXMOX BACKUP KEY-----");
548 println
!("{}", line
);
551 println
!("-----END PROXMOX BACKUP KEY-----");
555 let qr_code
= generate_qr_code("svg", lines
)?
;
556 let qr_code
= base64
::encode_config(&qr_code
, base64
::STANDARD_NO_PAD
);
558 println
!("<center>");
560 println
!("width=\"{}pt\" height=\"{}pt\"", img_size_pt
, img_size_pt
);
561 println
!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code
);
562 println
!("</center>");
572 fn paperkey_text(lines
: &[String
], subject
: Option
<String
>, is_private
: bool
) -> Result
<(), Error
> {
574 if let Some(subject
) = subject
{
575 println
!("Subject: {}\n", subject
);
579 const BLOCK_SIZE
: usize = 5;
580 let blocks
= (lines
.len() + BLOCK_SIZE
-1)/BLOCK_SIZE
;
583 let start
= i
*BLOCK_SIZE
;
584 let mut end
= start
+ BLOCK_SIZE
;
585 if end
> lines
.len() {
588 let data
= &lines
[start
..end
];
590 for l
in start
..end
{
591 println
!("{:-2}: {}", l
, lines
[l
]);
593 let qr_code
= generate_qr_code("utf8i", data
)?
;
594 let qr_code
= String
::from_utf8(qr_code
)
595 .map_err(|_
| format_err
!("Failed to read qr code (got non-utf8 data)"))?
;
596 println
!("{}", qr_code
);
597 println
!("{}", char::from(12u8)); // page break
603 println
!("-----BEGIN PROXMOX BACKUP KEY-----");
605 println
!("{}", line
);
607 println
!("-----END PROXMOX BACKUP KEY-----");
609 let qr_code
= generate_qr_code("utf8i", &lines
)?
;
610 let qr_code
= String
::from_utf8(qr_code
)
611 .map_err(|_
| format_err
!("Failed to read qr code (got non-utf8 data)"))?
;
613 println
!("{}", qr_code
);
618 fn generate_qr_code(output_type
: &str, lines
: &[String
]) -> Result
<Vec
<u8>, Error
> {
619 let mut child
= Command
::new("qrencode")
620 .args(&["-t", output_type
, "-m0", "-s1", "-lm", "--output", "-"])
621 .stdin(Stdio
::piped())
622 .stdout(Stdio
::piped())
626 let stdin
= child
.stdin
.as_mut()
627 .ok_or_else(|| format_err
!("Failed to open stdin"))?
;
628 let data
= lines
.join("\n");
629 stdin
.write_all(data
.as_bytes())
630 .map_err(|_
| format_err
!("Failed to write to stdin"))?
;
633 let output
= child
.wait_with_output()
634 .map_err(|_
| format_err
!("Failed to read stdout"))?
;
636 let output
= crate::tools
::command_output(output
, None
)?
;