]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
key: add fingerprint to key config
[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
8 use proxmox::api::api;
9 use proxmox::api::cli::{CliCommand, CliCommandMap};
10 use proxmox::sys::linux::tty;
11 use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
12
13 use proxmox_backup::backup::{
14 encrypt_key_with_passphrase,
15 load_and_decrypt_key,
16 store_key_config,
17 CryptConfig,
18 KeyConfig,
19 };
20 use proxmox_backup::tools;
21
22 #[api()]
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.
28 Text,
29 /// Format as Html. Includes QR codes as png images.
30 Html,
31 }
32
33 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
34 pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
35
36 pub fn find_master_pubkey() -> Result<Option<PathBuf>, Error> {
37 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
38 }
39
40 pub fn place_master_pubkey() -> Result<PathBuf, Error> {
41 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
42 }
43
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")
46 }
47
48 pub fn place_default_encryption_key() -> Result<PathBuf, Error> {
49 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file")
50 }
51
52 pub fn read_optional_default_encryption_key() -> Result<Option<Vec<u8>>, Error> {
53 find_default_encryption_key()?
54 .map(file_get_contents)
55 .transpose()
56 }
57
58 pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
59 // fixme: implement other input methods
60
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"),
65 Err(NotPresent) => {
66 // Try another method
67 }
68 }
69
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: ")?);
73 }
74
75 bail!("no password input mechanism available");
76 }
77
78 #[api(
79 default: "scrypt",
80 )]
81 #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
82 #[serde(rename_all = "kebab-case")]
83 /// Key derivation function for password protected encryption keys.
84 pub enum Kdf {
85 /// Do not encrypt the key.
86 None,
87
88 /// Encrypt they key with a password using SCrypt.
89 Scrypt,
90 }
91
92 impl Default for Kdf {
93 #[inline]
94 fn default() -> Self {
95 Kdf::Scrypt
96 }
97 }
98
99 #[api(
100 input: {
101 properties: {
102 kdf: {
103 type: Kdf,
104 optional: true,
105 },
106 path: {
107 description:
108 "Output file. Without this the key will become the new default encryption key.",
109 optional: true,
110 }
111 },
112 },
113 )]
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),
118 None => {
119 let path = place_default_encryption_key()?;
120 println!("creating default key at: {:?}", path);
121 path
122 }
123 };
124
125 let kdf = kdf.unwrap_or_default();
126
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();
131
132 match kdf {
133 Kdf::None => {
134 let created = proxmox::tools::time::epoch_i64();
135
136 store_key_config(
137 &path,
138 false,
139 KeyConfig {
140 kdf: None,
141 created,
142 modified: created,
143 data: key,
144 fingerprint: Some(crypt_config.fingerprint()),
145 },
146 )?;
147 }
148 Kdf::Scrypt => {
149 // always read passphrase from tty
150 if !tty::stdin_isatty() {
151 bail!("unable to read passphrase - no tty");
152 }
153
154 let password = tty::read_and_verify_password("Encryption Key Password: ")?;
155
156 let mut key_config = encrypt_key_with_passphrase(&key, &password)?;
157 key_config.fingerprint = Some(crypt_config.fingerprint());
158
159 store_key_config(&path, false, key_config)?;
160 }
161 }
162
163 Ok(())
164 }
165
166 #[api(
167 input: {
168 properties: {
169 kdf: {
170 type: Kdf,
171 optional: true,
172 },
173 path: {
174 description: "Key file. Without this the default key's password will be changed.",
175 optional: true,
176 }
177 },
178 },
179 )]
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),
184 None => {
185 let path = find_default_encryption_key()?
186 .ok_or_else(|| {
187 format_err!("no encryption file provided and no default file found")
188 })?;
189 println!("updating default key at: {:?}", path);
190 path
191 }
192 };
193
194 let kdf = kdf.unwrap_or_default();
195
196 if !tty::stdin_isatty() {
197 bail!("unable to change passphrase - no tty");
198 }
199
200 let (key, created, fingerprint) = load_and_decrypt_key(&path, &get_encryption_key_password)?;
201
202 match kdf {
203 Kdf::None => {
204 let modified = proxmox::tools::time::epoch_i64();
205
206 store_key_config(
207 &path,
208 true,
209 KeyConfig {
210 kdf: None,
211 created, // keep original value
212 modified,
213 data: key.to_vec(),
214 fingerprint: Some(fingerprint),
215 },
216 )?;
217 }
218 Kdf::Scrypt => {
219 let password = tty::read_and_verify_password("New Password: ")?;
220
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);
224
225 store_key_config(&path, true, new_key_config)?;
226 }
227 }
228
229 Ok(())
230 }
231
232 #[api(
233 input: {
234 properties: {
235 path: {
236 description: "Path to the PEM formatted RSA public key.",
237 },
238 },
239 },
240 )]
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)?;
245
246 if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) {
247 bail!("Unable to decode PEM data - {}", err);
248 }
249
250 let target_path = place_master_pubkey()?;
251
252 replace_file(&target_path, &pem_data, CreateOptions::new())?;
253
254 println!("Imported public master key to {:?}", target_path);
255
256 Ok(())
257 }
258
259 #[api]
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");
266 }
267
268 let rsa = openssl::rsa::Rsa::generate(4096)?;
269 let pkey = openssl::pkey::PKey::from_rsa(rsa)?;
270
271 let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?;
272
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())?;
277
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())?;
280
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())?;
284
285 Ok(())
286 }
287
288 #[api(
289 input: {
290 properties: {
291 path: {
292 description: "Key file. Without this the default key's will be used.",
293 optional: true,
294 },
295 subject: {
296 description: "Include the specified subject as titel text.",
297 optional: true,
298 },
299 "output-format": {
300 type: PaperkeyFormat,
301 description: "Output format. Text or Html.",
302 optional: true,
303 },
304 },
305 },
306 )]
307 /// Generate a printable, human readable text file containing the encryption key.
308 ///
309 /// This also includes a scanable QR code for fast key restore.
310 fn paper_key(
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),
317 None => {
318 let path = find_default_encryption_key()?
319 .ok_or_else(|| {
320 format_err!("no encryption file provided and no default file found")
321 })?;
322 path
323 }
324 };
325
326 let data = file_get_contents(&path)?;
327 let data = std::str::from_utf8(&data)?;
328
329 let format = output_format.unwrap_or(PaperkeyFormat::Html);
330
331 match format {
332 PaperkeyFormat::Html => paperkey_html(data, subject),
333 PaperkeyFormat::Text => paperkey_text(data, subject),
334 }
335 }
336
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);
341
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);
345
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);
350
351 let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY)
352 .arg_param(&["path"])
353 .completion_cb("path", tools::complete_file_name);
354
355 CliCommandMap::new()
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)
361 }
362
363 fn paperkey_html(data: &str, subject: Option<String>) -> Result<(), Error> {
364
365 let img_size_pt = 500;
366
367 println!("<!DOCTYPE html>");
368 println!("<html lang=\"en\">");
369 println!("<head>");
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\">");
374
375 println!(" p {{");
376 println!(" font-size: 12pt;");
377 println!(" font-family: monospace;");
378 println!(" white-space: pre-wrap;");
379 println!(" line-break: anywhere;");
380 println!(" }}");
381
382 println!("</style>");
383
384 println!("</head>");
385
386 println!("<body>");
387
388 if let Some(subject) = subject {
389 println!("<p>Subject: {}</p>", subject);
390 }
391
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())
396 .map(String::from)
397 .collect();
398
399 if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
400 bail!("unexpected key format");
401 }
402
403 if lines.len() < 20 {
404 bail!("unexpected key format");
405 }
406
407 const BLOCK_SIZE: usize = 20;
408 let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
409
410 for i in 0..blocks {
411 let start = i*BLOCK_SIZE;
412 let mut end = start + BLOCK_SIZE;
413 if end > lines.len() {
414 end = lines.len();
415 }
416 let data = &lines[start..end];
417
418 println!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
419 println!("<p>");
420
421 for l in start..end {
422 println!("{:02}: {}", l, lines[l]);
423 }
424
425 println!("</p>");
426
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);
430
431 println!("<center>");
432 println!("<img");
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>");
436 println!("</div>");
437 }
438
439 println!("</body>");
440 println!("</html>");
441 return Ok(());
442 }
443
444 let key_config: KeyConfig = serde_json::from_str(&data)?;
445 let key_text = serde_json::to_string_pretty(&key_config)?;
446
447 println!("<div style=\"page-break-inside: avoid\">");
448
449 println!("<p>");
450
451 println!("-----BEGIN PROXMOX BACKUP KEY-----");
452
453 for line in key_text.lines() {
454 println!("{}", line);
455 }
456
457 println!("-----END PROXMOX BACKUP KEY-----");
458
459 println!("</p>");
460
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);
463
464 println!("<center>");
465 println!("<img");
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>");
469
470 println!("</div>");
471
472 println!("</body>");
473 println!("</html>");
474
475 Ok(())
476 }
477
478 fn paperkey_text(data: &str, subject: Option<String>) -> Result<(), Error> {
479
480 if let Some(subject) = subject {
481 println!("Subject: {}\n", subject);
482 }
483
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())
488 .map(String::from)
489 .collect();
490
491 if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
492 bail!("unexpected key format");
493 }
494
495 if lines.len() < 20 {
496 bail!("unexpected key format");
497 }
498
499 const BLOCK_SIZE: usize = 5;
500 let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
501
502 for i in 0..blocks {
503 let start = i*BLOCK_SIZE;
504 let mut end = start + BLOCK_SIZE;
505 if end > lines.len() {
506 end = lines.len();
507 }
508 let data = &lines[start..end];
509
510 for l in start..end {
511 println!("{:-2}: {}", l, lines[l]);
512 }
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
519
520 }
521 return Ok(());
522 }
523
524 let key_config: KeyConfig = serde_json::from_str(&data)?;
525 let key_text = serde_json::to_string_pretty(&key_config)?;
526
527 println!("-----BEGIN PROXMOX BACKUP KEY-----");
528 println!("{}", key_text);
529 println!("-----END PROXMOX BACKUP KEY-----");
530
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)"))?;
534
535 println!("{}", qr_code);
536
537 Ok(())
538 }
539
540 fn generate_qr_code(output_type: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
541
542 let mut child = Command::new("qrencode")
543 .args(&["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"])
544 .stdin(Stdio::piped())
545 .stdout(Stdio::piped())
546 .spawn()?;
547
548 {
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"))?;
553 }
554
555 let output = child.wait_with_output()
556 .map_err(|_| format_err!("Failed to read stdout"))?;
557
558 let output = crate::tools::command_output(output, None)?;
559
560 Ok(output)
561 }