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