]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_backup_client/key.rs
typo fixes all over the place
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
CommitLineData
42c0f784 1use std::convert::TryFrom;
2924b37d 2use std::path::PathBuf;
9696f519 3
05389a01 4use anyhow::{bail, format_err, Error};
dfb04575 5use serde_json::Value;
9696f519
WB
6
7use proxmox::api::api;
dfb04575 8use proxmox::api::cli::{
2924b37d 9 format_and_print_result_full, get_output_format, CliCommand, CliCommandMap, ColumnConfig,
dfb04575
FG
10 OUTPUT_FORMAT,
11};
b2362a12 12use proxmox::api::router::ReturnType;
9696f519
WB
13use proxmox::sys::linux::tty;
14use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
15
82a103c8 16use proxmox_backup::{
2924b37d
FG
17 api2::types::{Kdf, KeyInfo, RsaPubKeyInfo, PASSWORD_HINT_SCHEMA},
18 backup::{rsa_decrypt_key_config, KeyConfig},
82a103c8 19 tools,
2924b37d 20 tools::paperkey::{generate_paper_key, PaperkeyFormat},
9696f519 21};
7137630d 22
2f26b866
FG
23use crate::KeyWithSource;
24
b65390eb 25pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
05f17d1e 26pub const DEFAULT_MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
b65390eb 27
05f17d1e 28pub fn find_default_master_pubkey() -> Result<Option<PathBuf>, Error> {
2924b37d
FG
29 super::find_xdg_file(
30 DEFAULT_MASTER_PUBKEY_FILE_NAME,
31 "default master public key file",
32 )
05389a01 33}
9696f519 34
05f17d1e 35pub fn place_default_master_pubkey() -> Result<PathBuf, Error> {
2924b37d
FG
36 super::place_xdg_file(
37 DEFAULT_MASTER_PUBKEY_FILE_NAME,
38 "default master public key file",
39 )
9696f519
WB
40}
41
b65390eb 42pub fn find_default_encryption_key() -> Result<Option<PathBuf>, Error> {
2924b37d
FG
43 super::find_xdg_file(
44 DEFAULT_ENCRYPTION_KEY_FILE_NAME,
45 "default encryption key file",
46 )
b65390eb 47}
9696f519 48
b65390eb 49pub fn place_default_encryption_key() -> Result<PathBuf, Error> {
2924b37d
FG
50 super::place_xdg_file(
51 DEFAULT_ENCRYPTION_KEY_FILE_NAME,
52 "default encryption key file",
53 )
9696f519
WB
54}
55
5bb057e5 56#[cfg(not(test))]
2f26b866 57pub(crate) fn read_optional_default_encryption_key() -> Result<Option<KeyWithSource>, Error> {
0351f23b 58 find_default_encryption_key()?
2f26b866 59 .map(|path| file_get_contents(path).map(KeyWithSource::from_default))
0351f23b
WB
60 .transpose()
61}
62
c0a87c12 63#[cfg(not(test))]
2f26b866 64pub(crate) fn read_optional_default_master_pubkey() -> Result<Option<KeyWithSource>, Error> {
c0a87c12 65 find_default_master_pubkey()?
2f26b866 66 .map(|path| file_get_contents(path).map(KeyWithSource::from_default))
c0a87c12
FG
67 .transpose()
68}
69
5bb057e5
FG
70#[cfg(test)]
71static mut TEST_DEFAULT_ENCRYPTION_KEY: Result<Option<Vec<u8>>, Error> = Ok(None);
72
73#[cfg(test)]
2f26b866 74pub(crate) fn read_optional_default_encryption_key() -> Result<Option<KeyWithSource>, Error> {
5bb057e5
FG
75 // not safe when multiple concurrent test cases end up here!
76 unsafe {
77 match &TEST_DEFAULT_ENCRYPTION_KEY {
2f26b866
FG
78 Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))),
79 Ok(None) => Ok(None),
5bb057e5
FG
80 Err(_) => bail!("test error"),
81 }
82 }
83}
84
85#[cfg(test)]
86// not safe when multiple concurrent test cases end up here!
2f26b866 87pub(crate) unsafe fn set_test_encryption_key(value: Result<Option<Vec<u8>>, Error>) {
5bb057e5
FG
88 TEST_DEFAULT_ENCRYPTION_KEY = value;
89}
90
c0a87c12
FG
91#[cfg(test)]
92static mut TEST_DEFAULT_MASTER_PUBKEY: Result<Option<Vec<u8>>, Error> = Ok(None);
93
94#[cfg(test)]
2f26b866 95pub(crate) fn read_optional_default_master_pubkey() -> Result<Option<KeyWithSource>, Error> {
c0a87c12
FG
96 // not safe when multiple concurrent test cases end up here!
97 unsafe {
98 match &TEST_DEFAULT_MASTER_PUBKEY {
2f26b866
FG
99 Ok(Some(key)) => Ok(Some(KeyWithSource::from_default(key.clone()))),
100 Ok(None) => Ok(None),
c0a87c12
FG
101 Err(_) => bail!("test error"),
102 }
103 }
104}
105
106#[cfg(test)]
107// not safe when multiple concurrent test cases end up here!
2f26b866 108pub(crate) unsafe fn set_test_default_master_pubkey(value: Result<Option<Vec<u8>>, Error>) {
c0a87c12
FG
109 TEST_DEFAULT_MASTER_PUBKEY = value;
110}
111
9696f519
WB
112pub 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
9696f519
WB
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,
82a103c8
DM
143 },
144 hint: {
145 schema: PASSWORD_HINT_SCHEMA,
146 optional: true,
147 },
9696f519
WB
148 },
149 },
150)]
151/// Create a new encryption key.
2924b37d 152fn create(kdf: Option<Kdf>, path: Option<String>, hint: Option<String>) -> Result<(), Error> {
9696f519
WB
153 let path = match path {
154 Some(path) => PathBuf::from(path),
0eaef8eb
WB
155 None => {
156 let path = place_default_encryption_key()?;
157 println!("creating default key at: {:?}", path);
158 path
159 }
9696f519
WB
160 };
161
162 let kdf = kdf.unwrap_or_default();
163
9a045790
DM
164 let mut key = [0u8; 32];
165 proxmox::sys::linux::fill_with_random_data(&mut key)?;
9696f519
WB
166
167 match kdf {
168 Kdf::None => {
82a103c8
DM
169 if hint.is_some() {
170 bail!("password hint not allowed for Kdf::None");
171 }
172
1c86893d 173 let key_config = KeyConfig::without_password(key)?;
9a045790
DM
174
175 key_config.store(path, false)?;
9696f519 176 }
5e17dbf2 177 Kdf::Scrypt | Kdf::PBKDF2 => {
9696f519
WB
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
9a045790 185 let mut key_config = KeyConfig::with_key(&key, &password, kdf)?;
82a103c8 186 key_config.hint = hint;
9696f519 187
9a045790 188 key_config.store(&path, false)?;
9696f519
WB
189 }
190 }
191
192 Ok(())
193}
194
7137630d
FG
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,
82a103c8
DM
212 },
213 hint: {
214 schema: PASSWORD_HINT_SCHEMA,
215 optional: true,
216 },
7137630d
FG
217 },
218 },
219)]
220/// Import an encrypted backup of an encryption key using a (private) master key.
221async fn import_with_master_key(
222 master_keyfile: String,
223 encrypted_keyfile: String,
224 kdf: Option<Kdf>,
225 path: Option<String>,
82a103c8 226 hint: Option<String>,
7137630d
FG
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
2924b37d 244 let master_key = openssl::pkey::PKey::private_key_from_pem_passphrase(&master_key, &password)
7137630d
FG
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
1c86893d 249 let (key, created, _fingerprint) =
7137630d
FG
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 => {
82a103c8
DM
255 if hint.is_some() {
256 bail!("password hint not allowed for Kdf::None");
257 }
258
1c86893d 259 let mut key_config = KeyConfig::without_password(key)?;
9a045790 260 key_config.created = created; // keep original value
9a045790
DM
261
262 key_config.store(path, true)?;
7137630d
FG
263 }
264 Kdf::Scrypt | Kdf::PBKDF2 => {
265 let password = tty::read_and_verify_password("New Password: ")?;
266
9a045790 267 let mut new_key_config = KeyConfig::with_key(&key, &password, kdf)?;
7137630d 268 new_key_config.created = created; // keep original value
82a103c8 269 new_key_config.hint = hint;
7137630d 270
9a045790 271 new_key_config.store(path, true)?;
7137630d
FG
272 }
273 }
274
275 Ok(())
276}
277
9696f519
WB
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,
82a103c8
DM
288 },
289 hint: {
290 schema: PASSWORD_HINT_SCHEMA,
291 optional: true,
292 },
9696f519
WB
293 },
294 },
295)]
296/// Change the encryption key's password.
82a103c8
DM
297fn change_passphrase(
298 kdf: Option<Kdf>,
299 path: Option<String>,
300 hint: Option<String>,
301) -> Result<(), Error> {
9696f519
WB
302 let path = match path {
303 Some(path) => PathBuf::from(path),
0eaef8eb 304 None => {
2924b37d
FG
305 let path = find_default_encryption_key()?.ok_or_else(|| {
306 format_err!("no encryption file provided and no default file found")
307 })?;
0eaef8eb
WB
308 println!("updating default key at: {:?}", path);
309 path
310 }
9696f519
WB
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
9a045790 319 let key_config = KeyConfig::load(&path)?;
1c86893d 320 let (key, created, _fingerprint) = key_config.decrypt(&get_encryption_key_password)?;
9696f519
WB
321
322 match kdf {
323 Kdf::None => {
82a103c8
DM
324 if hint.is_some() {
325 bail!("password hint not allowed for Kdf::None");
326 }
327
1c86893d 328 let mut key_config = KeyConfig::without_password(key)?;
2924b37d 329 key_config.created = created; // keep original value
9a045790
DM
330
331 key_config.store(&path, true)?;
9696f519 332 }
5e17dbf2 333 Kdf::Scrypt | Kdf::PBKDF2 => {
9696f519
WB
334 let password = tty::read_and_verify_password("New Password: ")?;
335
9a045790 336 let mut new_key_config = KeyConfig::with_key(&key, &password, kdf)?;
9696f519 337 new_key_config.created = created; // keep original value
82a103c8 338 new_key_config.hint = hint;
9a045790
DM
339
340 new_key_config.store(&path, true)?;
9696f519
WB
341 }
342 }
343
344 Ok(())
345}
346
dfb04575
FG
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.
4d104cd4 362fn show_key(path: Option<String>, param: Value) -> Result<(), Error> {
dfb04575
FG
363 let path = match path {
364 Some(path) => PathBuf::from(path),
4d104cd4
FG
365 None => find_default_encryption_key()?
366 .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?,
dfb04575
FG
367 };
368
dfb04575
FG
369 let config: KeyConfig = serde_json::from_slice(&file_get_contents(path.clone())?)?;
370
5e17dbf2
DM
371 let output_format = get_output_format(&param);
372
69b8bc3b
DM
373 let mut info: KeyInfo = (&config).into();
374 info.path = Some(format!("{:?}", path));
5e17dbf2
DM
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))
82a103c8
DM
381 .column(ColumnConfig::new("fingerprint"))
382 .column(ColumnConfig::new("hint"));
5e17dbf2 383
b2362a12 384 let return_type = ReturnType::new(false, &KeyInfo::API_SCHEMA);
5e17dbf2 385
b2362a12
WB
386 format_and_print_result_full(
387 &mut serde_json::to_value(info)?,
388 &return_type,
389 &output_format,
390 &options,
391 );
dfb04575
FG
392
393 Ok(())
394}
395
9696f519
WB
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.
05f17d1e
FG
407///
408/// The imported key will be used as default master key for future invocations by the same local
409/// user.
9696f519
WB
410fn import_master_pubkey(path: String) -> Result<(), Error> {
411 let pem_data = file_get_contents(&path)?;
412
42c0f784
FG
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);
2924b37d 420 }
42c0f784
FG
421 Err(err) => bail!("Unable to decode PEM data - {}", err),
422 };
9696f519 423
05f17d1e 424 let target_path = place_default_master_pubkey()?;
9696f519
WB
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.
436fn 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
42c0f784
FG
442 let bits = 4096;
443 println!("Generating {}-bit RSA key..", bits);
444 let rsa = openssl::rsa::Rsa::generate(bits)?;
2924b37d
FG
445 let public =
446 openssl::rsa::Rsa::from_public_components(rsa.n().to_owned()?, rsa.e().to_owned()?)?;
42c0f784
FG
447 let info = RsaPubKeyInfo::try_from(public)?;
448 println!("Modulus: {}", info.modulus);
449 println!("Exponent: {}", info.exponent);
450 println!();
451
9696f519
WB
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();
2924b37d
FG
462 let priv_key: Vec<u8> =
463 pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?;
9696f519
WB
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
42c0f784
FG
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
487fn 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
82a0cd2a
DM
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: {
d1d74c43 530 description: "Include the specified subject as title text.",
82a0cd2a
DM
531 optional: true,
532 },
ef1b4363
DM
533 "output-format": {
534 type: PaperkeyFormat,
ef1b4363
DM
535 optional: true,
536 },
82a0cd2a
DM
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.
ef1b4363
DM
543fn paper_key(
544 path: Option<String>,
545 subject: Option<String>,
546 output_format: Option<PaperkeyFormat>,
547) -> Result<(), Error> {
82a0cd2a
DM
548 let path = match path {
549 Some(path) => PathBuf::from(path),
4d104cd4
FG
550 None => find_default_encryption_key()?
551 .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?,
82a0cd2a
DM
552 };
553
554 let data = file_get_contents(&path)?;
f1e29041
FG
555 let data = String::from_utf8(data)?;
556
639a6782 557 generate_paper_key(std::io::stdout(), &data, subject, output_format)
ef1b4363
DM
558}
559
560pub 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
7137630d
FG
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
ef1b4363
DM
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);
42c0f784
FG
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);
ef1b4363 584
dfb04575
FG
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
ef1b4363
DM
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)
7137630d 595 .insert("import-with-master-key", key_import_with_master_key_cmd_def)
ef1b4363
DM
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)
dfb04575 599 .insert("show", key_show_cmd_def)
42c0f784 600 .insert("show-master-pubkey", key_show_master_pubkey_cmd_def)
fdc00811 601 .insert("paperkey", paper_key_cmd_def)
ef1b4363 602}