1 use std
::path
::PathBuf
;
3 use std
::process
::{Stdio, Command}
;
5 use anyhow
::{bail, format_err, Error}
;
6 use serde
::{Deserialize, Serialize}
;
9 use proxmox
::api
::cli
::{CliCommand, CliCommandMap}
;
10 use proxmox
::sys
::linux
::tty
;
11 use proxmox
::tools
::fs
::{file_get_contents, replace_file, CreateOptions}
;
13 use proxmox_backup
::backup
::{
14 encrypt_key_with_passphrase
,
20 use proxmox_backup
::tools
;
23 #[derive(Debug, Serialize, Deserialize)]
24 #[serde(rename_all = "lowercase")]
25 /// Paperkey output format
26 pub enum PaperkeyFormat
{
27 /// Format as Utf8 text. Includes QR codes as ascii-art.
29 /// Format as Html. Includes QR codes as png images.
33 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME
: &str = "encryption-key.json";
34 pub const MASTER_PUBKEY_FILE_NAME
: &str = "master-public.pem";
36 pub fn find_master_pubkey() -> Result
<Option
<PathBuf
>, Error
> {
37 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME
, "main public key file")
40 pub fn place_master_pubkey() -> Result
<PathBuf
, Error
> {
41 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME
, "main public key file")
44 pub fn find_default_encryption_key() -> Result
<Option
<PathBuf
>, Error
> {
45 super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME
, "default encryption key file")
48 pub fn place_default_encryption_key() -> Result
<PathBuf
, Error
> {
49 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME
, "default encryption key file")
52 pub fn read_optional_default_encryption_key() -> Result
<Option
<Vec
<u8>>, Error
> {
53 find_default_encryption_key()?
54 .map(file_get_contents
)
58 pub fn get_encryption_key_password() -> Result
<Vec
<u8>, Error
> {
59 // fixme: implement other input methods
61 use std
::env
::VarError
::*;
62 match std
::env
::var("PBS_ENCRYPTION_PASSWORD") {
63 Ok(p
) => return Ok(p
.as_bytes().to_vec()),
64 Err(NotUnicode(_
)) => bail
!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
70 // If we're on a TTY, query the user for a password
71 if tty
::stdin_isatty() {
72 return Ok(tty
::read_password("Encryption Key Password: ")?
);
75 bail
!("no password input mechanism available");
81 #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
82 #[serde(rename_all = "kebab-case")]
83 /// Key derivation function for password protected encryption keys.
85 /// Do not encrypt the key.
88 /// Encrypt they key with a password using SCrypt.
92 impl Default
for Kdf
{
94 fn default() -> Self {
108 "Output file. Without this the key will become the new default encryption key.",
114 /// Create a new encryption key.
115 fn create(kdf
: Option
<Kdf
>, path
: Option
<String
>) -> Result
<(), Error
> {
116 let path
= match path
{
117 Some(path
) => PathBuf
::from(path
),
119 let path
= place_default_encryption_key()?
;
120 println
!("creating default key at: {:?}", path
);
125 let kdf
= kdf
.unwrap_or_default();
127 let mut key_array
= [0u8; 32];
128 proxmox
::sys
::linux
::fill_with_random_data(&mut key_array
)?
;
129 let crypt_config
= CryptConfig
::new(key_array
.clone())?
;
130 let key
= key_array
.to_vec();
134 let created
= proxmox
::tools
::time
::epoch_i64();
144 fingerprint
: Some(crypt_config
.fingerprint()),
149 // always read passphrase from tty
150 if !tty
::stdin_isatty() {
151 bail
!("unable to read passphrase - no tty");
154 let password
= tty
::read_and_verify_password("Encryption Key Password: ")?
;
156 let mut key_config
= encrypt_key_with_passphrase(&key
, &password
)?
;
157 key_config
.fingerprint
= Some(crypt_config
.fingerprint());
159 store_key_config(&path
, false, key_config
)?
;
174 description
: "Key file. Without this the default key's password will be changed.",
180 /// Change the encryption key's password.
181 fn change_passphrase(kdf
: Option
<Kdf
>, path
: Option
<String
>) -> Result
<(), Error
> {
182 let path
= match path
{
183 Some(path
) => PathBuf
::from(path
),
185 let path
= find_default_encryption_key()?
187 format_err
!("no encryption file provided and no default file found")
189 println
!("updating default key at: {:?}", path
);
194 let kdf
= kdf
.unwrap_or_default();
196 if !tty
::stdin_isatty() {
197 bail
!("unable to change passphrase - no tty");
200 let (key
, created
, fingerprint
) = load_and_decrypt_key(&path
, &get_encryption_key_password
)?
;
204 let modified
= proxmox
::tools
::time
::epoch_i64();
211 created
, // keep original value
214 fingerprint
: Some(fingerprint
),
219 let password
= tty
::read_and_verify_password("New Password: ")?
;
221 let mut new_key_config
= encrypt_key_with_passphrase(&key
, &password
)?
;
222 new_key_config
.created
= created
; // keep original value
223 new_key_config
.fingerprint
= Some(fingerprint
);
225 store_key_config(&path
, true, new_key_config
)?
;
236 description
: "Path to the PEM formatted RSA public key.",
241 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
242 /// key onto the backup server along with each backup.
243 fn import_master_pubkey(path
: String
) -> Result
<(), Error
> {
244 let pem_data
= file_get_contents(&path
)?
;
246 if let Err(err
) = openssl
::pkey
::PKey
::public_key_from_pem(&pem_data
) {
247 bail
!("Unable to decode PEM data - {}", err
);
250 let target_path
= place_master_pubkey()?
;
252 replace_file(&target_path
, &pem_data
, CreateOptions
::new())?
;
254 println
!("Imported public master key to {:?}", target_path
);
260 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
261 /// encryption key onto the backup server along with each backup.
262 fn create_master_key() -> Result
<(), Error
> {
263 // we need a TTY to query the new password
264 if !tty
::stdin_isatty() {
265 bail
!("unable to create master key - no tty");
268 let rsa
= openssl
::rsa
::Rsa
::generate(4096)?
;
269 let pkey
= openssl
::pkey
::PKey
::from_rsa(rsa
)?
;
271 let password
= String
::from_utf8(tty
::read_and_verify_password("Master Key Password: ")?
)?
;
273 let pub_key
: Vec
<u8> = pkey
.public_key_to_pem()?
;
274 let filename_pub
= "master-public.pem";
275 println
!("Writing public master key to {}", filename_pub
);
276 replace_file(filename_pub
, pub_key
.as_slice(), CreateOptions
::new())?
;
278 let cipher
= openssl
::symm
::Cipher
::aes_256_cbc();
279 let priv_key
: Vec
<u8> = pkey
.private_key_to_pem_pkcs8_passphrase(cipher
, password
.as_bytes())?
;
281 let filename_priv
= "master-private.pem";
282 println
!("Writing private master key to {}", filename_priv
);
283 replace_file(filename_priv
, priv_key
.as_slice(), CreateOptions
::new())?
;
292 description
: "Key file. Without this the default key's will be used.",
296 description
: "Include the specified subject as titel text.",
300 type: PaperkeyFormat
,
301 description
: "Output format. Text or Html.",
307 /// Generate a printable, human readable text file containing the encryption key.
309 /// This also includes a scanable QR code for fast key restore.
311 path
: Option
<String
>,
312 subject
: Option
<String
>,
313 output_format
: Option
<PaperkeyFormat
>,
314 ) -> Result
<(), Error
> {
315 let path
= match path
{
316 Some(path
) => PathBuf
::from(path
),
318 let path
= find_default_encryption_key()?
320 format_err
!("no encryption file provided and no default file found")
326 let data
= file_get_contents(&path
)?
;
327 let data
= std
::str::from_utf8(&data
)?
;
329 let format
= output_format
.unwrap_or(PaperkeyFormat
::Html
);
332 PaperkeyFormat
::Html
=> paperkey_html(data
, subject
),
333 PaperkeyFormat
::Text
=> paperkey_text(data
, subject
),
337 pub fn cli() -> CliCommandMap
{
338 let key_create_cmd_def
= CliCommand
::new(&API_METHOD_CREATE
)
339 .arg_param(&["path"])
340 .completion_cb("path", tools
::complete_file_name
);
342 let key_change_passphrase_cmd_def
= CliCommand
::new(&API_METHOD_CHANGE_PASSPHRASE
)
343 .arg_param(&["path"])
344 .completion_cb("path", tools
::complete_file_name
);
346 let key_create_master_key_cmd_def
= CliCommand
::new(&API_METHOD_CREATE_MASTER_KEY
);
347 let key_import_master_pubkey_cmd_def
= CliCommand
::new(&API_METHOD_IMPORT_MASTER_PUBKEY
)
348 .arg_param(&["path"])
349 .completion_cb("path", tools
::complete_file_name
);
351 let paper_key_cmd_def
= CliCommand
::new(&API_METHOD_PAPER_KEY
)
352 .arg_param(&["path"])
353 .completion_cb("path", tools
::complete_file_name
);
356 .insert("create", key_create_cmd_def
)
357 .insert("create-master-key", key_create_master_key_cmd_def
)
358 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def
)
359 .insert("change-passphrase", key_change_passphrase_cmd_def
)
360 .insert("paperkey", paper_key_cmd_def
)
363 fn paperkey_html(data
: &str, subject
: Option
<String
>) -> Result
<(), Error
> {
365 let img_size_pt
= 500;
367 println
!("<!DOCTYPE html>");
368 println
!("<html lang=\"en\">");
370 println
!("<meta charset=\"utf-8\">");
371 println
!("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
372 println
!("<title>Proxmox Backup Paperkey</title>");
373 println
!("<style type=\"text/css\">");
376 println
!(" font-size: 12pt;");
377 println
!(" font-family: monospace;");
378 println
!(" white-space: pre-wrap;");
379 println
!(" line-break: anywhere;");
382 println
!("</style>");
388 if let Some(subject
) = subject
{
389 println
!("<p>Subject: {}</p>", subject
);
392 if data
.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
393 let lines
: Vec
<String
> = data
.lines()
394 .map(|s
| s
.trim_end())
395 .filter(|s
| !s
.is_empty())
399 if !lines
[lines
.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
400 bail
!("unexpected key format");
403 if lines
.len() < 20 {
404 bail
!("unexpected key format");
407 const BLOCK_SIZE
: usize = 20;
408 let blocks
= (lines
.len() + BLOCK_SIZE
-1)/BLOCK_SIZE
;
411 let start
= i
*BLOCK_SIZE
;
412 let mut end
= start
+ BLOCK_SIZE
;
413 if end
> lines
.len() {
416 let data
= &lines
[start
..end
];
418 println
!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
421 for l
in start
..end
{
422 println
!("{:02}: {}", l
, lines
[l
]);
427 let data
= data
.join("\n");
428 let qr_code
= generate_qr_code("svg", data
.as_bytes())?
;
429 let qr_code
= base64
::encode_config(&qr_code
, base64
::STANDARD_NO_PAD
);
431 println
!("<center>");
433 println
!("width=\"{}pt\" height=\"{}pt\"", img_size_pt
, img_size_pt
);
434 println
!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code
);
435 println
!("</center>");
444 let key_config
: KeyConfig
= serde_json
::from_str(&data
)?
;
445 let key_text
= serde_json
::to_string_pretty(&key_config
)?
;
447 println
!("<div style=\"page-break-inside: avoid\">");
451 println
!("-----BEGIN PROXMOX BACKUP KEY-----");
453 for line
in key_text
.lines() {
454 println
!("{}", line
);
457 println
!("-----END PROXMOX BACKUP KEY-----");
461 let qr_code
= generate_qr_code("svg", key_text
.as_bytes())?
;
462 let qr_code
= base64
::encode_config(&qr_code
, base64
::STANDARD_NO_PAD
);
464 println
!("<center>");
466 println
!("width=\"{}pt\" height=\"{}pt\"", img_size_pt
, img_size_pt
);
467 println
!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code
);
468 println
!("</center>");
478 fn paperkey_text(data
: &str, subject
: Option
<String
>) -> Result
<(), Error
> {
480 if let Some(subject
) = subject
{
481 println
!("Subject: {}\n", subject
);
484 if data
.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
485 let lines
: Vec
<String
> = data
.lines()
486 .map(|s
| s
.trim_end())
487 .filter(|s
| !s
.is_empty())
491 if !lines
[lines
.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
492 bail
!("unexpected key format");
495 if lines
.len() < 20 {
496 bail
!("unexpected key format");
499 const BLOCK_SIZE
: usize = 5;
500 let blocks
= (lines
.len() + BLOCK_SIZE
-1)/BLOCK_SIZE
;
503 let start
= i
*BLOCK_SIZE
;
504 let mut end
= start
+ BLOCK_SIZE
;
505 if end
> lines
.len() {
508 let data
= &lines
[start
..end
];
510 for l
in start
..end
{
511 println
!("{:-2}: {}", l
, lines
[l
]);
513 let data
= data
.join("\n");
514 let qr_code
= generate_qr_code("utf8i", data
.as_bytes())?
;
515 let qr_code
= String
::from_utf8(qr_code
)
516 .map_err(|_
| format_err
!("Failed to read qr code (got non-utf8 data)"))?
;
517 println
!("{}", qr_code
);
518 println
!("{}", char::from(12u8)); // page break
524 let key_config
: KeyConfig
= serde_json
::from_str(&data
)?
;
525 let key_text
= serde_json
::to_string_pretty(&key_config
)?
;
527 println
!("-----BEGIN PROXMOX BACKUP KEY-----");
528 println
!("{}", key_text
);
529 println
!("-----END PROXMOX BACKUP KEY-----");
531 let qr_code
= generate_qr_code("utf8i", key_text
.as_bytes())?
;
532 let qr_code
= String
::from_utf8(qr_code
)
533 .map_err(|_
| format_err
!("Failed to read qr code (got non-utf8 data)"))?
;
535 println
!("{}", qr_code
);
540 fn generate_qr_code(output_type
: &str, data
: &[u8]) -> Result
<Vec
<u8>, Error
> {
542 let mut child
= Command
::new("qrencode")
543 .args(&["-t", output_type
, "-m0", "-s1", "-lm", "--output", "-"])
544 .stdin(Stdio
::piped())
545 .stdout(Stdio
::piped())
549 let stdin
= child
.stdin
.as_mut()
550 .ok_or_else(|| format_err
!("Failed to open stdin"))?
;
551 stdin
.write_all(data
)
552 .map_err(|_
| format_err
!("Failed to write to stdin"))?
;
555 let output
= child
.wait_with_output()
556 .map_err(|_
| format_err
!("Failed to read stdout"))?
;
558 let output
= crate::tools
::command_output(output
, None
)?
;