]>
Commit | Line | Data |
---|---|---|
9696f519 | 1 | use std::path::PathBuf; |
82a0cd2a DM |
2 | use std::io::Write; |
3 | use std::process::{Stdio, Command}; | |
9696f519 | 4 | |
05389a01 | 5 | use anyhow::{bail, format_err, Error}; |
9696f519 | 6 | use serde::{Deserialize, Serialize}; |
9696f519 WB |
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, load_and_decrypt_key, store_key_config, KeyConfig, | |
15 | }; | |
16 | use proxmox_backup::tools; | |
17 | ||
ef1b4363 DM |
18 | #[api()] |
19 | #[derive(Debug, Serialize, Deserialize)] | |
20 | #[serde(rename_all = "lowercase")] | |
21 | /// Paperkey output format | |
22 | pub enum PaperkeyFormat { | |
23 | /// Format as Utf8 text. Includes QR codes as ascii-art. | |
24 | Text, | |
25 | /// Format as Html. Includes QR codes as png images. | |
26 | Html, | |
27 | } | |
28 | ||
b65390eb | 29 | pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json"; |
05389a01 | 30 | pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem"; |
b65390eb | 31 | |
05389a01 WB |
32 | pub fn find_master_pubkey() -> Result<Option<PathBuf>, Error> { |
33 | super::find_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file") | |
34 | } | |
9696f519 | 35 | |
05389a01 WB |
36 | pub fn place_master_pubkey() -> Result<PathBuf, Error> { |
37 | super::place_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file") | |
9696f519 WB |
38 | } |
39 | ||
b65390eb | 40 | pub fn find_default_encryption_key() -> Result<Option<PathBuf>, Error> { |
05389a01 | 41 | super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file") |
b65390eb | 42 | } |
9696f519 | 43 | |
b65390eb | 44 | pub fn place_default_encryption_key() -> Result<PathBuf, Error> { |
05389a01 | 45 | super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file") |
9696f519 WB |
46 | } |
47 | ||
0351f23b WB |
48 | pub fn read_optional_default_encryption_key() -> Result<Option<Vec<u8>>, Error> { |
49 | find_default_encryption_key()? | |
50 | .map(file_get_contents) | |
51 | .transpose() | |
52 | } | |
53 | ||
9696f519 WB |
54 | pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> { |
55 | // fixme: implement other input methods | |
56 | ||
57 | use std::env::VarError::*; | |
58 | match std::env::var("PBS_ENCRYPTION_PASSWORD") { | |
59 | Ok(p) => return Ok(p.as_bytes().to_vec()), | |
60 | Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"), | |
61 | Err(NotPresent) => { | |
62 | // Try another method | |
63 | } | |
64 | } | |
65 | ||
66 | // If we're on a TTY, query the user for a password | |
67 | if tty::stdin_isatty() { | |
68 | return Ok(tty::read_password("Encryption Key Password: ")?); | |
69 | } | |
70 | ||
71 | bail!("no password input mechanism available"); | |
72 | } | |
73 | ||
74 | #[api( | |
75 | default: "scrypt", | |
76 | )] | |
77 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] | |
78 | #[serde(rename_all = "kebab-case")] | |
79 | /// Key derivation function for password protected encryption keys. | |
80 | pub enum Kdf { | |
81 | /// Do not encrypt the key. | |
82 | None, | |
83 | ||
84 | /// Encrypt they key with a password using SCrypt. | |
85 | Scrypt, | |
86 | } | |
87 | ||
88 | impl Default for Kdf { | |
89 | #[inline] | |
90 | fn default() -> Self { | |
91 | Kdf::Scrypt | |
92 | } | |
93 | } | |
94 | ||
95 | #[api( | |
96 | input: { | |
97 | properties: { | |
98 | kdf: { | |
99 | type: Kdf, | |
100 | optional: true, | |
101 | }, | |
102 | path: { | |
103 | description: | |
104 | "Output file. Without this the key will become the new default encryption key.", | |
105 | optional: true, | |
106 | } | |
107 | }, | |
108 | }, | |
109 | )] | |
110 | /// Create a new encryption key. | |
111 | fn create(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> { | |
112 | let path = match path { | |
113 | Some(path) => PathBuf::from(path), | |
0eaef8eb WB |
114 | None => { |
115 | let path = place_default_encryption_key()?; | |
116 | println!("creating default key at: {:?}", path); | |
117 | path | |
118 | } | |
9696f519 WB |
119 | }; |
120 | ||
121 | let kdf = kdf.unwrap_or_default(); | |
122 | ||
123 | let key = proxmox::sys::linux::random_data(32)?; | |
124 | ||
125 | match kdf { | |
126 | Kdf::None => { | |
6a7be83e | 127 | let created = proxmox::tools::time::epoch_i64(); |
9696f519 WB |
128 | |
129 | store_key_config( | |
130 | &path, | |
131 | false, | |
132 | KeyConfig { | |
133 | kdf: None, | |
134 | created, | |
135 | modified: created, | |
136 | data: key, | |
137 | }, | |
138 | )?; | |
139 | } | |
140 | Kdf::Scrypt => { | |
141 | // always read passphrase from tty | |
142 | if !tty::stdin_isatty() { | |
143 | bail!("unable to read passphrase - no tty"); | |
144 | } | |
145 | ||
146 | let password = tty::read_and_verify_password("Encryption Key Password: ")?; | |
147 | ||
148 | let key_config = encrypt_key_with_passphrase(&key, &password)?; | |
149 | ||
150 | store_key_config(&path, false, key_config)?; | |
151 | } | |
152 | } | |
153 | ||
154 | Ok(()) | |
155 | } | |
156 | ||
157 | #[api( | |
158 | input: { | |
159 | properties: { | |
160 | kdf: { | |
161 | type: Kdf, | |
162 | optional: true, | |
163 | }, | |
164 | path: { | |
165 | description: "Key file. Without this the default key's password will be changed.", | |
166 | optional: true, | |
167 | } | |
168 | }, | |
169 | }, | |
170 | )] | |
171 | /// Change the encryption key's password. | |
172 | fn change_passphrase(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> { | |
173 | let path = match path { | |
174 | Some(path) => PathBuf::from(path), | |
0eaef8eb WB |
175 | None => { |
176 | let path = find_default_encryption_key()? | |
177 | .ok_or_else(|| { | |
178 | format_err!("no encryption file provided and no default file found") | |
179 | })?; | |
180 | println!("updating default key at: {:?}", path); | |
181 | path | |
182 | } | |
9696f519 WB |
183 | }; |
184 | ||
185 | let kdf = kdf.unwrap_or_default(); | |
186 | ||
187 | if !tty::stdin_isatty() { | |
188 | bail!("unable to change passphrase - no tty"); | |
189 | } | |
190 | ||
191 | let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?; | |
192 | ||
193 | match kdf { | |
194 | Kdf::None => { | |
6a7be83e | 195 | let modified = proxmox::tools::time::epoch_i64(); |
9696f519 WB |
196 | |
197 | store_key_config( | |
198 | &path, | |
199 | true, | |
200 | KeyConfig { | |
201 | kdf: None, | |
202 | created, // keep original value | |
203 | modified, | |
204 | data: key.to_vec(), | |
205 | }, | |
206 | )?; | |
207 | } | |
208 | Kdf::Scrypt => { | |
209 | let password = tty::read_and_verify_password("New Password: ")?; | |
210 | ||
211 | let mut new_key_config = encrypt_key_with_passphrase(&key, &password)?; | |
212 | new_key_config.created = created; // keep original value | |
213 | ||
214 | store_key_config(&path, true, new_key_config)?; | |
215 | } | |
216 | } | |
217 | ||
218 | Ok(()) | |
219 | } | |
220 | ||
221 | #[api( | |
222 | input: { | |
223 | properties: { | |
224 | path: { | |
225 | description: "Path to the PEM formatted RSA public key.", | |
226 | }, | |
227 | }, | |
228 | }, | |
229 | )] | |
230 | /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption | |
231 | /// key onto the backup server along with each backup. | |
232 | fn import_master_pubkey(path: String) -> Result<(), Error> { | |
233 | let pem_data = file_get_contents(&path)?; | |
234 | ||
235 | if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) { | |
236 | bail!("Unable to decode PEM data - {}", err); | |
237 | } | |
238 | ||
05389a01 | 239 | let target_path = place_master_pubkey()?; |
9696f519 WB |
240 | |
241 | replace_file(&target_path, &pem_data, CreateOptions::new())?; | |
242 | ||
243 | println!("Imported public master key to {:?}", target_path); | |
244 | ||
245 | Ok(()) | |
246 | } | |
247 | ||
248 | #[api] | |
249 | /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup | |
250 | /// encryption key onto the backup server along with each backup. | |
251 | fn create_master_key() -> Result<(), Error> { | |
252 | // we need a TTY to query the new password | |
253 | if !tty::stdin_isatty() { | |
254 | bail!("unable to create master key - no tty"); | |
255 | } | |
256 | ||
257 | let rsa = openssl::rsa::Rsa::generate(4096)?; | |
258 | let pkey = openssl::pkey::PKey::from_rsa(rsa)?; | |
259 | ||
260 | let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?; | |
261 | ||
262 | let pub_key: Vec<u8> = pkey.public_key_to_pem()?; | |
263 | let filename_pub = "master-public.pem"; | |
264 | println!("Writing public master key to {}", filename_pub); | |
265 | replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?; | |
266 | ||
267 | let cipher = openssl::symm::Cipher::aes_256_cbc(); | |
268 | let priv_key: Vec<u8> = pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?; | |
269 | ||
270 | let filename_priv = "master-private.pem"; | |
271 | println!("Writing private master key to {}", filename_priv); | |
272 | replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?; | |
273 | ||
274 | Ok(()) | |
275 | } | |
276 | ||
82a0cd2a DM |
277 | #[api( |
278 | input: { | |
279 | properties: { | |
280 | path: { | |
281 | description: "Key file. Without this the default key's will be used.", | |
282 | optional: true, | |
283 | }, | |
284 | subject: { | |
285 | description: "Include the specified subject as titel text.", | |
286 | optional: true, | |
287 | }, | |
ef1b4363 DM |
288 | "output-format": { |
289 | type: PaperkeyFormat, | |
290 | description: "Output format. Text or Html.", | |
291 | optional: true, | |
292 | }, | |
82a0cd2a DM |
293 | }, |
294 | }, | |
295 | )] | |
296 | /// Generate a printable, human readable text file containing the encryption key. | |
297 | /// | |
298 | /// This also includes a scanable QR code for fast key restore. | |
ef1b4363 DM |
299 | fn paper_key( |
300 | path: Option<String>, | |
301 | subject: Option<String>, | |
302 | output_format: Option<PaperkeyFormat>, | |
303 | ) -> Result<(), Error> { | |
82a0cd2a DM |
304 | let path = match path { |
305 | Some(path) => PathBuf::from(path), | |
306 | None => { | |
307 | let path = find_default_encryption_key()? | |
308 | .ok_or_else(|| { | |
309 | format_err!("no encryption file provided and no default file found") | |
310 | })?; | |
311 | path | |
312 | } | |
313 | }; | |
314 | ||
315 | let data = file_get_contents(&path)?; | |
b02d49ab | 316 | let data = std::str::from_utf8(&data)?; |
82a0cd2a | 317 | |
ef1b4363 DM |
318 | let format = output_format.unwrap_or(PaperkeyFormat::Html); |
319 | ||
320 | match format { | |
321 | PaperkeyFormat::Html => paperkey_html(data, subject), | |
322 | PaperkeyFormat::Text => paperkey_text(data, subject), | |
323 | } | |
324 | } | |
325 | ||
326 | pub fn cli() -> CliCommandMap { | |
327 | let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE) | |
328 | .arg_param(&["path"]) | |
329 | .completion_cb("path", tools::complete_file_name); | |
330 | ||
331 | let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE) | |
332 | .arg_param(&["path"]) | |
333 | .completion_cb("path", tools::complete_file_name); | |
334 | ||
335 | let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY); | |
336 | let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY) | |
337 | .arg_param(&["path"]) | |
338 | .completion_cb("path", tools::complete_file_name); | |
339 | ||
340 | let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY) | |
341 | .arg_param(&["path"]) | |
342 | .completion_cb("path", tools::complete_file_name); | |
343 | ||
344 | CliCommandMap::new() | |
345 | .insert("create", key_create_cmd_def) | |
346 | .insert("create-master-key", key_create_master_key_cmd_def) | |
347 | .insert("import-master-pubkey", key_import_master_pubkey_cmd_def) | |
348 | .insert("change-passphrase", key_change_passphrase_cmd_def) | |
fdc00811 | 349 | .insert("paperkey", paper_key_cmd_def) |
ef1b4363 DM |
350 | } |
351 | ||
352 | fn paperkey_html(data: &str, subject: Option<String>) -> Result<(), Error> { | |
353 | ||
354 | let img_size_pt = 500; | |
355 | ||
356 | println!("<!DOCTYPE html>"); | |
357 | println!("<html lang=\"en\">"); | |
358 | println!("<head>"); | |
359 | println!("<meta charset=\"utf-8\">"); | |
360 | println!("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"); | |
361 | println!("<title>Proxmox Backup Paperkey</title>"); | |
362 | println!("<style type=\"text/css\">"); | |
363 | ||
364 | println!(" p {{"); | |
365 | println!(" font-size: 12pt;"); | |
366 | println!(" font-family: monospace;"); | |
367 | println!(" white-space: pre-wrap;"); | |
368 | println!(" line-break: anywhere;"); | |
369 | println!(" }}"); | |
370 | ||
371 | println!("</style>"); | |
372 | ||
373 | println!("</head>"); | |
374 | ||
375 | println!("<body>"); | |
376 | ||
82a0cd2a | 377 | if let Some(subject) = subject { |
ef1b4363 | 378 | println!("<p>Subject: {}</p>", subject); |
82a0cd2a DM |
379 | } |
380 | ||
b02d49ab | 381 | if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") { |
ef1b4363 DM |
382 | let lines: Vec<String> = data.lines() |
383 | .map(|s| s.trim_end()) | |
384 | .filter(|s| !s.is_empty()) | |
385 | .map(String::from) | |
386 | .collect(); | |
387 | ||
388 | if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") { | |
389 | bail!("unexpected key format"); | |
390 | } | |
391 | ||
392 | if lines.len() < 20 { | |
393 | bail!("unexpected key format"); | |
394 | } | |
395 | ||
396 | const BLOCK_SIZE: usize = 20; | |
397 | let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE; | |
82a0cd2a | 398 | |
ef1b4363 DM |
399 | for i in 0..blocks { |
400 | let start = i*BLOCK_SIZE; | |
401 | let mut end = start + BLOCK_SIZE; | |
402 | if end > lines.len() { | |
403 | end = lines.len(); | |
404 | } | |
405 | let data = &lines[start..end]; | |
406 | ||
407 | println!("<div style=\"page-break-inside: avoid;page-break-after: always\">"); | |
408 | println!("<p>"); | |
409 | ||
410 | for l in start..end { | |
411 | println!("{:02}: {}", l, lines[l]); | |
412 | } | |
413 | ||
414 | println!("</p>"); | |
415 | ||
416 | let data = data.join("\n"); | |
c8774067 | 417 | let qr_code = generate_qr_code("svg", data.as_bytes())?; |
ef1b4363 DM |
418 | let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD); |
419 | ||
420 | println!("<center>"); | |
421 | println!("<img"); | |
422 | println!("width=\"{}pt\" height=\"{}pt\"", img_size_pt, img_size_pt); | |
c8774067 | 423 | println!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code); |
ef1b4363 DM |
424 | println!("</center>"); |
425 | println!("</div>"); | |
426 | } | |
427 | ||
428 | println!("</body>"); | |
429 | println!("</html>"); | |
430 | return Ok(()); | |
431 | } | |
432 | ||
433 | let key_config: KeyConfig = serde_json::from_str(&data)?; | |
434 | let key_text = serde_json::to_string_pretty(&key_config)?; | |
435 | ||
436 | println!("<div style=\"page-break-inside: avoid\">"); | |
437 | ||
438 | println!("<p>"); | |
439 | ||
440 | println!("-----BEGIN PROXMOX BACKUP KEY-----"); | |
441 | ||
442 | for line in key_text.lines() { | |
443 | println!("{}", line); | |
444 | } | |
445 | ||
446 | println!("-----END PROXMOX BACKUP KEY-----"); | |
447 | ||
448 | println!("</p>"); | |
449 | ||
c8774067 | 450 | let qr_code = generate_qr_code("svg", key_text.as_bytes())?; |
ef1b4363 DM |
451 | let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD); |
452 | ||
453 | println!("<center>"); | |
454 | println!("<img"); | |
455 | println!("width=\"{}pt\" height=\"{}pt\"", img_size_pt, img_size_pt); | |
c8774067 | 456 | println!("src=\"data:image/svg+xml;base64,{}\"/>", qr_code); |
ef1b4363 DM |
457 | println!("</center>"); |
458 | ||
459 | println!("</div>"); | |
460 | ||
461 | println!("</body>"); | |
462 | println!("</html>"); | |
463 | ||
464 | Ok(()) | |
465 | } | |
466 | ||
467 | fn paperkey_text(data: &str, subject: Option<String>) -> Result<(), Error> { | |
468 | ||
469 | if let Some(subject) = subject { | |
470 | println!("Subject: {}\n", subject); | |
471 | } | |
472 | ||
473 | if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") { | |
b02d49ab DM |
474 | let lines: Vec<String> = data.lines() |
475 | .map(|s| s.trim_end()) | |
476 | .filter(|s| !s.is_empty()) | |
477 | .map(String::from) | |
478 | .collect(); | |
82a0cd2a | 479 | |
b02d49ab DM |
480 | if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") { |
481 | bail!("unexpected key format"); | |
482 | } | |
483 | ||
484 | if lines.len() < 20 { | |
485 | bail!("unexpected key format"); | |
486 | } | |
487 | ||
488 | const BLOCK_SIZE: usize = 5; | |
489 | let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE; | |
490 | ||
491 | for i in 0..blocks { | |
492 | let start = i*BLOCK_SIZE; | |
493 | let mut end = start + BLOCK_SIZE; | |
494 | if end > lines.len() { | |
495 | end = lines.len(); | |
496 | } | |
497 | let data = &lines[start..end]; | |
498 | ||
499 | for l in start..end { | |
ef1b4363 | 500 | println!("{:-2}: {}", l, lines[l]); |
b02d49ab DM |
501 | } |
502 | let data = data.join("\n"); | |
503 | let qr_code = generate_qr_code("utf8i", data.as_bytes())?; | |
ef1b4363 DM |
504 | let qr_code = String::from_utf8(qr_code) |
505 | .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?; | |
b02d49ab DM |
506 | println!("{}", qr_code); |
507 | println!("{}", char::from(12u8)); // page break | |
508 | ||
509 | } | |
510 | return Ok(()); | |
82a0cd2a DM |
511 | } |
512 | ||
b02d49ab DM |
513 | let key_config: KeyConfig = serde_json::from_str(&data)?; |
514 | let key_text = serde_json::to_string_pretty(&key_config)?; | |
82a0cd2a | 515 | |
b02d49ab DM |
516 | println!("-----BEGIN PROXMOX BACKUP KEY-----"); |
517 | println!("{}", key_text); | |
518 | println!("-----END PROXMOX BACKUP KEY-----"); | |
519 | ||
520 | let qr_code = generate_qr_code("utf8i", key_text.as_bytes())?; | |
ef1b4363 DM |
521 | let qr_code = String::from_utf8(qr_code) |
522 | .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?; | |
b02d49ab DM |
523 | |
524 | println!("{}", qr_code); | |
82a0cd2a DM |
525 | |
526 | Ok(()) | |
527 | } | |
528 | ||
ef1b4363 | 529 | fn generate_qr_code(output_type: &str, data: &[u8]) -> Result<Vec<u8>, Error> { |
b02d49ab DM |
530 | |
531 | let mut child = Command::new("qrencode") | |
ef1b4363 | 532 | .args(&["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"]) |
b02d49ab DM |
533 | .stdin(Stdio::piped()) |
534 | .stdout(Stdio::piped()) | |
535 | .spawn()?; | |
536 | ||
537 | { | |
538 | let stdin = child.stdin.as_mut() | |
539 | .ok_or_else(|| format_err!("Failed to open stdin"))?; | |
540 | stdin.write_all(data) | |
541 | .map_err(|_| format_err!("Failed to write to stdin"))?; | |
542 | } | |
543 | ||
544 | let output = child.wait_with_output() | |
545 | .map_err(|_| format_err!("Failed to read stdout"))?; | |
546 | ||
ef1b4363 | 547 | let output = crate::tools::command_output(output, None)?; |
b02d49ab DM |
548 | |
549 | Ok(output) | |
550 | } |