]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
proxmox_backup_client key: add new paper-key command
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
1 use std::path::PathBuf;
2 use std::io::Write;
3 use std::process::{Stdio, Command};
4
5 use anyhow::{bail, format_err, Error};
6 use serde::{Deserialize, Serialize};
7
8 use proxmox::api::api;
9 use proxmox::api::cli::{CliCommand, CliCommandMap};
10 use proxmox::sys::linux::tty;
11 use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
12
13 use proxmox_backup::backup::{
14 encrypt_key_with_passphrase, load_and_decrypt_key, store_key_config, KeyConfig,
15 };
16 use proxmox_backup::tools;
17
18 pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
19 pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
20
21 pub fn find_master_pubkey() -> Result<Option<PathBuf>, Error> {
22 super::find_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
23 }
24
25 pub fn place_master_pubkey() -> Result<PathBuf, Error> {
26 super::place_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file")
27 }
28
29 pub fn find_default_encryption_key() -> Result<Option<PathBuf>, Error> {
30 super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file")
31 }
32
33 pub fn place_default_encryption_key() -> Result<PathBuf, Error> {
34 super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file")
35 }
36
37 pub fn read_optional_default_encryption_key() -> Result<Option<Vec<u8>>, Error> {
38 find_default_encryption_key()?
39 .map(file_get_contents)
40 .transpose()
41 }
42
43 pub fn get_encryption_key_password() -> Result<Vec<u8>, Error> {
44 // fixme: implement other input methods
45
46 use std::env::VarError::*;
47 match std::env::var("PBS_ENCRYPTION_PASSWORD") {
48 Ok(p) => return Ok(p.as_bytes().to_vec()),
49 Err(NotUnicode(_)) => bail!("PBS_ENCRYPTION_PASSWORD contains bad characters"),
50 Err(NotPresent) => {
51 // Try another method
52 }
53 }
54
55 // If we're on a TTY, query the user for a password
56 if tty::stdin_isatty() {
57 return Ok(tty::read_password("Encryption Key Password: ")?);
58 }
59
60 bail!("no password input mechanism available");
61 }
62
63 #[api(
64 default: "scrypt",
65 )]
66 #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
67 #[serde(rename_all = "kebab-case")]
68 /// Key derivation function for password protected encryption keys.
69 pub enum Kdf {
70 /// Do not encrypt the key.
71 None,
72
73 /// Encrypt they key with a password using SCrypt.
74 Scrypt,
75 }
76
77 impl Default for Kdf {
78 #[inline]
79 fn default() -> Self {
80 Kdf::Scrypt
81 }
82 }
83
84 #[api(
85 input: {
86 properties: {
87 kdf: {
88 type: Kdf,
89 optional: true,
90 },
91 path: {
92 description:
93 "Output file. Without this the key will become the new default encryption key.",
94 optional: true,
95 }
96 },
97 },
98 )]
99 /// Create a new encryption key.
100 fn create(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> {
101 let path = match path {
102 Some(path) => PathBuf::from(path),
103 None => {
104 let path = place_default_encryption_key()?;
105 println!("creating default key at: {:?}", path);
106 path
107 }
108 };
109
110 let kdf = kdf.unwrap_or_default();
111
112 let key = proxmox::sys::linux::random_data(32)?;
113
114 match kdf {
115 Kdf::None => {
116 let created = proxmox::tools::time::epoch_i64();
117
118 store_key_config(
119 &path,
120 false,
121 KeyConfig {
122 kdf: None,
123 created,
124 modified: created,
125 data: key,
126 },
127 )?;
128 }
129 Kdf::Scrypt => {
130 // always read passphrase from tty
131 if !tty::stdin_isatty() {
132 bail!("unable to read passphrase - no tty");
133 }
134
135 let password = tty::read_and_verify_password("Encryption Key Password: ")?;
136
137 let key_config = encrypt_key_with_passphrase(&key, &password)?;
138
139 store_key_config(&path, false, key_config)?;
140 }
141 }
142
143 Ok(())
144 }
145
146 #[api(
147 input: {
148 properties: {
149 kdf: {
150 type: Kdf,
151 optional: true,
152 },
153 path: {
154 description: "Key file. Without this the default key's password will be changed.",
155 optional: true,
156 }
157 },
158 },
159 )]
160 /// Change the encryption key's password.
161 fn change_passphrase(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> {
162 let path = match path {
163 Some(path) => PathBuf::from(path),
164 None => {
165 let path = find_default_encryption_key()?
166 .ok_or_else(|| {
167 format_err!("no encryption file provided and no default file found")
168 })?;
169 println!("updating default key at: {:?}", path);
170 path
171 }
172 };
173
174 let kdf = kdf.unwrap_or_default();
175
176 if !tty::stdin_isatty() {
177 bail!("unable to change passphrase - no tty");
178 }
179
180 let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?;
181
182 match kdf {
183 Kdf::None => {
184 let modified = proxmox::tools::time::epoch_i64();
185
186 store_key_config(
187 &path,
188 true,
189 KeyConfig {
190 kdf: None,
191 created, // keep original value
192 modified,
193 data: key.to_vec(),
194 },
195 )?;
196 }
197 Kdf::Scrypt => {
198 let password = tty::read_and_verify_password("New Password: ")?;
199
200 let mut new_key_config = encrypt_key_with_passphrase(&key, &password)?;
201 new_key_config.created = created; // keep original value
202
203 store_key_config(&path, true, new_key_config)?;
204 }
205 }
206
207 Ok(())
208 }
209
210 #[api(
211 input: {
212 properties: {
213 path: {
214 description: "Path to the PEM formatted RSA public key.",
215 },
216 },
217 },
218 )]
219 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
220 /// key onto the backup server along with each backup.
221 fn import_master_pubkey(path: String) -> Result<(), Error> {
222 let pem_data = file_get_contents(&path)?;
223
224 if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) {
225 bail!("Unable to decode PEM data - {}", err);
226 }
227
228 let target_path = place_master_pubkey()?;
229
230 replace_file(&target_path, &pem_data, CreateOptions::new())?;
231
232 println!("Imported public master key to {:?}", target_path);
233
234 Ok(())
235 }
236
237 #[api]
238 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
239 /// encryption key onto the backup server along with each backup.
240 fn create_master_key() -> Result<(), Error> {
241 // we need a TTY to query the new password
242 if !tty::stdin_isatty() {
243 bail!("unable to create master key - no tty");
244 }
245
246 let rsa = openssl::rsa::Rsa::generate(4096)?;
247 let pkey = openssl::pkey::PKey::from_rsa(rsa)?;
248
249 let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?;
250
251 let pub_key: Vec<u8> = pkey.public_key_to_pem()?;
252 let filename_pub = "master-public.pem";
253 println!("Writing public master key to {}", filename_pub);
254 replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?;
255
256 let cipher = openssl::symm::Cipher::aes_256_cbc();
257 let priv_key: Vec<u8> = pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?;
258
259 let filename_priv = "master-private.pem";
260 println!("Writing private master key to {}", filename_priv);
261 replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?;
262
263 Ok(())
264 }
265
266 #[api(
267 input: {
268 properties: {
269 path: {
270 description: "Key file. Without this the default key's will be used.",
271 optional: true,
272 },
273 subject: {
274 description: "Include the specified subject as titel text.",
275 optional: true,
276 },
277 },
278 },
279 )]
280 /// Generate a printable, human readable text file containing the encryption key.
281 ///
282 /// This also includes a scanable QR code for fast key restore.
283 fn paper_key(path: Option<String>, subject: Option<String>) -> Result<(), Error> {
284 let path = match path {
285 Some(path) => PathBuf::from(path),
286 None => {
287 let path = find_default_encryption_key()?
288 .ok_or_else(|| {
289 format_err!("no encryption file provided and no default file found")
290 })?;
291 path
292 }
293 };
294
295 let data = file_get_contents(&path)?;
296 let key_config: KeyConfig = serde_json::from_slice(&data)?;
297 let key_text = serde_json::to_string_pretty(&key_config)?;
298
299 if let Some(subject) = subject {
300 println!("Subject: {}\n", subject);
301 }
302
303 println!("-----BEGIN PROXMOX BACKUP KEY-----");
304 println!("{}", key_text);
305 println!("-----END PROXMOX BACKUP KEY-----");
306
307 let mut child = Command::new("qrencode")
308 .args(&["-t", "utf8i", "-lm"])
309 .stdin(Stdio::piped())
310 .stdout(Stdio::piped())
311 .spawn()?;
312
313 {
314 let stdin = child.stdin.as_mut().expect("Failed to open stdin");
315 stdin.write_all(key_text.as_bytes()).expect("Failed to write to stdin");
316 }
317
318 let output = child.wait_with_output().expect("Failed to read stdout");
319
320 println!("{}", String::from_utf8_lossy(&output.stdout));
321
322 Ok(())
323 }
324
325
326 pub fn cli() -> CliCommandMap {
327 let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE)
328 .arg_param(&["path"])
329 .completion_cb("path", tools::complete_file_name);
330
331 let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE)
332 .arg_param(&["path"])
333 .completion_cb("path", tools::complete_file_name);
334
335 let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY);
336 let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY)
337 .arg_param(&["path"])
338 .completion_cb("path", tools::complete_file_name);
339
340 let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY)
341 .arg_param(&["path"])
342 .completion_cb("path", tools::complete_file_name);
343
344 CliCommandMap::new()
345 .insert("create", key_create_cmd_def)
346 .insert("create-master-key", key_create_master_key_cmd_def)
347 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def)
348 .insert("change-passphrase", key_change_passphrase_cmd_def)
349 .insert("paper-key", paper_key_cmd_def)
350 }