]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
remove unused descriptions from api macros
[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 optional: true,
376 },
377 },
378 },
379 )]
380 /// Generate a printable, human readable text file containing the encryption key.
381 ///
382 /// This also includes a scanable QR code for fast key restore.
383 fn paper_key(
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),
390 None => {
391 let path = find_default_encryption_key()?
392 .ok_or_else(|| {
393 format_err!("no encryption file provided and no default file found")
394 })?;
395 path
396 }
397 };
398
399 let data = file_get_contents(&path)?;
400 let data = String::from_utf8(data)?;
401
402 let (data, is_private_key) = if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
403 let lines: Vec<String> = data
404 .lines()
405 .map(|s| s.trim_end())
406 .filter(|s| !s.is_empty())
407 .map(String::from)
408 .collect();
409
410 if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
411 bail!("unexpected key format");
412 }
413
414 if lines.len() < 20 {
415 bail!("unexpected key format");
416 }
417
418 (lines, true)
419 } else {
420 match serde_json::from_str::<KeyConfig>(&data) {
421 Ok(key_config) => {
422 let lines = serde_json::to_string_pretty(&key_config)?
423 .lines()
424 .map(String::from)
425 .collect();
426
427 (lines, false)
428 },
429 Err(err) => {
430 eprintln!("Couldn't parse '{:?}' as KeyConfig - {}", path, err);
431 bail!("Neither a PEM-formatted private key, nor a PBS key file.");
432 },
433 }
434 };
435
436 let format = output_format.unwrap_or(PaperkeyFormat::Html);
437
438 match format {
439 PaperkeyFormat::Html => paperkey_html(&data, subject, is_private_key),
440 PaperkeyFormat::Text => paperkey_text(&data, subject, is_private_key),
441 }
442 }
443
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);
448
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);
452
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);
457
458 let key_show_cmd_def = CliCommand::new(&API_METHOD_SHOW_KEY)
459 .arg_param(&["path"])
460 .completion_cb("path", tools::complete_file_name);
461
462 let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY)
463 .arg_param(&["path"])
464 .completion_cb("path", tools::complete_file_name);
465
466 CliCommandMap::new()
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)
473 }
474
475 fn paperkey_html(lines: &[String], subject: Option<String>, is_private: bool) -> Result<(), Error> {
476
477 let img_size_pt = 500;
478
479 println!("<!DOCTYPE html>");
480 println!("<html lang=\"en\">");
481 println!("<head>");
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\">");
486
487 println!(" p {{");
488 println!(" font-size: 12pt;");
489 println!(" font-family: monospace;");
490 println!(" white-space: pre-wrap;");
491 println!(" line-break: anywhere;");
492 println!(" }}");
493
494 println!("</style>");
495
496 println!("</head>");
497
498 println!("<body>");
499
500 if let Some(subject) = subject {
501 println!("<p>Subject: {}</p>", subject);
502 }
503
504 if is_private {
505 const BLOCK_SIZE: usize = 20;
506 let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
507
508 for i in 0..blocks {
509 let start = i*BLOCK_SIZE;
510 let mut end = start + BLOCK_SIZE;
511 if end > lines.len() {
512 end = lines.len();
513 }
514 let data = &lines[start..end];
515
516 println!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
517 println!("<p>");
518
519 for l in start..end {
520 println!("{:02}: {}", l, lines[l]);
521 }
522
523 println!("</p>");
524
525 let qr_code = generate_qr_code("svg", data)?;
526 let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
527
528 println!("<center>");
529 println!("<img");
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>");
533 println!("</div>");
534 }
535
536 println!("</body>");
537 println!("</html>");
538 return Ok(());
539 }
540
541 println!("<div style=\"page-break-inside: avoid\">");
542
543 println!("<p>");
544
545 println!("-----BEGIN PROXMOX BACKUP KEY-----");
546
547 for line in lines {
548 println!("{}", line);
549 }
550
551 println!("-----END PROXMOX BACKUP KEY-----");
552
553 println!("</p>");
554
555 let qr_code = generate_qr_code("svg", lines)?;
556 let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
557
558 println!("<center>");
559 println!("<img");
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>");
563
564 println!("</div>");
565
566 println!("</body>");
567 println!("</html>");
568
569 Ok(())
570 }
571
572 fn paperkey_text(lines: &[String], subject: Option<String>, is_private: bool) -> Result<(), Error> {
573
574 if let Some(subject) = subject {
575 println!("Subject: {}\n", subject);
576 }
577
578 if is_private {
579 const BLOCK_SIZE: usize = 5;
580 let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
581
582 for i in 0..blocks {
583 let start = i*BLOCK_SIZE;
584 let mut end = start + BLOCK_SIZE;
585 if end > lines.len() {
586 end = lines.len();
587 }
588 let data = &lines[start..end];
589
590 for l in start..end {
591 println!("{:-2}: {}", l, lines[l]);
592 }
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
598
599 }
600 return Ok(());
601 }
602
603 println!("-----BEGIN PROXMOX BACKUP KEY-----");
604 for line in lines {
605 println!("{}", line);
606 }
607 println!("-----END PROXMOX BACKUP KEY-----");
608
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)"))?;
612
613 println!("{}", qr_code);
614
615 Ok(())
616 }
617
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())
623 .spawn()?;
624
625 {
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"))?;
631 }
632
633 let output = child.wait_with_output()
634 .map_err(|_| format_err!("Failed to read stdout"))?;
635
636 let output = crate::tools::command_output(output, None)?;
637
638 Ok(output)
639 }