]> git.proxmox.com Git - proxmox-backup.git/blame - pbs-datastore/src/paperkey.rs
moved key_derivation.rs from pbs_datastore to pbs-config/src/key_config.rs
[proxmox-backup.git] / pbs-datastore / src / paperkey.rs
CommitLineData
639a6782 1use std::io::Write;
01c023d5 2use std::process::{Command, Stdio};
639a6782
DM
3
4use anyhow::{bail, format_err, Error};
5use serde::{Deserialize, Serialize};
6
7use proxmox::api::api;
8
bbdda58b 9use pbs_config::key_config::KeyConfig;
639a6782
DM
10
11#[api()]
12#[derive(Debug, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14/// Paperkey output format
15pub enum PaperkeyFormat {
16 /// Format as Utf8 text. Includes QR codes as ascii-art.
17 Text,
18 /// Format as Html. Includes QR codes as SVG images.
19 Html,
20}
21
22/// Generate a paper key (html or utf8 text)
23///
24/// This function takes an encryption key (either RSA private key
25/// text, or `KeyConfig` json), and generates a printable text or html
26/// page, including a scanable QR code to recover the key.
27pub fn generate_paper_key<W: Write>(
28 output: W,
29 data: &str,
30 subject: Option<String>,
31 output_format: Option<PaperkeyFormat>,
32) -> Result<(), Error> {
5dae81d1
FG
33 let (data, is_master_key) = if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n")
34 || data.starts_with("-----BEGIN RSA PRIVATE KEY-----\n")
35 {
36 let data = data.trim_end();
37 if !(data.ends_with("\n-----END ENCRYPTED PRIVATE KEY-----")
38 || data.ends_with("\n-----END RSA PRIVATE KEY-----"))
39 {
40 bail!("unexpected key format");
41 }
639a6782 42
639a6782
DM
43 let lines: Vec<String> = data
44 .lines()
45 .map(|s| s.trim_end())
46 .filter(|s| !s.is_empty())
47 .map(String::from)
48 .collect();
49
639a6782
DM
50 if lines.len() < 20 {
51 bail!("unexpected key format");
52 }
53
54 (lines, true)
55 } else {
56 match serde_json::from_str::<KeyConfig>(&data) {
57 Ok(key_config) => {
58 let lines = serde_json::to_string_pretty(&key_config)?
59 .lines()
60 .map(String::from)
61 .collect();
62
63 (lines, false)
01c023d5 64 }
639a6782
DM
65 Err(err) => {
66 eprintln!("Couldn't parse data as KeyConfig - {}", err);
67 bail!("Neither a PEM-formatted private key, nor a PBS key file.");
01c023d5 68 }
639a6782
DM
69 }
70 };
71
72 let format = output_format.unwrap_or(PaperkeyFormat::Html);
73
74 match format {
5dae81d1
FG
75 PaperkeyFormat::Html => paperkey_html(output, &data, subject, is_master_key),
76 PaperkeyFormat::Text => paperkey_text(output, &data, subject, is_master_key),
639a6782
DM
77 }
78}
79
80fn paperkey_html<W: Write>(
81 mut output: W,
82 lines: &[String],
83 subject: Option<String>,
5dae81d1 84 is_master: bool,
639a6782 85) -> Result<(), Error> {
639a6782
DM
86 let img_size_pt = 500;
87
88 writeln!(output, "<!DOCTYPE html>")?;
89 writeln!(output, "<html lang=\"en\">")?;
90 writeln!(output, "<head>")?;
91 writeln!(output, "<meta charset=\"utf-8\">")?;
01c023d5
FG
92 writeln!(
93 output,
94 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
95 )?;
639a6782
DM
96 writeln!(output, "<title>Proxmox Backup Paperkey</title>")?;
97 writeln!(output, "<style type=\"text/css\">")?;
98
99 writeln!(output, " p {{")?;
100 writeln!(output, " font-size: 12pt;")?;
101 writeln!(output, " font-family: monospace;")?;
102 writeln!(output, " white-space: pre-wrap;")?;
103 writeln!(output, " line-break: anywhere;")?;
104 writeln!(output, " }}")?;
105
106 writeln!(output, "</style>")?;
107
108 writeln!(output, "</head>")?;
109
110 writeln!(output, "<body>")?;
111
112 if let Some(subject) = subject {
113 writeln!(output, "<p>Subject: {}</p>", subject)?;
114 }
115
5dae81d1 116 if is_master {
639a6782 117 const BLOCK_SIZE: usize = 20;
639a6782 118
c2113a40 119 for (block_nr, block) in lines.chunks(BLOCK_SIZE).enumerate() {
01c023d5
FG
120 writeln!(
121 output,
122 "<div style=\"page-break-inside: avoid;page-break-after: always\">"
123 )?;
639a6782
DM
124 writeln!(output, "<p>")?;
125
c2113a40
FG
126 for (i, line) in block.iter().enumerate() {
127 writeln!(output, "{:02}: {}", i + block_nr * BLOCK_SIZE, line)?;
639a6782
DM
128 }
129
130 writeln!(output, "</p>")?;
131
c2113a40 132 let qr_code = generate_qr_code("svg", block)?;
639a6782
DM
133 let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
134
135 writeln!(output, "<center>")?;
136 writeln!(output, "<img")?;
01c023d5
FG
137 writeln!(
138 output,
139 "width=\"{}pt\" height=\"{}pt\"",
140 img_size_pt, img_size_pt
141 )?;
639a6782
DM
142 writeln!(output, "src=\"data:image/svg+xml;base64,{}\"/>", qr_code)?;
143 writeln!(output, "</center>")?;
144 writeln!(output, "</div>")?;
01c023d5 145 }
639a6782
DM
146
147 writeln!(output, "</body>")?;
148 writeln!(output, "</html>")?;
149 return Ok(());
150 }
151
152 writeln!(output, "<div style=\"page-break-inside: avoid\">")?;
153
154 writeln!(output, "<p>")?;
155
156 writeln!(output, "-----BEGIN PROXMOX BACKUP KEY-----")?;
157
158 for line in lines {
159 writeln!(output, "{}", line)?;
160 }
161
162 writeln!(output, "-----END PROXMOX BACKUP KEY-----")?;
163
164 writeln!(output, "</p>")?;
165
166 let qr_code = generate_qr_code("svg", lines)?;
167 let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
168
169 writeln!(output, "<center>")?;
170 writeln!(output, "<img")?;
01c023d5
FG
171 writeln!(
172 output,
173 "width=\"{}pt\" height=\"{}pt\"",
174 img_size_pt, img_size_pt
175 )?;
639a6782
DM
176 writeln!(output, "src=\"data:image/svg+xml;base64,{}\"/>", qr_code)?;
177 writeln!(output, "</center>")?;
178
179 writeln!(output, "</div>")?;
180
181 writeln!(output, "</body>")?;
182 writeln!(output, "</html>")?;
183
184 Ok(())
185}
186
187fn paperkey_text<W: Write>(
188 mut output: W,
189 lines: &[String],
190 subject: Option<String>,
191 is_private: bool,
192) -> Result<(), Error> {
639a6782
DM
193 if let Some(subject) = subject {
194 writeln!(output, "Subject: {}\n", subject)?;
195 }
196
197 if is_private {
198 const BLOCK_SIZE: usize = 5;
639a6782 199
c2113a40
FG
200 for (block_nr, block) in lines.chunks(BLOCK_SIZE).enumerate() {
201 for (i, line) in block.iter().enumerate() {
202 writeln!(output, "{:-2}: {}", i + block_nr * BLOCK_SIZE, line)?;
639a6782 203 }
c2113a40 204 let qr_code = generate_qr_code("utf8i", block)?;
639a6782
DM
205 let qr_code = String::from_utf8(qr_code)
206 .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?;
207 writeln!(output, "{}", qr_code)?;
208 writeln!(output, "{}", char::from(12u8))?; // page break
639a6782
DM
209 }
210 return Ok(());
211 }
212
213 writeln!(output, "-----BEGIN PROXMOX BACKUP KEY-----")?;
214 for line in lines {
215 writeln!(output, "{}", line)?;
216 }
217 writeln!(output, "-----END PROXMOX BACKUP KEY-----")?;
218
219 let qr_code = generate_qr_code("utf8i", &lines)?;
220 let qr_code = String::from_utf8(qr_code)
221 .map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?;
222
223 writeln!(output, "{}", qr_code)?;
224
225 Ok(())
226}
227
228fn generate_qr_code(output_type: &str, lines: &[String]) -> Result<Vec<u8>, Error> {
229 let mut child = Command::new("qrencode")
230 .args(&["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"])
231 .stdin(Stdio::piped())
232 .stdout(Stdio::piped())
233 .spawn()?;
234
235 {
01c023d5
FG
236 let stdin = child
237 .stdin
238 .as_mut()
639a6782
DM
239 .ok_or_else(|| format_err!("Failed to open stdin"))?;
240 let data = lines.join("\n");
01c023d5
FG
241 stdin
242 .write_all(data.as_bytes())
639a6782
DM
243 .map_err(|_| format_err!("Failed to write to stdin"))?;
244 }
245
01c023d5
FG
246 let output = child
247 .wait_with_output()
639a6782
DM
248 .map_err(|_| format_err!("Failed to read stdout"))?;
249
eb5e0ae6 250 let output = pbs_tools::command_output(output, None)?;
639a6782
DM
251
252 Ok(output)
253}