]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
typo fixes all over the place
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
1 use std::convert::TryFrom;
2 use std::path::PathBuf;
3
4 use anyhow::{bail, format_err, Error};
5 use serde_json::Value;
6
7 use proxmox::api::api;
8 use proxmox::api::cli::{
9 format_and_print_result_full, get_output_format, CliCommand, CliCommandMap, ColumnConfig,
10 OUTPUT_FORMAT,
11 };
12 use proxmox::api::router::ReturnType;
13 use proxmox::sys::linux::tty;
14 use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
15
16 use proxmox_backup::{
17 api2::types::{Kdf, KeyInfo, RsaPubKeyInfo, PASSWORD_HINT_SCHEMA},
18 backup::{rsa_decrypt_key_config, KeyConfig},
19 tools,
20 tools::paperkey::{generate_paper_key, PaperkeyFormat},
21 };
22
23 use crate::KeyWithSource;
24
25 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
26 pub const DEFAULT_MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
27
28 pub fn find_default_master_pubkey() -> Result<Option<PathBuf>, Error> {
29 super::find_xdg_file(
30 DEFAULT_MASTER_PUBKEY_FILE_NAME,
31 "default master public key file",
32 )
33 }
34
35 pub fn place_default_master_pubkey() -> Result<PathBuf, Error> {
36 super::place_xdg_file(
37 DEFAULT_MASTER_PUBKEY_FILE_NAME,
38 "default master public key file",
39 )
40 }
41
42 pub fn find_default_encryption_key() -> Result<Option<PathBuf>, Error> {
43 super::find_xdg_file(
44 DEFAULT_ENCRYPTION_KEY_FILE_NAME,
45 "default encryption key file",
46 )
47 }
48
49 pub fn place_default_encryption_key() -> Result<PathBuf, Error> {
50 super::place_xdg_file(
51 DEFAULT_ENCRYPTION_KEY_FILE_NAME,
52 "default encryption key file",
53 )
54 }
55
56 #[cfg(not(test))]
57 pub(crate) fn read_optional_default_encryption_key() -> Result<Option<KeyWithSource>, Error> {
58 find_default_encryption_key()?
59 .map(|path| file_get_contents(path).map(KeyWithSource::from_default))
60 .transpose()
61 }
62
63 #[cfg(not(test))]
64 pub(crate) fn read_optional_default_master_pubkey() -> Result<Option<KeyWithSource>, Error> {
65 find_default_master_pubkey()?
66 .map(|path| file_get_contents(path).map(KeyWithSource::from_default))
67 .transpose()
68 }
69
70 #[cfg(test)]
71 static mut TEST_DEFAULT_ENCRYPTION_KEY: Result<Option<Vec<u8>>, Error> = Ok(None);
72
73 #[cfg(test)]
74 pub(crate) fn read_optional_default_encryption_key() -> Result<Option<KeyWithSource>, Error> {
75 // not safe when multiple concurrent test cases end up here!
76 unsafe {
77 match &TEST_DEFAULT_ENCRYPTION_KEY {
78 Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))),
79 Ok(None) => Ok(None),
80 Err(_) => bail!("test error"),
81 }
82 }
83 }
84
85 #[cfg(test)]
86 // not safe when multiple concurrent test cases end up here!
87 pub(crate) unsafe fn set_test_encryption_key(value: Result<Option<Vec<u8>>, Error>) {
88 TEST_DEFAULT_ENCRYPTION_KEY = value;
89 }
90
91 #[cfg(test)]
92 static mut TEST_DEFAULT_MASTER_PUBKEY: Result<Option<Vec<u8>>, Error> = Ok(None);
93
94 #[cfg(test)]
95 pub(crate) fn read_optional_default_master_pubkey() -> Result<Option<KeyWithSource>, Error> {
96 // not safe when multiple concurrent test cases end up here!
97 unsafe {
98 match &TEST_DEFAULT_MASTER_PUBKEY {
99 Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))),
100 Ok(None) => Ok(None),
101 Err(_) => bail!("test error"),
102 }
103 }
104 }
105
106 #[cfg(test)]
107 // not safe when multiple concurrent test cases end up here!
108 pub(crate) unsafe fn set_test_default_master_pubkey(value: Result<Option<Vec<u8>>, Error>) {
109 TEST_DEFAULT_MASTER_PUBKEY = value;
110 }
111
112 pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
113 // fixme: implement other input methods
114
115 use std::env::VarError::*;
116 match std::env::var("PBS_ENCRYPTION_PASSWORD") {
117 Ok(p) => return Ok(p.as_bytes().to_vec()),
118 Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
119 Err(NotPresent) => {
120 // Try another method
121 }
122 }
123
124 // If we're on a TTY, query the user for a password
125 if tty::stdin_isatty() {
126 return Ok(tty::read_password("Encryption Key Password: ")?);
127 }
128
129 bail!("no password input mechanism available");
130 }
131
132 #[api(
133 input: {
134 properties: {
135 kdf: {
136 type: Kdf,
137 optional: true,
138 },
139 path: {
140 description:
141 "Output file. Without this the key will become the new default encryption key.",
142 optional: true,
143 },
144 hint: {
145 schema: PASSWORD_HINT_SCHEMA,
146 optional: true,
147 },
148 },
149 },
150 )]
151 /// Create a new encryption key.
152 fn create(kdf: Option<Kdf>, path: Option<String>, hint: Option<String>) -> Result<(), Error> {
153 let path = match path {
154 Some(path) => PathBuf::from(path),
155 None => {
156 let path = place_default_encryption_key()?;
157 println!("creating default key at: {:?}", path);
158 path
159 }
160 };
161
162 let kdf = kdf.unwrap_or_default();
163
164 let mut key = [0u8; 32];
165 proxmox::sys::linux::fill_with_random_data(&mut key)?;
166
167 match kdf {
168 Kdf::None => {
169 if hint.is_some() {
170 bail!("password hint not allowed for Kdf::None");
171 }
172
173 let key_config = KeyConfig::without_password(key)?;
174
175 key_config.store(path, false)?;
176 }
177 Kdf::Scrypt | Kdf::PBKDF2 => {
178 // always read passphrase from tty
179 if !tty::stdin_isatty() {
180 bail!("unable to read passphrase - no tty");
181 }
182
183 let password = tty::read_and_verify_password("Encryption Key Password: ")?;
184
185 let mut key_config = KeyConfig::with_key(&key, &password, kdf)?;
186 key_config.hint = hint;
187
188 key_config.store(&path, false)?;
189 }
190 }
191
192 Ok(())
193 }
194
195 #[api(
196 input: {
197 properties: {
198 "master-keyfile": {
199 description: "(Private) master key to use.",
200 },
201 "encrypted-keyfile": {
202 description: "RSA-encrypted keyfile to import.",
203 },
204 kdf: {
205 type: Kdf,
206 optional: true,
207 },
208 "path": {
209 description:
210 "Output file. Without this the key will become the new default encryption key.",
211 optional: true,
212 },
213 hint: {
214 schema: PASSWORD_HINT_SCHEMA,
215 optional: true,
216 },
217 },
218 },
219 )]
220 /// Import an encrypted backup of an encryption key using a (private) master key.
221 async fn import_with_master_key(
222 master_keyfile: String,
223 encrypted_keyfile: String,
224 kdf: Option<Kdf>,
225 path: Option<String>,
226 hint: Option<String>,
227 ) -> Result<(), Error> {
228 let path = match path {
229 Some(path) => PathBuf::from(path),
230 None => {
231 let path = place_default_encryption_key()?;
232 if path.exists() {
233 bail!("Please remove default encryption key at {:?} before importing to default location (or choose a non-default one).", path);
234 }
235 println!("Importing key to default location at: {:?}", path);
236 path
237 }
238 };
239
240 let encrypted_key = file_get_contents(&encrypted_keyfile)?;
241 let master_key = file_get_contents(&master_keyfile)?;
242 let password = tty::read_password("Master Key Password: ")?;
243
244 let master_key = openssl::pkey::PKey::private_key_from_pem_passphrase(&master_key, &password)
245 .map_err(|err| format_err!("failed to read PEM-formatted private key - {}", err))?
246 .rsa()
247 .map_err(|err| format_err!("not a valid private RSA key - {}", err))?;
248
249 let (key, created, _fingerprint) =
250 rsa_decrypt_key_config(master_key, &encrypted_key, &get_encryption_key_password)?;
251
252 let kdf = kdf.unwrap_or_default();
253 match kdf {
254 Kdf::None => {
255 if hint.is_some() {
256 bail!("password hint not allowed for Kdf::None");
257 }
258
259 let mut key_config = KeyConfig::without_password(key)?;
260 key_config.created = created; // keep original value
261
262 key_config.store(path, true)?;
263 }
264 Kdf::Scrypt | Kdf::PBKDF2 => {
265 let password = tty::read_and_verify_password("New Password: ")?;
266
267 let mut new_key_config = KeyConfig::with_key(&key, &password, kdf)?;
268 new_key_config.created = created; // keep original value
269 new_key_config.hint = hint;
270
271 new_key_config.store(path, true)?;
272 }
273 }
274
275 Ok(())
276 }
277
278 #[api(
279 input: {
280 properties: {
281 kdf: {
282 type: Kdf,
283 optional: true,
284 },
285 path: {
286 description: "Key file. Without this the default key's password will be changed.",
287 optional: true,
288 },
289 hint: {
290 schema: PASSWORD_HINT_SCHEMA,
291 optional: true,
292 },
293 },
294 },
295 )]
296 /// Change the encryption key's password.
297 fn change_passphrase(
298 kdf: Option<Kdf>,
299 path: Option<String>,
300 hint: Option<String>,
301 ) -> Result<(), Error> {
302 let path = match path {
303 Some(path) => PathBuf::from(path),
304 None => {
305 let path = find_default_encryption_key()?.ok_or_else(|| {
306 format_err!("no encryption file provided and no default file found")
307 })?;
308 println!("updating default key at: {:?}", path);
309 path
310 }
311 };
312
313 let kdf = kdf.unwrap_or_default();
314
315 if !tty::stdin_isatty() {
316 bail!("unable to change passphrase - no tty");
317 }
318
319 let key_config = KeyConfig::load(&path)?;
320 let (key, created, _fingerprint) = key_config.decrypt(&get_encryption_key_password)?;
321
322 match kdf {
323 Kdf::None => {
324 if hint.is_some() {
325 bail!("password hint not allowed for Kdf::None");
326 }
327
328 let mut key_config = KeyConfig::without_password(key)?;
329 key_config.created = created; // keep original value
330
331 key_config.store(&path, true)?;
332 }
333 Kdf::Scrypt | Kdf::PBKDF2 => {
334 let password = tty::read_and_verify_password("New Password: ")?;
335
336 let mut new_key_config = KeyConfig::with_key(&key, &password, kdf)?;
337 new_key_config.created = created; // keep original value
338 new_key_config.hint = hint;
339
340 new_key_config.store(&path, true)?;
341 }
342 }
343
344 Ok(())
345 }
346
347 #[api(
348 input: {
349 properties: {
350 path: {
351 description: "Key file. Without this the default key's metadata will be shown.",
352 optional: true,
353 },
354 "output-format": {
355 schema: OUTPUT_FORMAT,
356 optional: true,
357 },
358 },
359 },
360 )]
361 /// Print the encryption key's metadata.
362 fn show_key(path: Option<String>, param: Value) -> Result<(), Error> {
363 let path = match path {
364 Some(path) => PathBuf::from(path),
365 None => find_default_encryption_key()?
366 .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?,
367 };
368
369 let config: KeyConfig = serde_json::from_slice(&file_get_contents(path.clone())?)?;
370
371 let output_format = get_output_format(&param);
372
373 let mut info: KeyInfo = (&config).into();
374 info.path = Some(format!("{:?}", path));
375
376 let options = proxmox::api::cli::default_table_format_options()
377 .column(ColumnConfig::new("path"))
378 .column(ColumnConfig::new("kdf"))
379 .column(ColumnConfig::new("created").renderer(tools::format::render_epoch))
380 .column(ColumnConfig::new("modified").renderer(tools::format::render_epoch))
381 .column(ColumnConfig::new("fingerprint"))
382 .column(ColumnConfig::new("hint"));
383
384 let return_type = ReturnType::new(false, &KeyInfo::API_SCHEMA);
385
386 format_and_print_result_full(
387 &mut serde_json::to_value(info)?,
388 &return_type,
389 &output_format,
390 &options,
391 );
392
393 Ok(())
394 }
395
396 #[api(
397 input: {
398 properties: {
399 path: {
400 description: "Path to the PEM formatted RSA public key.",
401 },
402 },
403 },
404 )]
405 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
406 /// key onto the backup server along with each backup.
407 ///
408 /// The imported key will be used as default master key for future invocations by the same local
409 /// user.
410 fn import_master_pubkey(path: String) -> Result<(), Error> {
411 let pem_data = file_get_contents(&path)?;
412
413 match openssl::pkey::PKey::public_key_from_pem(&pem_data) {
414 Ok(key) => {
415 let info = RsaPubKeyInfo::try_from(key.rsa()?)?;
416 println!("Found following key at {:?}", path);
417 println!("Modulus: {}", info.modulus);
418 println!("Exponent: {}", info.exponent);
419 println!("Length: {}", info.length);
420 }
421 Err(err) => bail!("Unable to decode PEM data - {}", err),
422 };
423
424 let target_path = place_default_master_pubkey()?;
425
426 replace_file(&target_path, &pem_data, CreateOptions::new())?;
427
428 println!("Imported public master key to {:?}", target_path);
429
430 Ok(())
431 }
432
433 #[api]
434 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
435 /// encryption key onto the backup server along with each backup.
436 fn create_master_key() -> Result<(), Error> {
437 // we need a TTY to query the new password
438 if !tty::stdin_isatty() {
439 bail!("unable to create master key - no tty");
440 }
441
442 let bits = 4096;
443 println!("Generating {}-bit RSA key..", bits);
444 let rsa = openssl::rsa::Rsa::generate(bits)?;
445 let public =
446 openssl::rsa::Rsa::from_public_components(rsa.n().to_owned()?, rsa.e().to_owned()?)?;
447 let info = RsaPubKeyInfo::try_from(public)?;
448 println!("Modulus: {}", info.modulus);
449 println!("Exponent: {}", info.exponent);
450 println!();
451
452 let pkey = openssl::pkey::PKey::from_rsa(rsa)?;
453
454 let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?;
455
456 let pub_key: Vec<u8> = pkey.public_key_to_pem()?;
457 let filename_pub = "master-public.pem";
458 println!("Writing public master key to {}", filename_pub);
459 replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?;
460
461 let cipher = openssl::symm::Cipher::aes_256_cbc();
462 let priv_key: Vec<u8> =
463 pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?;
464
465 let filename_priv = "master-private.pem";
466 println!("Writing private master key to {}", filename_priv);
467 replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?;
468
469 Ok(())
470 }
471
472 #[api(
473 input: {
474 properties: {
475 path: {
476 description: "Path to the PEM formatted RSA public key. Default location will be used if not specified.",
477 optional: true,
478 },
479 "output-format": {
480 schema: OUTPUT_FORMAT,
481 optional: true,
482 },
483 },
484 },
485 )]
486 /// List information about master key
487 fn show_master_pubkey(path: Option<String>, param: Value) -> Result<(), Error> {
488 let path = match path {
489 Some(path) => PathBuf::from(path),
490 None => find_default_master_pubkey()?
491 .ok_or_else(|| format_err!("No path specified and no default master key available."))?,
492 };
493
494 let path = path.canonicalize()?;
495
496 let output_format = get_output_format(&param);
497
498 let pem_data = file_get_contents(path.clone())?;
499 let rsa = openssl::rsa::Rsa::public_key_from_pem(&pem_data)?;
500
501 let mut info = RsaPubKeyInfo::try_from(rsa)?;
502 info.path = Some(path.display().to_string());
503
504 let options = proxmox::api::cli::default_table_format_options()
505 .column(ColumnConfig::new("path"))
506 .column(ColumnConfig::new("modulus"))
507 .column(ColumnConfig::new("exponent"))
508 .column(ColumnConfig::new("length"));
509
510 let return_type = ReturnType::new(false, &RsaPubKeyInfo::API_SCHEMA);
511
512 format_and_print_result_full(
513 &mut serde_json::to_value(info)?,
514 &return_type,
515 &output_format,
516 &options,
517 );
518
519 Ok(())
520 }
521
522 #[api(
523 input: {
524 properties: {
525 path: {
526 description: "Key file. Without this the default key's will be used.",
527 optional: true,
528 },
529 subject: {
530 description: "Include the specified subject as title text.",
531 optional: true,
532 },
533 "output-format": {
534 type: PaperkeyFormat,
535 optional: true,
536 },
537 },
538 },
539 )]
540 /// Generate a printable, human readable text file containing the encryption key.
541 ///
542 /// This also includes a scanable QR code for fast key restore.
543 fn paper_key(
544 path: Option<String>,
545 subject: Option<String>,
546 output_format: Option<PaperkeyFormat>,
547 ) -> Result<(), Error> {
548 let path = match path {
549 Some(path) => PathBuf::from(path),
550 None => find_default_encryption_key()?
551 .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?,
552 };
553
554 let data = file_get_contents(&path)?;
555 let data = String::from_utf8(data)?;
556
557 generate_paper_key(std::io::stdout(), &data, subject, output_format)
558 }
559
560 pub fn cli() -> CliCommandMap {
561 let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE)
562 .arg_param(&["path"])
563 .completion_cb("path", tools::complete_file_name);
564
565 let key_import_with_master_key_cmd_def = CliCommand::new(&API_METHOD_IMPORT_WITH_MASTER_KEY)
566 .arg_param(&["master-keyfile"])
567 .completion_cb("master-keyfile", tools::complete_file_name)
568 .arg_param(&["encrypted-keyfile"])
569 .completion_cb("encrypted-keyfile", tools::complete_file_name)
570 .arg_param(&["path"])
571 .completion_cb("path", tools::complete_file_name);
572
573 let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE)
574 .arg_param(&["path"])
575 .completion_cb("path", tools::complete_file_name);
576
577 let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY);
578 let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY)
579 .arg_param(&["path"])
580 .completion_cb("path", tools::complete_file_name);
581 let key_show_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_SHOW_MASTER_PUBKEY)
582 .arg_param(&["path"])
583 .completion_cb("path", tools::complete_file_name);
584
585 let key_show_cmd_def = CliCommand::new(&API_METHOD_SHOW_KEY)
586 .arg_param(&["path"])
587 .completion_cb("path", tools::complete_file_name);
588
589 let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY)
590 .arg_param(&["path"])
591 .completion_cb("path", tools::complete_file_name);
592
593 CliCommandMap::new()
594 .insert("create", key_create_cmd_def)
595 .insert("import-with-master-key", key_import_with_master_key_cmd_def)
596 .insert("create-master-key", key_create_master_key_cmd_def)
597 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def)
598 .insert("change-passphrase", key_change_passphrase_cmd_def)
599 .insert("show", key_show_cmd_def)
600 .insert("show-master-pubkey", key_show_master_pubkey_cmd_def)
601 .insert("paperkey", paper_key_cmd_def)
602 }