]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_client/key.rs
client: show key path when creating/changing default key
[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 => {
103 let path = place_default_encryption_key()?;
104 println!("creating default key at: {:?}", path);
105 path
106 }
107 };
108
109 let kdf = kdf.unwrap_or_default();
110
111 let key = proxmox::sys::linux::random_data(32)?;
112
113 match kdf {
114 Kdf::None => {
115 let created = Local.timestamp(Local::now().timestamp(), 0);
116
117 store_key_config(
118 &path,
119 false,
120 KeyConfig {
121 kdf: None,
122 created,
123 modified: created,
124 data: key,
125 },
126 )?;
127 }
128 Kdf::Scrypt => {
129 // always read passphrase from tty
130 if !tty::stdin_isatty() {
131 bail!("unable to read passphrase - no tty");
132 }
133
134 let password = tty::read_and_verify_password("Encryption Key Password: ")?;
135
136 let key_config = encrypt_key_with_passphrase(&key, &password)?;
137
138 store_key_config(&path, false, key_config)?;
139 }
140 }
141
142 Ok(())
143 }
144
145 #[api(
146 input: {
147 properties: {
148 kdf: {
149 type: Kdf,
150 optional: true,
151 },
152 path: {
153 description: "Key file. Without this the default key's password will be changed.",
154 optional: true,
155 }
156 },
157 },
158 )]
159 /// Change the encryption key's password.
160 fn change_passphrase(kdf: Option<Kdf>, path: Option<String>) -> Result<(), Error> {
161 let path = match path {
162 Some(path) => PathBuf::from(path),
163 None => {
164 let path = find_default_encryption_key()?
165 .ok_or_else(|| {
166 format_err!("no encryption file provided and no default file found")
167 })?;
168 println!("updating default key at: {:?}", path);
169 path
170 }
171 };
172
173 let kdf = kdf.unwrap_or_default();
174
175 if !tty::stdin_isatty() {
176 bail!("unable to change passphrase - no tty");
177 }
178
179 let (key, created) = load_and_decrypt_key(&path, &get_encryption_key_password)?;
180
181 match kdf {
182 Kdf::None => {
183 let modified = Local.timestamp(Local::now().timestamp(), 0);
184
185 store_key_config(
186 &path,
187 true,
188 KeyConfig {
189 kdf: None,
190 created, // keep original value
191 modified,
192 data: key.to_vec(),
193 },
194 )?;
195 }
196 Kdf::Scrypt => {
197 let password = tty::read_and_verify_password("New Password: ")?;
198
199 let mut new_key_config = encrypt_key_with_passphrase(&key, &password)?;
200 new_key_config.created = created; // keep original value
201
202 store_key_config(&path, true, new_key_config)?;
203 }
204 }
205
206 Ok(())
207 }
208
209 #[api(
210 input: {
211 properties: {
212 path: {
213 description: "Path to the PEM formatted RSA public key.",
214 },
215 },
216 },
217 )]
218 /// Import an RSA public key used to put an encrypted version of the symmetric backup encryption
219 /// key onto the backup server along with each backup.
220 fn import_master_pubkey(path: String) -> Result<(), Error> {
221 let pem_data = file_get_contents(&path)?;
222
223 if let Err(err) = openssl::pkey::PKey::public_key_from_pem(&pem_data) {
224 bail!("Unable to decode PEM data - {}", err);
225 }
226
227 let target_path = place_master_pubkey()?;
228
229 replace_file(&target_path, &pem_data, CreateOptions::new())?;
230
231 println!("Imported public master key to {:?}", target_path);
232
233 Ok(())
234 }
235
236 #[api]
237 /// Create an RSA public/private key pair used to put an encrypted version of the symmetric backup
238 /// encryption key onto the backup server along with each backup.
239 fn create_master_key() -> Result<(), Error> {
240 // we need a TTY to query the new password
241 if !tty::stdin_isatty() {
242 bail!("unable to create master key - no tty");
243 }
244
245 let rsa = openssl::rsa::Rsa::generate(4096)?;
246 let pkey = openssl::pkey::PKey::from_rsa(rsa)?;
247
248 let password = String::from_utf8(tty::read_and_verify_password("Master Key Password: ")?)?;
249
250 let pub_key: Vec<u8> = pkey.public_key_to_pem()?;
251 let filename_pub = "master-public.pem";
252 println!("Writing public master key to {}", filename_pub);
253 replace_file(filename_pub, pub_key.as_slice(), CreateOptions::new())?;
254
255 let cipher = openssl::symm::Cipher::aes_256_cbc();
256 let priv_key: Vec<u8> = pkey.private_key_to_pem_pkcs8_passphrase(cipher, password.as_bytes())?;
257
258 let filename_priv = "master-private.pem";
259 println!("Writing private master key to {}", filename_priv);
260 replace_file(filename_priv, priv_key.as_slice(), CreateOptions::new())?;
261
262 Ok(())
263 }
264
265 pub fn cli() -> CliCommandMap {
266 let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE)
267 .arg_param(&["path"])
268 .completion_cb("path", tools::complete_file_name);
269
270 let key_change_passphrase_cmd_def = CliCommand::new(&API_METHOD_CHANGE_PASSPHRASE)
271 .arg_param(&["path"])
272 .completion_cb("path", tools::complete_file_name);
273
274 let key_create_master_key_cmd_def = CliCommand::new(&API_METHOD_CREATE_MASTER_KEY);
275 let key_import_master_pubkey_cmd_def = CliCommand::new(&API_METHOD_IMPORT_MASTER_PUBKEY)
276 .arg_param(&["path"])
277 .completion_cb("path", tools::complete_file_name);
278
279 CliCommandMap::new()
280 .insert("create", key_create_cmd_def)
281 .insert("create-master-key", key_create_master_key_cmd_def)
282 .insert("import-master-pubkey", key_import_master_pubkey_cmd_def)
283 .insert("change-passphrase", key_change_passphrase_cmd_def)
284 }