]>
Commit | Line | Data |
---|---|---|
9696f519 WB |
1 | use std::path::PathBuf; |
2 | ||
05389a01 | 3 | use anyhow::{bail, format_err, Error}; |
9696f519 WB |
4 | use chrono::{Local, TimeZone}; |
5 | use serde::{Deserialize, Serialize}; | |
9696f519 WB |
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 | ||
b65390eb | 17 | pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json"; |
05389a01 | 18 | pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem"; |
b65390eb | 19 | |
05389a01 WB |
20 | pub fn find_master_pubkey() -> Result<Option<PathBuf>, Error> { |
21 | super::find_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file") | |
22 | } | |
9696f519 | 23 | |
05389a01 WB |
24 | pub fn place_master_pubkey() -> Result<PathBuf, Error> { |
25 | super::place_xdg_file(MASTER_PUBKEY_FILE_NAME, "main public key file") | |
9696f519 WB |
26 | } |
27 | ||
b65390eb | 28 | pub fn find_default_encryption_key() -> Result<Option<PathBuf>, Error> { |
05389a01 | 29 | super::find_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file") |
b65390eb | 30 | } |
9696f519 | 31 | |
b65390eb | 32 | pub fn place_default_encryption_key() -> Result<PathBuf, Error> { |
05389a01 | 33 | super::place_xdg_file(DEFAULT_ENCRYPTION_KEY_FILE_NAME, "default encryption key file") |
9696f519 WB |
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), | |
b65390eb | 96 | None => place_default_encryption_key()?, |
9696f519 WB |
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), | |
b65390eb WB |
153 | None => find_default_encryption_key()? |
154 | .ok_or_else(|| format_err!("no encryption file provided and no default file found"))?, | |
9696f519 WB |
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 | ||
05389a01 | 211 | let target_path = place_master_pubkey()?; |
9696f519 WB |
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 | } |