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