]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
cli: cleanup 'key show' - use format_and_print_result_full
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
1 use std::path::PathBuf;
2 use std::io::Write;
3 use std::process::{Stdio, Command};
4
5 use anyhow::{bail, format_err, Error};
6 use serde::{Deserialize, Serialize};
7 use serde_json::Value;
8
9 use proxmox::api::api;
10 use proxmox::api::cli::{
11 ColumnConfig,
12 CliCommand,
13 CliCommandMap,
14 format_and_print_result_full,
15 get_output_format,
16 OUTPUT_FORMAT,
17 };
18 use proxmox::sys::linux::tty;
19 use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
20
21 use proxmox_backup::backup::{
22 encrypt_key_with_passphrase,
23 load_and_decrypt_key,
24 store_key_config,
25 CryptConfig,
26 Kdf,
27 KeyConfig,
28 KeyDerivationConfig,
29 };
30 use proxmox_backup::tools;
31
32 #[api()]
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.
38 Text,
39 /// Format as Html. Includes QR codes as png images.
40 Html,
41 }
42
43 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
44 pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
45
46 pub fn find_master_pubkey() -> Result<Option<PathBuf>, Error> {
47 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
48 }
49
50 pub fn place_master_pubkey() -> Result<PathBuf, Error> {
51 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
52 }
53
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")
56 }
57
58 pub fn place_default_encryption_key() -> Result<PathBuf, Error> {
59 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file")
60 }
61
62 pub fn read_optional_default_encryption_key() -> Result<Option<Vec<u8>>, Error> {
63 find_default_encryption_key()?
64 .map(file_get_contents)
65 .transpose()
66 }
67
68 pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
69 // fixme: implement other input methods
70
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"),
75 Err(NotPresent) => {
76 // Try another method
77 }
78 }
79
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: ")?);
83 }
84
85 bail!("no password input mechanism available");
86 }
87
88 #[api(
89 input: {
90 properties: {
91 kdf: {
92 type: Kdf,
93 optional: true,
94 },
95 path: {
96 description:
97 "Output file. Without this the key will become the new default encryption key.",
98 optional: true,
99 }
100 },
101 },
102 )]
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),
107 None => {
108 let path = place_default_encryption_key()?;
109 println!("creating default key at: {:?}", path);
110 path
111 }
112 };
113
114 let kdf = kdf.unwrap_or_default();
115
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();
120
121 match kdf {
122 Kdf::None => {
123 let created = proxmox::tools::time::epoch_i64();
124
125 store_key_config(
126 &path,
127 false,
128 KeyConfig {
129 kdf: None,
130 created,
131 modified: created,
132 data: key,
133 fingerprint: Some(crypt_config.fingerprint()),
134 },
135 )?;
136 }
137 Kdf::Scrypt | Kdf::PBKDF2 => {
138 // always read passphrase from tty
139 if !tty::stdin_isatty() {
140 bail!("unable to read passphrase - no tty");
141 }
142
143 let password = tty::read_and_verify_password("Encryption Key Password: ")?;
144
145 let mut key_config = encrypt_key_with_passphrase(&key, &password, kdf)?;
146 key_config.fingerprint = Some(crypt_config.fingerprint());
147
148 store_key_config(&path, false, key_config)?;
149 }
150 }
151
152 Ok(())
153 }
154
155 #[api(
156 input: {
157 properties: {
158 kdf: {
159 type: Kdf,
160 optional: true,
161 },
162 path: {
163 description: "Key file. Without this the default key's password will be changed.",
164 optional: true,
165 }
166 },
167 },
168 )]
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),
173 None => {
174 let path = find_default_encryption_key()?
175 .ok_or_else(|| {
176 format_err!("no encryption file provided and no default file found")
177 })?;
178 println!("updating default key at: {:?}", path);
179 path
180 }
181 };
182
183 let kdf = kdf.unwrap_or_default();
184
185 if !tty::stdin_isatty() {
186 bail!("unable to change passphrase - no tty");
187 }
188
189 let (key, created, fingerprint) = load_and_decrypt_key(&path, &get_encryption_key_password)?;
190
191 match kdf {
192 Kdf::None => {
193 let modified = proxmox::tools::time::epoch_i64();
194
195 store_key_config(
196 &path,
197 true,
198 KeyConfig {
199 kdf: None,
200 created, // keep original value
201 modified,
202 data: key.to_vec(),
203 fingerprint: Some(fingerprint),
204 },
205 )?;
206 }
207 Kdf::Scrypt | Kdf::PBKDF2 => {
208 let password = tty::read_and_verify_password("New Password: ")?;
209
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);
213
214 store_key_config(&path, true, new_key_config)?;
215 }
216 }
217
218 Ok(())
219 }
220
221 #[api(
222 properties: {
223 kdf: {
224 type: Kdf,
225 },
226 },
227 )]
228 #[derive(Deserialize, Serialize)]
229 /// Encryption Key Information
230 struct KeyInfo {
231 /// Path to key
232 path: String,
233 kdf: Kdf,
234 /// Key creation time
235 pub created: i64,
236 /// Key modification time
237 pub modified: i64,
238 /// Key fingerprint
239 pub fingerprint: Option<String>,
240 }
241
242 #[api(
243 input: {
244 properties: {
245 path: {
246 description: "Key file. Without this the default key's metadata will be shown.",
247 optional: true,
248 },
249 "output-format": {
250 schema: OUTPUT_FORMAT,
251 optional: true,
252 },
253 },
254 },
255 )]
256 /// Print the encryption key's metadata.
257 fn show_key(
258 path: Option<String>,
259 param: Value,
260 ) -> Result<(), Error> {
261 let path = match path {
262 Some(path) => PathBuf::from(path),
263 None => {
264 let path = find_default_encryption_key()?
265 .ok_or_else(|| {
266 format_err!("no encryption file provided and no default file found")
267 })?;
268 path
269 }
270 };
271
272
273 let config: KeyConfig = serde_json::from_slice(&file_get_contents(path.clone())?)?;
274
275 let output_format = get_output_format(&param);
276
277 let info = KeyInfo {
278 path: format!("{:?}", path),
279 kdf: match config.kdf {
280 Some(KeyDerivationConfig::PBKDF2 { .. }) => Kdf::PBKDF2,
281 Some(KeyDerivationConfig::Scrypt { .. }) => Kdf::Scrypt,
282 None => Kdf::None,
283 },
284 created: config.created,
285 modified: config.modified,
286 fingerprint: match config.fingerprint {
287 Some(ref fp) => Some(format!("{}", fp)),
288 None => None,
289 },
290 };
291
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"));
298
299 let schema = &KeyInfo::API_SCHEMA;
300
301 format_and_print_result_full(&mut serde_json::to_value(info)?, schema, &output_format, &options);
302
303 Ok(())
304 }
305
306 #[api(
307 input: {
308 properties: {
309 path: {
310 description: "Path to the PEM formatted RSA public key.",
311 },
312 },
313 },
314 )]
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)?;
319
320 if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) {
321 bail!("Unable to decode PEM data - {}", err);
322 }
323
324 let target_path = place_master_pubkey()?;
325
326 replace_file(&target_path, &pem_data, CreateOptions::new())?;
327
328 println!("Imported public master key to {:?}", target_path);
329
330 Ok(())
331 }
332
333 #[api]
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");
340 }
341
342 let rsa = openssl::rsa::Rsa::generate(4096)?;
343 let pkey = openssl::pkey::PKey::from_rsa(rsa)?;
344
345 let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?;
346
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())?;
351
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())?;
354
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())?;
358
359 Ok(())
360 }
361
362 #[api(
363 input: {
364 properties: {
365 path: {
366 description: "Key file. Without this the default key's will be used.",
367 optional: true,
368 },
369 subject: {
370 description: "Include the specified subject as titel text.",
371 optional: true,
372 },
373 "output-format": {
374 type: PaperkeyFormat,
375 description: "Output format. Text or Html.",
376 optional: true,
377 },
378 },
379 },
380 )]
381 /// Generate a printable, human readable text file containing the encryption key.
382 ///
383 /// This also includes a scanable QR code for fast key restore.
384 fn paper_key(
385 path: Option<String>,
386 subject: Option<String>,
387 output_format: Option<PaperkeyFormat>,
388 ) -> Result<(), Error> {
389 let path = match path {
390 Some(path) => PathBuf::from(path),
391 None => {
392 let path = find_default_encryption_key()?
393 .ok_or_else(|| {
394 format_err!("no encryption file provided and no default file found")
395 })?;
396 path
397 }
398 };
399
400 let data = file_get_contents(&path)?;
401 let data = std::str::from_utf8(&data)?;
402
403 let format = output_format.unwrap_or(PaperkeyFormat::Html);
404
405 match format {
406 PaperkeyFormat::Html => paperkey_html(data, subject),
407 PaperkeyFormat::Text => paperkey_text(data, subject),
408 }
409 }
410
411 pub fn cli() -> CliCommandMap {
412 let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE)
413 .arg_param(&["path"])
414 .completion_cb("path", tools::complete_file_name);
415
416 let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE)
417 .arg_param(&["path"])
418 .completion_cb("path", tools::complete_file_name);
419
420 let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY);
421 let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY)
422 .arg_param(&["path"])
423 .completion_cb("path", tools::complete_file_name);
424
425 let key_show_cmd_def = CliCommand::new(&API_METHOD_SHOW_KEY)
426 .arg_param(&["path"])
427 .completion_cb("path", tools::complete_file_name);
428
429 let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY)
430 .arg_param(&["path"])
431 .completion_cb("path", tools::complete_file_name);
432
433 CliCommandMap::new()
434 .insert("create", key_create_cmd_def)
435 .insert("create-master-key", key_create_master_key_cmd_def)
436 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def)
437 .insert("change-passphrase", key_change_passphrase_cmd_def)
438 .insert("show", key_show_cmd_def)
439 .insert("paperkey", paper_key_cmd_def)
440 }
441
442 fn paperkey_html(data: &str, subject: Option<String>) -> Result<(), Error> {
443
444 let img_size_pt = 500;
445
446 println!("<!DOCTYPE html>");
447 println!("<html lang=\"en\">");
448 println!("<head>");
449 println!("<meta charset=\"utf-8\">");
450 println!("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
451 println!("<title>Proxmox Backup Paperkey</title>");
452 println!("<style type=\"text/css\">");
453
454 println!(" p {{");
455 println!(" font-size: 12pt;");
456 println!(" font-family: monospace;");
457 println!(" white-space: pre-wrap;");
458 println!(" line-break: anywhere;");
459 println!(" }}");
460
461 println!("</style>");
462
463 println!("</head>");
464
465 println!("<body>");
466
467 if let Some(subject) = subject {
468 println!("<p>Subject: {}</p>", subject);
469 }
470
471 if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
472 let lines: Vec<String> = data.lines()
473 .map(|s| s.trim_end())
474 .filter(|s| !s.is_empty())
475 .map(String::from)
476 .collect();
477
478 if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
479 bail!("unexpected key format");
480 }
481
482 if lines.len() < 20 {
483 bail!("unexpected key format");
484 }
485
486 const BLOCK_SIZE: usize = 20;
487 let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
488
489 for i in 0..blocks {
490 let start = i*BLOCK_SIZE;
491 let mut end = start + BLOCK_SIZE;
492 if end > lines.len() {
493 end = lines.len();
494 }
495 let data = &lines[start..end];
496
497 println!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
498 println!("<p>");
499
500 for l in start..end {
501 println!("{:02}: {}", l, lines[l]);
502 }
503
504 println!("</p>");
505
506 let data = data.join("\n");
507 let qr_code = generate_qr_code("svg", data.as_bytes())?;
508 let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
509
510 println!("<center>");
511 println!("<img");
512 println!("width=\"{}pt\" height=\"{}pt\"", img_size_pt, img_size_pt);
513 println!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code);
514 println!("</center>");
515 println!("</div>");
516 }
517
518 println!("</body>");
519 println!("</html>");
520 return Ok(());
521 }
522
523 let key_config: KeyConfig = serde_json::from_str(&data)?;
524 let key_text = serde_json::to_string_pretty(&key_config)?;
525
526 println!("<div style=\"page-break-inside: avoid\">");
527
528 println!("<p>");
529
530 println!("-----BEGIN PROXMOX BACKUP KEY-----");
531
532 for line in key_text.lines() {
533 println!("{}", line);
534 }
535
536 println!("-----END PROXMOX BACKUP KEY-----");
537
538 println!("</p>");
539
540 let qr_code = generate_qr_code("svg", key_text.as_bytes())?;
541 let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
542
543 println!("<center>");
544 println!("<img");
545 println!("width=\"{}pt\" height=\"{}pt\"", img_size_pt, img_size_pt);
546 println!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code);
547 println!("</center>");
548
549 println!("</div>");
550
551 println!("</body>");
552 println!("</html>");
553
554 Ok(())
555 }
556
557 fn paperkey_text(data: &str, subject: Option<String>) -> Result<(), Error> {
558
559 if let Some(subject) = subject {
560 println!("Subject: {}\n", subject);
561 }
562
563 if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
564 let lines: Vec<String> = data.lines()
565 .map(|s| s.trim_end())
566 .filter(|s| !s.is_empty())
567 .map(String::from)
568 .collect();
569
570 if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
571 bail!("unexpected key format");
572 }
573
574 if lines.len() < 20 {
575 bail!("unexpected key format");
576 }
577
578 const BLOCK_SIZE: usize = 5;
579 let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
580
581 for i in 0..blocks {
582 let start = i*BLOCK_SIZE;
583 let mut end = start + BLOCK_SIZE;
584 if end > lines.len() {
585 end = lines.len();
586 }
587 let data = &lines[start..end];
588
589 for l in start..end {
590 println!("{:-2}: {}", l, lines[l]);
591 }
592 let data = data.join("\n");
593 let qr_code = generate_qr_code("utf8i", data.as_bytes())?;
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
598
599 }
600 return Ok(());
601 }
602
603 let key_config: KeyConfig = serde_json::from_str(&data)?;
604 let key_text = serde_json::to_string_pretty(&key_config)?;
605
606 println!("-----BEGIN PROXMOX BACKUP KEY-----");
607 println!("{}", key_text);
608 println!("-----END PROXMOX BACKUP KEY-----");
609
610 let qr_code = generate_qr_code("utf8i", key_text.as_bytes())?;
611 let qr_code = String::from_utf8(qr_code)
612 .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?;
613
614 println!("{}", qr_code);
615
616 Ok(())
617 }
618
619 fn generate_qr_code(output_type: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
620
621 let mut child = Command::new("qrencode")
622 .args(&["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"])
623 .stdin(Stdio::piped())
624 .stdout(Stdio::piped())
625 .spawn()?;
626
627 {
628 let stdin = child.stdin.as_mut()
629 .ok_or_else(|| format_err!("Failed to open stdin"))?;
630 stdin.write_all(data)
631 .map_err(|_| format_err!("Failed to write to stdin"))?;
632 }
633
634 let output = child.wait_with_output()
635 .map_err(|_| format_err!("Failed to read stdout"))?;
636
637 let output = crate::tools::command_output(output, None)?;
638
639 Ok(output)
640 }