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