]>
Commit | Line | Data |
---|---|---|
639a6782 | 1 | use std::io::Write; |
01c023d5 | 2 | use std::process::{Command, Stdio}; |
639a6782 DM |
3 | |
4 | use anyhow::{bail, format_err, Error}; | |
5 | use serde::{Deserialize, Serialize}; | |
6 | ||
6ef1b649 | 7 | use proxmox_schema::api; |
639a6782 | 8 | |
1104d2a2 | 9 | use pbs_key_config::KeyConfig; |
639a6782 DM |
10 | |
11 | #[api()] | |
12 | #[derive(Debug, Serialize, Deserialize)] | |
13 | #[serde(rename_all = "lowercase")] | |
14 | /// Paperkey output format | |
15 | pub 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. | |
27 | pub 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 { | |
9a37bd6c | 56 | match serde_json::from_str::<KeyConfig>(data) { |
639a6782 DM |
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 | 65 | Err(err) => { |
dce4b540 | 66 | log::error!("Couldn't parse data as KeyConfig - {}", err); |
639a6782 | 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 | ||
80 | fn 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)?; |
a57413a5 | 133 | let qr_code = base64::encode_config(qr_code, base64::STANDARD_NO_PAD); |
639a6782 DM |
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)?; | |
a57413a5 | 167 | let qr_code = base64::encode_config(qr_code, base64::STANDARD_NO_PAD); |
639a6782 DM |
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 | ||
187 | fn 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 | ||
9a37bd6c | 219 | let qr_code = generate_qr_code("utf8i", lines)?; |
639a6782 DM |
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 | ||
228 | fn generate_qr_code(output_type: &str, lines: &[String]) -> Result<Vec<u8>, Error> { | |
229 | let mut child = Command::new("qrencode") | |
16f6766a | 230 | .args(["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"]) |
639a6782 DM |
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 | ||
25877d05 | 250 | let output = proxmox_sys::command::command_output(output, None)?; |
639a6782 DM |
251 | |
252 | Ok(output) | |
253 | } |