]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
more xdg cleanup and encryption parameter improvements
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
1 use std::path::PathBuf;
2
3 use anyhow::{bail, format_err, Error};
4 use chrono::{Local, TimeZone};
5 use serde::{Deserialize, Serialize};
6
7 use proxmox::api::api;
8 use proxmox::api::cli::{CliCommand, CliCommandMap};
9 use proxmox::sys::linux::tty;
10 use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
11
12 use proxmox_backup::backup::{
13 encrypt_key_with_passphrase, load_and_decrypt_key, store_key_config, KeyConfig,
14 };
15 use proxmox_backup::tools;
16
17 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
18 pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
19
20 pub fn find_master_pubkey() -> Result<Option<PathBuf>, Error> {
21 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
22 }
23
24 pub fn place_master_pubkey() -> Result<PathBuf, Error> {
25 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
26 }
27
28 pub fn find_default_encryption_key() -> Result<Option<PathBuf>, Error> {
29 super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file")
30 }
31
32 pub fn place_default_encryption_key() -> Result<PathBuf, Error> {
33 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file")
34 }
35
36 pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
37 // fixme: implement other input methods
38
39 use std::env::VarError::*;
40 match std::env::var("PBS_ENCRYPTION_PASSWORD") {
41 Ok(p) => return Ok(p.as_bytes().to_vec()),
42 Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
43 Err(NotPresent) => {
44 // Try another method
45 }
46 }
47
48 // If we're on a TTY, query the user for a password
49 if tty::stdin_isatty() {
50 return Ok(tty::read_password("Encryption Key Password: ")?);
51 }
52
53 bail!("no password input mechanism available");
54 }
55
56 #[api(
57 default: "scrypt",
58 )]
59 #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
60 #[serde(rename_all = "kebab-case")]
61 /// Key derivation function for password protected encryption keys.
62 pub enum Kdf {
63 /// Do not encrypt the key.
64 None,
65
66 /// Encrypt they key with a password using SCrypt.
67 Scrypt,
68 }
69
70 impl Default for Kdf {
71 #[inline]
72 fn default() -> Self {
73 Kdf::Scrypt
74 }
75 }
76
77 #[api(
78 input: {
79 properties: {
80 kdf: {
81 type: Kdf,
82 optional: true,
83 },
84 path: {
85 description:
86 "Output file. Without this the key will become the new default encryption key.",
87 optional: true,
88 }
89 },
90 },
91 )]
92 /// Create a new encryption key.
93 fn create(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> {
94 let path = match path {
95 Some(path) => PathBuf::from(path),
96 None => place_default_encryption_key()?,
97 };
98
99 let kdf = kdf.unwrap_or_default();
100
101 let key = proxmox::sys::linux::random_data(32)?;
102
103 match kdf {
104 Kdf::None => {
105 let created = Local.timestamp(Local::now().timestamp(), 0);
106
107 store_key_config(
108 &path,
109 false,
110 KeyConfig {
111 kdf: None,
112 created,
113 modified: created,
114 data: key,
115 },
116 )?;
117 }
118 Kdf::Scrypt => {
119 // always read passphrase from tty
120 if !tty::stdin_isatty() {
121 bail!("unable to read passphrase - no tty");
122 }
123
124 let password = tty::read_and_verify_password("Encryption Key Password: ")?;
125
126 let key_config = encrypt_key_with_passphrase(&key, &password)?;
127
128 store_key_config(&path, false, key_config)?;
129 }
130 }
131
132 Ok(())
133 }
134
135 #[api(
136 input: {
137 properties: {
138 kdf: {
139 type: Kdf,
140 optional: true,
141 },
142 path: {
143 description: "Key file. Without this the default key's password will be changed.",
144 optional: true,
145 }
146 },
147 },
148 )]
149 /// Change the encryption key's password.
150 fn change_passphrase(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> {
151 let path = match path {
152 Some(path) => PathBuf::from(path),
153 None => find_default_encryption_key()?
154 .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?,
155 };
156
157 let kdf = kdf.unwrap_or_default();
158
159 if !tty::stdin_isatty() {
160 bail!("unable to change passphrase - no tty");
161 }
162
163 let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?;
164
165 match kdf {
166 Kdf::None => {
167 let modified = Local.timestamp(Local::now().timestamp(), 0);
168
169 store_key_config(
170 &path,
171 true,
172 KeyConfig {
173 kdf: None,
174 created, // keep original value
175 modified,
176 data: key.to_vec(),
177 },
178 )?;
179 }
180 Kdf::Scrypt => {
181 let password = tty::read_and_verify_password("New Password: ")?;
182
183 let mut new_key_config = encrypt_key_with_passphrase(&key, &password)?;
184 new_key_config.created = created; // keep original value
185
186 store_key_config(&path, true, new_key_config)?;
187 }
188 }
189
190 Ok(())
191 }
192
193 #[api(
194 input: {
195 properties: {
196 path: {
197 description: "Path to the PEM formatted RSA public key.",
198 },
199 },
200 },
201 )]
202 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
203 /// key onto the backup server along with each backup.
204 fn import_master_pubkey(path: String) -> Result<(), Error> {
205 let pem_data = file_get_contents(&path)?;
206
207 if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) {
208 bail!("Unable to decode PEM data - {}", err);
209 }
210
211 let target_path = place_master_pubkey()?;
212
213 replace_file(&target_path, &pem_data, CreateOptions::new())?;
214
215 println!("Imported public master key to {:?}", target_path);
216
217 Ok(())
218 }
219
220 #[api]
221 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
222 /// encryption key onto the backup server along with each backup.
223 fn create_master_key() -> Result<(), Error> {
224 // we need a TTY to query the new password
225 if !tty::stdin_isatty() {
226 bail!("unable to create master key - no tty");
227 }
228
229 let rsa = openssl::rsa::Rsa::generate(4096)?;
230 let pkey = openssl::pkey::PKey::from_rsa(rsa)?;
231
232 let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?;
233
234 let pub_key: Vec<u8> = pkey.public_key_to_pem()?;
235 let filename_pub = "master-public.pem";
236 println!("Writing public master key to {}", filename_pub);
237 replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?;
238
239 let cipher = openssl::symm::Cipher::aes_256_cbc();
240 let priv_key: Vec<u8> = pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?;
241
242 let filename_priv = "master-private.pem";
243 println!("Writing private master key to {}", filename_priv);
244 replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?;
245
246 Ok(())
247 }
248
249 pub fn cli() -> CliCommandMap {
250 let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE)
251 .arg_param(&["path"])
252 .completion_cb("path", tools::complete_file_name);
253
254 let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE)
255 .arg_param(&["path"])
256 .completion_cb("path", tools::complete_file_name);
257
258 let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY);
259 let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY)
260 .arg_param(&["path"])
261 .completion_cb("path", tools::complete_file_name);
262
263 CliCommandMap::new()
264 .insert("create", key_create_cmd_def)
265 .insert("create-master-key", key_create_master_key_cmd_def)
266 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def)
267 .insert("change-passphrase", key_change_passphrase_cmd_def)
268 }