use std::io::Write; use std::process::{Command, Stdio}; use anyhow::{bail, format_err, Error}; use serde::{Deserialize, Serialize}; use proxmox::api::api; use pbs_config::key_config::KeyConfig; #[api()] #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] /// Paperkey output format pub enum PaperkeyFormat { /// Format as Utf8 text. Includes QR codes as ascii-art. Text, /// Format as Html. Includes QR codes as SVG images. Html, } /// Generate a paper key (html or utf8 text) /// /// This function takes an encryption key (either RSA private key /// text, or `KeyConfig` json), and generates a printable text or html /// page, including a scanable QR code to recover the key. pub fn generate_paper_key( output: W, data: &str, subject: Option, output_format: Option, ) -> Result<(), Error> { let (data, is_master_key) = if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") || data.starts_with("-----BEGIN RSA PRIVATE KEY-----\n") { let data = data.trim_end(); if !(data.ends_with("\n-----END ENCRYPTED PRIVATE KEY-----") || data.ends_with("\n-----END RSA PRIVATE KEY-----")) { bail!("unexpected key format"); } let lines: Vec = data .lines() .map(|s| s.trim_end()) .filter(|s| !s.is_empty()) .map(String::from) .collect(); if lines.len() < 20 { bail!("unexpected key format"); } (lines, true) } else { match serde_json::from_str::(&data) { Ok(key_config) => { let lines = serde_json::to_string_pretty(&key_config)? .lines() .map(String::from) .collect(); (lines, false) } Err(err) => { eprintln!("Couldn't parse data as KeyConfig - {}", err); bail!("Neither a PEM-formatted private key, nor a PBS key file."); } } }; let format = output_format.unwrap_or(PaperkeyFormat::Html); match format { PaperkeyFormat::Html => paperkey_html(output, &data, subject, is_master_key), PaperkeyFormat::Text => paperkey_text(output, &data, subject, is_master_key), } } fn paperkey_html( mut output: W, lines: &[String], subject: Option, is_master: bool, ) -> Result<(), Error> { let img_size_pt = 500; writeln!(output, "")?; writeln!(output, "")?; writeln!(output, "")?; writeln!(output, "")?; writeln!( output, "" )?; writeln!(output, "Proxmox Backup Paperkey")?; writeln!(output, "")?; writeln!(output, "")?; writeln!(output, "")?; if let Some(subject) = subject { writeln!(output, "

Subject: {}

", subject)?; } if is_master { const BLOCK_SIZE: usize = 20; for (block_nr, block) in lines.chunks(BLOCK_SIZE).enumerate() { writeln!( output, "
" )?; writeln!(output, "

")?; for (i, line) in block.iter().enumerate() { writeln!(output, "{:02}: {}", i + block_nr * BLOCK_SIZE, line)?; } writeln!(output, "

")?; let qr_code = generate_qr_code("svg", block)?; let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD); writeln!(output, "
")?; writeln!(output, "", qr_code)?; writeln!(output, "
")?; writeln!(output, "
")?; } writeln!(output, "")?; writeln!(output, "")?; return Ok(()); } writeln!(output, "
")?; writeln!(output, "

")?; writeln!(output, "-----BEGIN PROXMOX BACKUP KEY-----")?; for line in lines { writeln!(output, "{}", line)?; } writeln!(output, "-----END PROXMOX BACKUP KEY-----")?; writeln!(output, "

")?; let qr_code = generate_qr_code("svg", lines)?; let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD); writeln!(output, "
")?; writeln!(output, "", qr_code)?; writeln!(output, "
")?; writeln!(output, "
")?; writeln!(output, "")?; writeln!(output, "")?; Ok(()) } fn paperkey_text( mut output: W, lines: &[String], subject: Option, is_private: bool, ) -> Result<(), Error> { if let Some(subject) = subject { writeln!(output, "Subject: {}\n", subject)?; } if is_private { const BLOCK_SIZE: usize = 5; for (block_nr, block) in lines.chunks(BLOCK_SIZE).enumerate() { for (i, line) in block.iter().enumerate() { writeln!(output, "{:-2}: {}", i + block_nr * BLOCK_SIZE, line)?; } let qr_code = generate_qr_code("utf8i", block)?; let qr_code = String::from_utf8(qr_code) .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?; writeln!(output, "{}", qr_code)?; writeln!(output, "{}", char::from(12u8))?; // page break } return Ok(()); } writeln!(output, "-----BEGIN PROXMOX BACKUP KEY-----")?; for line in lines { writeln!(output, "{}", line)?; } writeln!(output, "-----END PROXMOX BACKUP KEY-----")?; let qr_code = generate_qr_code("utf8i", &lines)?; let qr_code = String::from_utf8(qr_code) .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?; writeln!(output, "{}", qr_code)?; Ok(()) } fn generate_qr_code(output_type: &str, lines: &[String]) -> Result, Error> { let mut child = Command::new("qrencode") .args(&["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn()?; { let stdin = child .stdin .as_mut() .ok_or_else(|| format_err!("Failed to open stdin"))?; let data = lines.join("\n"); stdin .write_all(data.as_bytes()) .map_err(|_| format_err!("Failed to write to stdin"))?; } let output = child .wait_with_output() .map_err(|_| format_err!("Failed to read stdout"))?; let output = pbs_tools::command_output(output, None)?; Ok(output) }