]>
Commit | Line | Data |
---|---|---|
42c0f784 | 1 | use std::convert::TryFrom; |
2924b37d | 2 | use std::path::PathBuf; |
9696f519 | 3 | |
05389a01 | 4 | use anyhow::{bail, format_err, Error}; |
dfb04575 | 5 | use serde_json::Value; |
9696f519 | 6 | |
6ef1b649 WB |
7 | use proxmox::sys::linux::tty; |
8 | use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions}; | |
9 | use proxmox_router::cli::{ | |
2924b37d | 10 | format_and_print_result_full, get_output_format, CliCommand, CliCommandMap, ColumnConfig, |
dfb04575 FG |
11 | OUTPUT_FORMAT, |
12 | }; | |
6ef1b649 | 13 | use proxmox_schema::{api, ApiType, ReturnType}; |
9696f519 | 14 | |
ed208076 | 15 | use pbs_api_types::{RsaPubKeyInfo, PASSWORD_HINT_SCHEMA, Kdf, KeyInfo}; |
bbdda58b | 16 | use pbs_config::key_config::{KeyConfig, rsa_decrypt_key_config}; |
eb5e0ae6 | 17 | use pbs_datastore::paperkey::{generate_paper_key, PaperkeyFormat}; |
2b7f8dd5 WB |
18 | use pbs_client::tools::key_source::{ |
19 | find_default_encryption_key, find_default_master_pubkey, get_encryption_key_password, | |
20 | place_default_encryption_key, place_default_master_pubkey, | |
21 | }; | |
22 | ||
9696f519 WB |
23 | #[api( |
24 | input: { | |
25 | properties: { | |
26 | kdf: { | |
27 | type: Kdf, | |
28 | optional: true, | |
29 | }, | |
30 | path: { | |
31 | description: | |
32 | "Output file. Without this the key will become the new default encryption key.", | |
33 | optional: true, | |
82a103c8 DM |
34 | }, |
35 | hint: { | |
36 | schema: PASSWORD_HINT_SCHEMA, | |
37 | optional: true, | |
38 | }, | |
9696f519 WB |
39 | }, |
40 | }, | |
41 | )] | |
42 | /// Create a new encryption key. | |
2924b37d | 43 | fn create(kdf: Option<Kdf>, path: Option<String>, hint: Option<String>) -> Result<(), Error> { |
9696f519 WB |
44 | let path = match path { |
45 | Some(path) => PathBuf::from(path), | |
0eaef8eb WB |
46 | None => { |
47 | let path = place_default_encryption_key()?; | |
48 | println!("creating default key at: {:?}", path); | |
49 | path | |
50 | } | |
9696f519 WB |
51 | }; |
52 | ||
53 | let kdf = kdf.unwrap_or_default(); | |
54 | ||
9a045790 DM |
55 | let mut key = [0u8; 32]; |
56 | proxmox::sys::linux::fill_with_random_data(&mut key)?; | |
9696f519 WB |
57 | |
58 | match kdf { | |
59 | Kdf::None => { | |
82a103c8 DM |
60 | if hint.is_some() { |
61 | bail!("password hint not allowed for Kdf::None"); | |
62 | } | |
63 | ||
1c86893d | 64 | let key_config = KeyConfig::without_password(key)?; |
9a045790 DM |
65 | |
66 | key_config.store(path, false)?; | |
9696f519 | 67 | } |
5e17dbf2 | 68 | Kdf::Scrypt | Kdf::PBKDF2 => { |
9696f519 WB |
69 | // always read passphrase from tty |
70 | if !tty::stdin_isatty() { | |
71 | bail!("unable to read passphrase - no tty"); | |
72 | } | |
73 | ||
74 | let password = tty::read_and_verify_password("Encryption Key Password: ")?; | |
75 | ||
9a045790 | 76 | let mut key_config = KeyConfig::with_key(&key, &password, kdf)?; |
82a103c8 | 77 | key_config.hint = hint; |
9696f519 | 78 | |
9a045790 | 79 | key_config.store(&path, false)?; |
9696f519 WB |
80 | } |
81 | } | |
82 | ||
83 | Ok(()) | |
84 | } | |
85 | ||
7137630d FG |
86 | #[api( |
87 | input: { | |
88 | properties: { | |
89 | "master-keyfile": { | |
90 | description: "(Private) master key to use.", | |
91 | }, | |
92 | "encrypted-keyfile": { | |
93 | description: "RSA-encrypted keyfile to import.", | |
94 | }, | |
95 | kdf: { | |
96 | type: Kdf, | |
97 | optional: true, | |
98 | }, | |
99 | "path": { | |
100 | description: | |
101 | "Output file. Without this the key will become the new default encryption key.", | |
102 | optional: true, | |
82a103c8 DM |
103 | }, |
104 | hint: { | |
105 | schema: PASSWORD_HINT_SCHEMA, | |
106 | optional: true, | |
107 | }, | |
7137630d FG |
108 | }, |
109 | }, | |
110 | )] | |
111 | /// Import an encrypted backup of an encryption key using a (private) master key. | |
112 | async fn import_with_master_key( | |
113 | master_keyfile: String, | |
114 | encrypted_keyfile: String, | |
115 | kdf: Option<Kdf>, | |
116 | path: Option<String>, | |
82a103c8 | 117 | hint: Option<String>, |
7137630d FG |
118 | ) -> Result<(), Error> { |
119 | let path = match path { | |
120 | Some(path) => PathBuf::from(path), | |
121 | None => { | |
122 | let path = place_default_encryption_key()?; | |
123 | if path.exists() { | |
124 | bail!("Please remove default encryption key at {:?} before importing to default location (or choose a non-default one).", path); | |
125 | } | |
126 | println!("Importing key to default location at: {:?}", path); | |
127 | path | |
128 | } | |
129 | }; | |
130 | ||
131 | let encrypted_key = file_get_contents(&encrypted_keyfile)?; | |
132 | let master_key = file_get_contents(&master_keyfile)?; | |
133 | let password = tty::read_password("Master Key Password: ")?; | |
134 | ||
2924b37d | 135 | let master_key = openssl::pkey::PKey::private_key_from_pem_passphrase(&master_key, &password) |
7137630d FG |
136 | .map_err(|err| format_err!("failed to read PEM-formatted private key - {}", err))? |
137 | .rsa() | |
138 | .map_err(|err| format_err!("not a valid private RSA key - {}", err))?; | |
139 | ||
1c86893d | 140 | let (key, created, _fingerprint) = |
7137630d FG |
141 | rsa_decrypt_key_config(master_key, &encrypted_key, &get_encryption_key_password)?; |
142 | ||
143 | let kdf = kdf.unwrap_or_default(); | |
144 | match kdf { | |
145 | Kdf::None => { | |
82a103c8 DM |
146 | if hint.is_some() { |
147 | bail!("password hint not allowed for Kdf::None"); | |
148 | } | |
149 | ||
1c86893d | 150 | let mut key_config = KeyConfig::without_password(key)?; |
9a045790 | 151 | key_config.created = created; // keep original value |
9a045790 DM |
152 | |
153 | key_config.store(path, true)?; | |
7137630d FG |
154 | } |
155 | Kdf::Scrypt | Kdf::PBKDF2 => { | |
156 | let password = tty::read_and_verify_password("New Password: ")?; | |
157 | ||
9a045790 | 158 | let mut new_key_config = KeyConfig::with_key(&key, &password, kdf)?; |
7137630d | 159 | new_key_config.created = created; // keep original value |
82a103c8 | 160 | new_key_config.hint = hint; |
7137630d | 161 | |
9a045790 | 162 | new_key_config.store(path, true)?; |
7137630d FG |
163 | } |
164 | } | |
165 | ||
166 | Ok(()) | |
167 | } | |
168 | ||
9696f519 WB |
169 | #[api( |
170 | input: { | |
171 | properties: { | |
172 | kdf: { | |
173 | type: Kdf, | |
174 | optional: true, | |
175 | }, | |
176 | path: { | |
177 | description: "Key file. Without this the default key's password will be changed.", | |
178 | optional: true, | |
82a103c8 DM |
179 | }, |
180 | hint: { | |
181 | schema: PASSWORD_HINT_SCHEMA, | |
182 | optional: true, | |
183 | }, | |
9696f519 WB |
184 | }, |
185 | }, | |
186 | )] | |
187 | /// Change the encryption key's password. | |
82a103c8 DM |
188 | fn change_passphrase( |
189 | kdf: Option<Kdf>, | |
190 | path: Option<String>, | |
191 | hint: Option<String>, | |
192 | ) -> Result<(), Error> { | |
9696f519 WB |
193 | let path = match path { |
194 | Some(path) => PathBuf::from(path), | |
0eaef8eb | 195 | None => { |
2924b37d FG |
196 | let path = find_default_encryption_key()?.ok_or_else(|| { |
197 | format_err!("no encryption file provided and no default file found") | |
198 | })?; | |
0eaef8eb WB |
199 | println!("updating default key at: {:?}", path); |
200 | path | |
201 | } | |
9696f519 WB |
202 | }; |
203 | ||
204 | let kdf = kdf.unwrap_or_default(); | |
205 | ||
206 | if !tty::stdin_isatty() { | |
207 | bail!("unable to change passphrase - no tty"); | |
208 | } | |
209 | ||
9a045790 | 210 | let key_config = KeyConfig::load(&path)?; |
1c86893d | 211 | let (key, created, _fingerprint) = key_config.decrypt(&get_encryption_key_password)?; |
9696f519 WB |
212 | |
213 | match kdf { | |
214 | Kdf::None => { | |
82a103c8 DM |
215 | if hint.is_some() { |
216 | bail!("password hint not allowed for Kdf::None"); | |
217 | } | |
218 | ||
1c86893d | 219 | let mut key_config = KeyConfig::without_password(key)?; |
2924b37d | 220 | key_config.created = created; // keep original value |
9a045790 DM |
221 | |
222 | key_config.store(&path, true)?; | |
9696f519 | 223 | } |
5e17dbf2 | 224 | Kdf::Scrypt | Kdf::PBKDF2 => { |
9696f519 WB |
225 | let password = tty::read_and_verify_password("New Password: ")?; |
226 | ||
9a045790 | 227 | let mut new_key_config = KeyConfig::with_key(&key, &password, kdf)?; |
9696f519 | 228 | new_key_config.created = created; // keep original value |
82a103c8 | 229 | new_key_config.hint = hint; |
9a045790 DM |
230 | |
231 | new_key_config.store(&path, true)?; | |
9696f519 WB |
232 | } |
233 | } | |
234 | ||
235 | Ok(()) | |
236 | } | |
237 | ||
dfb04575 FG |
238 | #[api( |
239 | input: { | |
240 | properties: { | |
241 | path: { | |
242 | description: "Key file. Without this the default key's metadata will be shown.", | |
243 | optional: true, | |
244 | }, | |
245 | "output-format": { | |
246 | schema: OUTPUT_FORMAT, | |
247 | optional: true, | |
248 | }, | |
249 | }, | |
250 | }, | |
251 | )] | |
252 | /// Print the encryption key's metadata. | |
4d104cd4 | 253 | fn show_key(path: Option<String>, param: Value) -> Result<(), Error> { |
dfb04575 FG |
254 | let path = match path { |
255 | Some(path) => PathBuf::from(path), | |
4d104cd4 FG |
256 | None => find_default_encryption_key()? |
257 | .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?, | |
dfb04575 FG |
258 | }; |
259 | ||
dfb04575 FG |
260 | let config: KeyConfig = serde_json::from_slice(&file_get_contents(path.clone())?)?; |
261 | ||
5e17dbf2 DM |
262 | let output_format = get_output_format(¶m); |
263 | ||
69b8bc3b DM |
264 | let mut info: KeyInfo = (&config).into(); |
265 | info.path = Some(format!("{:?}", path)); | |
5e17dbf2 | 266 | |
6ef1b649 | 267 | let options = proxmox_router::cli::default_table_format_options() |
5e17dbf2 DM |
268 | .column(ColumnConfig::new("path")) |
269 | .column(ColumnConfig::new("kdf")) | |
770a36e5 WB |
270 | .column(ColumnConfig::new("created").renderer(pbs_tools::format::render_epoch)) |
271 | .column(ColumnConfig::new("modified").renderer(pbs_tools::format::render_epoch)) | |
82a103c8 DM |
272 | .column(ColumnConfig::new("fingerprint")) |
273 | .column(ColumnConfig::new("hint")); | |
5e17dbf2 | 274 | |
b2362a12 | 275 | let return_type = ReturnType::new(false, &KeyInfo::API_SCHEMA); |
5e17dbf2 | 276 | |
b2362a12 WB |
277 | format_and_print_result_full( |
278 | &mut serde_json::to_value(info)?, | |
279 | &return_type, | |
280 | &output_format, | |
281 | &options, | |
282 | ); | |
dfb04575 FG |
283 | |
284 | Ok(()) | |
285 | } | |
286 | ||
9696f519 WB |
287 | #[api( |
288 | input: { | |
289 | properties: { | |
290 | path: { | |
291 | description: "Path to the PEM formatted RSA public key.", | |
292 | }, | |
293 | }, | |
294 | }, | |
295 | )] | |
296 | /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption | |
297 | /// key onto the backup server along with each backup. | |
05f17d1e FG |
298 | /// |
299 | /// The imported key will be used as default master key for future invocations by the same local | |
300 | /// user. | |
9696f519 WB |
301 | fn import_master_pubkey(path: String) -> Result<(), Error> { |
302 | let pem_data = file_get_contents(&path)?; | |
303 | ||
42c0f784 FG |
304 | match openssl::pkey::PKey::public_key_from_pem(&pem_data) { |
305 | Ok(key) => { | |
306 | let info = RsaPubKeyInfo::try_from(key.rsa()?)?; | |
307 | println!("Found following key at {:?}", path); | |
308 | println!("Modulus: {}", info.modulus); | |
309 | println!("Exponent: {}", info.exponent); | |
310 | println!("Length: {}", info.length); | |
2924b37d | 311 | } |
42c0f784 FG |
312 | Err(err) => bail!("Unable to decode PEM data - {}", err), |
313 | }; | |
9696f519 | 314 | |
05f17d1e | 315 | let target_path = place_default_master_pubkey()?; |
9696f519 WB |
316 | |
317 | replace_file(&target_path, &pem_data, CreateOptions::new())?; | |
318 | ||
319 | println!("Imported public master key to {:?}", target_path); | |
320 | ||
321 | Ok(()) | |
322 | } | |
323 | ||
324 | #[api] | |
325 | /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup | |
326 | /// encryption key onto the backup server along with each backup. | |
327 | fn create_master_key() -> Result<(), Error> { | |
328 | // we need a TTY to query the new password | |
329 | if !tty::stdin_isatty() { | |
330 | bail!("unable to create master key - no tty"); | |
331 | } | |
332 | ||
42c0f784 FG |
333 | let bits = 4096; |
334 | println!("Generating {}-bit RSA key..", bits); | |
335 | let rsa = openssl::rsa::Rsa::generate(bits)?; | |
2924b37d FG |
336 | let public = |
337 | openssl::rsa::Rsa::from_public_components(rsa.n().to_owned()?, rsa.e().to_owned()?)?; | |
42c0f784 FG |
338 | let info = RsaPubKeyInfo::try_from(public)?; |
339 | println!("Modulus: {}", info.modulus); | |
340 | println!("Exponent: {}", info.exponent); | |
341 | println!(); | |
342 | ||
9696f519 WB |
343 | let pkey = openssl::pkey::PKey::from_rsa(rsa)?; |
344 | ||
345 | let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?; | |
346 | ||
347 | let pub_key: Vec<u8> = pkey.public_key_to_pem()?; | |
348 | let filename_pub = "master-public.pem"; | |
349 | println!("Writing public master key to {}", filename_pub); | |
350 | replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?; | |
351 | ||
352 | let cipher = openssl::symm::Cipher::aes_256_cbc(); | |
2924b37d FG |
353 | let priv_key: Vec<u8> = |
354 | pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?; | |
9696f519 WB |
355 | |
356 | let filename_priv = "master-private.pem"; | |
357 | println!("Writing private master key to {}", filename_priv); | |
358 | replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?; | |
359 | ||
360 | Ok(()) | |
361 | } | |
362 | ||
42c0f784 FG |
363 | #[api( |
364 | input: { | |
365 | properties: { | |
366 | path: { | |
367 | description: "Path to the PEM formatted RSA public key. Default location will be used if not specified.", | |
368 | optional: true, | |
369 | }, | |
370 | "output-format": { | |
371 | schema: OUTPUT_FORMAT, | |
372 | optional: true, | |
373 | }, | |
374 | }, | |
375 | }, | |
376 | )] | |
377 | /// List information about master key | |
378 | fn show_master_pubkey(path: Option<String>, param: Value) -> Result<(), Error> { | |
379 | let path = match path { | |
380 | Some(path) => PathBuf::from(path), | |
381 | None => find_default_master_pubkey()? | |
382 | .ok_or_else(|| format_err!("No path specified and no default master key available."))?, | |
383 | }; | |
384 | ||
385 | let path = path.canonicalize()?; | |
386 | ||
387 | let output_format = get_output_format(¶m); | |
388 | ||
389 | let pem_data = file_get_contents(path.clone())?; | |
390 | let rsa = openssl::rsa::Rsa::public_key_from_pem(&pem_data)?; | |
391 | ||
392 | let mut info = RsaPubKeyInfo::try_from(rsa)?; | |
393 | info.path = Some(path.display().to_string()); | |
394 | ||
6ef1b649 | 395 | let options = proxmox_router::cli::default_table_format_options() |
42c0f784 FG |
396 | .column(ColumnConfig::new("path")) |
397 | .column(ColumnConfig::new("modulus")) | |
398 | .column(ColumnConfig::new("exponent")) | |
399 | .column(ColumnConfig::new("length")); | |
400 | ||
401 | let return_type = ReturnType::new(false, &RsaPubKeyInfo::API_SCHEMA); | |
402 | ||
403 | format_and_print_result_full( | |
404 | &mut serde_json::to_value(info)?, | |
405 | &return_type, | |
406 | &output_format, | |
407 | &options, | |
408 | ); | |
409 | ||
410 | Ok(()) | |
411 | } | |
412 | ||
82a0cd2a DM |
413 | #[api( |
414 | input: { | |
415 | properties: { | |
416 | path: { | |
417 | description: "Key file. Without this the default key's will be used.", | |
418 | optional: true, | |
419 | }, | |
420 | subject: { | |
d1d74c43 | 421 | description: "Include the specified subject as title text.", |
82a0cd2a DM |
422 | optional: true, |
423 | }, | |
ef1b4363 DM |
424 | "output-format": { |
425 | type: PaperkeyFormat, | |
ef1b4363 DM |
426 | optional: true, |
427 | }, | |
82a0cd2a DM |
428 | }, |
429 | }, | |
430 | )] | |
431 | /// Generate a printable, human readable text file containing the encryption key. | |
432 | /// | |
433 | /// This also includes a scanable QR code for fast key restore. | |
ef1b4363 DM |
434 | fn paper_key( |
435 | path: Option<String>, | |
436 | subject: Option<String>, | |
437 | output_format: Option<PaperkeyFormat>, | |
438 | ) -> Result<(), Error> { | |
82a0cd2a DM |
439 | let path = match path { |
440 | Some(path) => PathBuf::from(path), | |
4d104cd4 FG |
441 | None => find_default_encryption_key()? |
442 | .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?, | |
82a0cd2a DM |
443 | }; |
444 | ||
445 | let data = file_get_contents(&path)?; | |
f1e29041 FG |
446 | let data = String::from_utf8(data)?; |
447 | ||
639a6782 | 448 | generate_paper_key(std::io::stdout(), &data, subject, output_format) |
ef1b4363 DM |
449 | } |
450 | ||
451 | pub fn cli() -> CliCommandMap { | |
452 | let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE) | |
453 | .arg_param(&["path"]) | |
2b7f8dd5 | 454 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
ef1b4363 | 455 | |
7137630d FG |
456 | let key_import_with_master_key_cmd_def = CliCommand::new(&API_METHOD_IMPORT_WITH_MASTER_KEY) |
457 | .arg_param(&["master-keyfile"]) | |
2b7f8dd5 | 458 | .completion_cb("master-keyfile", pbs_tools::fs::complete_file_name) |
7137630d | 459 | .arg_param(&["encrypted-keyfile"]) |
2b7f8dd5 | 460 | .completion_cb("encrypted-keyfile", pbs_tools::fs::complete_file_name) |
7137630d | 461 | .arg_param(&["path"]) |
2b7f8dd5 | 462 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
7137630d | 463 | |
ef1b4363 DM |
464 | let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE) |
465 | .arg_param(&["path"]) | |
2b7f8dd5 | 466 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
ef1b4363 DM |
467 | |
468 | let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY); | |
469 | let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY) | |
470 | .arg_param(&["path"]) | |
2b7f8dd5 | 471 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
42c0f784 FG |
472 | let key_show_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_SHOW_MASTER_PUBKEY) |
473 | .arg_param(&["path"]) | |
2b7f8dd5 | 474 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
ef1b4363 | 475 | |
dfb04575 FG |
476 | let key_show_cmd_def = CliCommand::new(&API_METHOD_SHOW_KEY) |
477 | .arg_param(&["path"]) | |
2b7f8dd5 | 478 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
dfb04575 | 479 | |
ef1b4363 DM |
480 | let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY) |
481 | .arg_param(&["path"]) | |
2b7f8dd5 | 482 | .completion_cb("path", pbs_tools::fs::complete_file_name); |
ef1b4363 DM |
483 | |
484 | CliCommandMap::new() | |
485 | .insert("create", key_create_cmd_def) | |
7137630d | 486 | .insert("import-with-master-key", key_import_with_master_key_cmd_def) |
ef1b4363 DM |
487 | .insert("create-master-key", key_create_master_key_cmd_def) |
488 | .insert("import-master-pubkey", key_import_master_pubkey_cmd_def) | |
489 | .insert("change-passphrase", key_change_passphrase_cmd_def) | |
dfb04575 | 490 | .insert("show", key_show_cmd_def) |
42c0f784 | 491 | .insert("show-master-pubkey", key_show_master_pubkey_cmd_def) |
fdc00811 | 492 | .insert("paperkey", paper_key_cmd_def) |
ef1b4363 | 493 | } |