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