]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_backup_client/key.rs
paperkey: use svg as image format to provide better scalability
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
CommitLineData
9696f519 1use std::path::PathBuf;
82a0cd2a
DM
2use std::io::Write;
3use std::process::{Stdio, Command};
9696f519 4
05389a01 5use anyhow::{bail, format_err, Error};
9696f519 6use serde::{Deserialize, Serialize};
9696f519
WB
7
8use proxmox::api::api;
9use proxmox::api::cli::{CliCommand, CliCommandMap};
10use proxmox::sys::linux::tty;
11use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
12
13use proxmox_backup::backup::{
14 encrypt_key_with_passphrase, load_and_decrypt_key, store_key_config, KeyConfig,
15};
16use proxmox_backup::tools;
17
ef1b4363
DM
18#[api()]
19#[derive(Debug, Serialize, Deserialize)]
20#[serde(rename_all = "lowercase")]
21/// Paperkey output format
22pub 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 29pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
05389a01 30pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
b65390eb 31
05389a01
WB
32pub 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
36pub 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 40pub 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 44pub 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
48pub 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
54pub 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.
80pub enum Kdf {
81 /// Do not encrypt the key.
82 None,
83
84 /// Encrypt they key with a password using SCrypt.
85 Scrypt,
86}
87
88impl 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.
111fn 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.
172fn 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.
232fn 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.
251fn 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
299fn 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
326pub 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
352fn 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
467fn 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 529fn 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}