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