]> git.proxmox.com Git - proxmox-backup.git/blame - src/bin/proxmox_backup_client/key.rs
more xdg cleanup and encryption parameter improvements
[proxmox-backup.git] / src / bin / proxmox_backup_client / key.rs
CommitLineData
9696f519
WB
1use std::path::PathBuf;
2
05389a01 3use anyhow::{bail, format_err, Error};
9696f519
WB
4use chrono::{Local, TimeZone};
5use serde::{Deserialize, Serialize};
9696f519
WB
6
7use proxmox::api::api;
8use proxmox::api::cli::{CliCommand, CliCommandMap};
9use proxmox::sys::linux::tty;
10use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
11
12use proxmox_backup::backup::{
13 encrypt_key_with_passphrase, load_and_decrypt_key, store_key_config, KeyConfig,
14};
15use proxmox_backup::tools;
16
b65390eb 17pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
05389a01 18pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
b65390eb 19
05389a01
WB
20pub 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
24pub 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 28pub 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 32pub 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
36pub 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.
62pub enum Kdf {
63 /// Do not encrypt the key.
64 None,
65
66 /// Encrypt they key with a password using SCrypt.
67 Scrypt,
68}
69
70impl 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.
93fn 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.
150fn 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.
204fn 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.
223fn 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
249pub 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}