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