proxmox-backup/src/tools/paperkey.rs

254 lines
7.4 KiB
Rust

use std::io::Write;
use std::process::{Command, Stdio};
use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use proxmox::api::api;
use crate::backup::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<W: Write>(
output: W,
data: &str,
subject: Option<String>,
output_format: Option<PaperkeyFormat>,
) -> 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<String> = 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::<KeyConfig>(&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<W: Write>(
mut output: W,
lines: &[String],
subject: Option<String>,
is_master: bool,
) -> Result<(), Error> {
let img_size_pt = 500;
writeln!(output, "<!DOCTYPE html>")?;
writeln!(output, "<html lang=\"en\">")?;
writeln!(output, "<head>")?;
writeln!(output, "<meta charset=\"utf-8\">")?;
writeln!(
output,
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
)?;
writeln!(output, "<title>Proxmox Backup Paperkey</title>")?;
writeln!(output, "<style type=\"text/css\">")?;
writeln!(output, " p {{")?;
writeln!(output, " font-size: 12pt;")?;
writeln!(output, " font-family: monospace;")?;
writeln!(output, " white-space: pre-wrap;")?;
writeln!(output, " line-break: anywhere;")?;
writeln!(output, " }}")?;
writeln!(output, "</style>")?;
writeln!(output, "</head>")?;
writeln!(output, "<body>")?;
if let Some(subject) = subject {
writeln!(output, "<p>Subject: {}</p>", subject)?;
}
if is_master {
const BLOCK_SIZE: usize = 20;
for (block_nr, block) in lines.chunks(BLOCK_SIZE).enumerate() {
writeln!(
output,
"<div style=\"page-break-inside: avoid;page-break-after: always\">"
)?;
writeln!(output, "<p>")?;
for (i, line) in block.iter().enumerate() {
writeln!(output, "{:02}: {}", i + block_nr * BLOCK_SIZE, line)?;
}
writeln!(output, "</p>")?;
let qr_code = generate_qr_code("svg", block)?;
let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
writeln!(output, "<center>")?;
writeln!(output, "<img")?;
writeln!(
output,
"width=\"{}pt\" height=\"{}pt\"",
img_size_pt, img_size_pt
)?;
writeln!(output, "src=\"data:image/svg+xml;base64,{}\"/>", qr_code)?;
writeln!(output, "</center>")?;
writeln!(output, "</div>")?;
}
writeln!(output, "</body>")?;
writeln!(output, "</html>")?;
return Ok(());
}
writeln!(output, "<div style=\"page-break-inside: avoid\">")?;
writeln!(output, "<p>")?;
writeln!(output, "-----BEGIN PROXMOX BACKUP KEY-----")?;
for line in lines {
writeln!(output, "{}", line)?;
}
writeln!(output, "-----END PROXMOX BACKUP KEY-----")?;
writeln!(output, "</p>")?;
let qr_code = generate_qr_code("svg", lines)?;
let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
writeln!(output, "<center>")?;
writeln!(output, "<img")?;
writeln!(
output,
"width=\"{}pt\" height=\"{}pt\"",
img_size_pt, img_size_pt
)?;
writeln!(output, "src=\"data:image/svg+xml;base64,{}\"/>", qr_code)?;
writeln!(output, "</center>")?;
writeln!(output, "</div>")?;
writeln!(output, "</body>")?;
writeln!(output, "</html>")?;
Ok(())
}
fn paperkey_text<W: Write>(
mut output: W,
lines: &[String],
subject: Option<String>,
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<Vec<u8>, 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 = crate::tools::command_output(output, None)?;
Ok(output)
}