]>
Commit | Line | Data |
---|---|---|
1 | use std::collections::{HashSet, HashMap}; | |
2 | ||
3 | use anyhow::{bail, Error}; | |
4 | use serde::{Deserialize, Serialize}; | |
5 | ||
6 | use proxmox::tools::fs::{ | |
7 | file_read_optional_string, | |
8 | replace_file, | |
9 | open_file_locked, | |
10 | CreateOptions, | |
11 | }; | |
12 | ||
13 | use crate::{ | |
14 | backup::{ | |
15 | Fingerprint, | |
16 | Kdf, | |
17 | KeyConfig, | |
18 | CryptConfig, | |
19 | encrypt_key_with_passphrase, | |
20 | }, | |
21 | }; | |
22 | ||
23 | mod hex_key { | |
24 | use serde::{self, Deserialize, Serializer, Deserializer}; | |
25 | ||
26 | pub fn serialize<S>( | |
27 | csum: &[u8; 32], | |
28 | serializer: S, | |
29 | ) -> Result<S::Ok, S::Error> | |
30 | where | |
31 | S: Serializer, | |
32 | { | |
33 | let s = proxmox::tools::digest_to_hex(csum); | |
34 | serializer.serialize_str(&s) | |
35 | } | |
36 | ||
37 | pub fn deserialize<'de, D>( | |
38 | deserializer: D, | |
39 | ) -> Result<[u8; 32], D::Error> | |
40 | where | |
41 | D: Deserializer<'de>, | |
42 | { | |
43 | let s = String::deserialize(deserializer)?; | |
44 | proxmox::tools::hex_to_digest(&s).map_err(serde::de::Error::custom) | |
45 | } | |
46 | } | |
47 | ||
48 | /// Store Hardware Encryption keys (private part) | |
49 | #[derive(Deserialize, Serialize)] | |
50 | pub struct EncryptionKeyInfo { | |
51 | pub fingerprint: Fingerprint, | |
52 | #[serde(with = "hex_key")] | |
53 | pub key: [u8; 32], | |
54 | } | |
55 | ||
56 | /// Store Hardware Encryption keys (public part) | |
57 | #[derive(Deserialize, Serialize)] | |
58 | pub struct EncryptionKeyConfig { | |
59 | pub hint: String, | |
60 | pub key_config: KeyConfig, | |
61 | } | |
62 | ||
63 | pub fn compute_tape_key_fingerprint(key: &[u8; 32]) -> Result<Fingerprint, Error> { | |
64 | let crypt_config = CryptConfig::new(*key)?; | |
65 | Ok(crypt_config.fingerprint()) | |
66 | } | |
67 | ||
68 | pub fn generate_tape_encryption_key(password: &[u8]) -> Result<([u8; 32], KeyConfig), Error> { | |
69 | ||
70 | let mut key = [0u8; 32]; | |
71 | proxmox::sys::linux::fill_with_random_data(&mut key)?; | |
72 | ||
73 | let mut key_config = encrypt_key_with_passphrase(&key, password, Kdf::Scrypt)?; | |
74 | key_config.fingerprint = Some(compute_tape_key_fingerprint(&key)?); | |
75 | Ok((key, key_config)) | |
76 | } | |
77 | ||
78 | impl EncryptionKeyInfo { | |
79 | pub fn new(key: [u8; 32], fingerprint: Fingerprint) -> Self { | |
80 | Self { fingerprint, key } | |
81 | } | |
82 | } | |
83 | ||
84 | impl EncryptionKeyConfig { | |
85 | pub fn new(key_config: KeyConfig, hint: String) -> Self { | |
86 | Self { hint, key_config } | |
87 | } | |
88 | } | |
89 | ||
90 | pub const TAPE_KEYS_FILENAME: &str = "/etc/proxmox-backup/tape-encryption-keys.json"; | |
91 | pub const TAPE_KEY_CONFIG_FILENAME: &str = "/etc/proxmox-backup/tape-encryption-key-config.json"; | |
92 | pub const TAPE_KEYS_LOCKFILE: &str = "/etc/proxmox-backup/.tape-encryption-keys.lck"; | |
93 | ||
94 | /// Load tape encryption keys (private part) | |
95 | pub fn load_keys() -> Result<(HashMap<Fingerprint, EncryptionKeyInfo>, [u8;32]), Error> { | |
96 | ||
97 | let content = file_read_optional_string(TAPE_KEYS_FILENAME)?; | |
98 | let content = content.unwrap_or_else(|| String::from("[]")); | |
99 | ||
100 | let digest = openssl::sha::sha256(content.as_bytes()); | |
101 | ||
102 | let key_list: Vec<EncryptionKeyInfo> = serde_json::from_str(&content)?; | |
103 | ||
104 | let mut map = HashMap::new(); | |
105 | ||
106 | for item in key_list { | |
107 | let expected_fingerprint = compute_tape_key_fingerprint(&item.key)?; | |
108 | if item.fingerprint != expected_fingerprint { | |
109 | bail!( | |
110 | "inconsistent fingerprint ({} != {})", | |
111 | item.fingerprint, | |
112 | expected_fingerprint, | |
113 | ); | |
114 | } | |
115 | ||
116 | if map.insert(item.fingerprint.clone(), item).is_some() { | |
117 | bail!("found duplicate fingerprint"); | |
118 | } | |
119 | } | |
120 | ||
121 | Ok((map, digest)) | |
122 | } | |
123 | ||
124 | /// Load tape encryption key configurations (public part) | |
125 | pub fn load_key_configs() -> Result<(HashMap<Fingerprint, EncryptionKeyConfig>, [u8;32]), Error> { | |
126 | ||
127 | let content = file_read_optional_string(TAPE_KEY_CONFIG_FILENAME)?; | |
128 | let content = content.unwrap_or_else(|| String::from("[]")); | |
129 | ||
130 | let digest = openssl::sha::sha256(content.as_bytes()); | |
131 | ||
132 | let key_list: Vec<EncryptionKeyConfig> = serde_json::from_str(&content)?; | |
133 | ||
134 | let mut map = HashMap::new(); | |
135 | let mut hint_set = HashSet::new(); | |
136 | ||
137 | for item in key_list { | |
138 | match item.key_config.fingerprint { | |
139 | Some(ref fingerprint) => { | |
140 | if !hint_set.insert(item.hint.clone()) { | |
141 | bail!("found duplicate password hint '{}'", item.hint); | |
142 | } | |
143 | if map.insert(fingerprint.clone(), item).is_some() { | |
144 | bail!("found duplicate fingerprint"); | |
145 | } | |
146 | } | |
147 | None => bail!("missing fingerprint"), | |
148 | } | |
149 | } | |
150 | ||
151 | Ok((map, digest)) | |
152 | } | |
153 | ||
154 | pub fn save_keys(map: HashMap<Fingerprint, EncryptionKeyInfo>) -> Result<(), Error> { | |
155 | ||
156 | let mut list = Vec::new(); | |
157 | ||
158 | for (_fp, item) in map { | |
159 | list.push(item); | |
160 | } | |
161 | ||
162 | let raw = serde_json::to_string_pretty(&list)?; | |
163 | ||
164 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); | |
165 | // set the correct owner/group/permissions while saving file | |
166 | // owner(rw) = root, group(r)= root | |
167 | let options = CreateOptions::new() | |
168 | .perm(mode) | |
169 | .owner(nix::unistd::ROOT) | |
170 | .group(nix::unistd::Gid::from_raw(0)); | |
171 | ||
172 | replace_file(TAPE_KEYS_FILENAME, raw.as_bytes(), options)?; | |
173 | ||
174 | Ok(()) | |
175 | } | |
176 | ||
177 | pub fn save_key_configs(map: HashMap<Fingerprint, EncryptionKeyConfig>) -> Result<(), Error> { | |
178 | ||
179 | let mut list = Vec::new(); | |
180 | ||
181 | let mut hint_set = HashSet::new(); | |
182 | ||
183 | for (_fp, item) in map { | |
184 | if !hint_set.insert(item.hint.clone()) { | |
185 | bail!("found duplicate password hint '{}'", item.hint); | |
186 | } | |
187 | list.push(item); | |
188 | } | |
189 | ||
190 | let raw = serde_json::to_string_pretty(&list)?; | |
191 | ||
192 | let backup_user = crate::backup::backup_user()?; | |
193 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600); | |
194 | // set the correct owner/group/permissions while saving file | |
195 | // owner(rw) = root, group(r)= backup | |
196 | let options = CreateOptions::new() | |
197 | .perm(mode) | |
198 | .owner(nix::unistd::ROOT) | |
199 | .group(backup_user.gid); | |
200 | ||
201 | replace_file(TAPE_KEY_CONFIG_FILENAME, raw.as_bytes(), options)?; | |
202 | ||
203 | Ok(()) | |
204 | } | |
205 | ||
206 | pub fn insert_key(key: [u8;32], key_config: KeyConfig, hint: String) -> Result<(), Error> { | |
207 | ||
208 | let _lock = open_file_locked( | |
209 | TAPE_KEYS_LOCKFILE, | |
210 | std::time::Duration::new(10, 0), | |
211 | true, | |
212 | )?; | |
213 | ||
214 | let (mut key_map, _) = load_keys()?; | |
215 | let (mut config_map, _) = load_key_configs()?; | |
216 | ||
217 | let fingerprint = match key_config.fingerprint.clone() { | |
218 | Some(fingerprint) => fingerprint, | |
219 | None => bail!("missing encryption key fingerprint - internal error"), | |
220 | }; | |
221 | ||
222 | if config_map.get(&fingerprint).is_some() { | |
223 | bail!("encryption key '{}' already exists.", fingerprint); | |
224 | } | |
225 | ||
226 | let item = EncryptionKeyInfo::new(key, fingerprint.clone()); | |
227 | key_map.insert(fingerprint.clone(), item); | |
228 | save_keys(key_map)?; | |
229 | ||
230 | let item = EncryptionKeyConfig::new(key_config, hint); | |
231 | config_map.insert(fingerprint, item); | |
232 | save_key_configs(config_map)?; | |
233 | ||
234 | Ok(()) | |
235 | ||
236 | } | |
237 | ||
238 | // shell completion helper | |
239 | pub fn complete_key_fingerprint(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> { | |
240 | let data = match load_key_configs() { | |
241 | Ok((data, _digest)) => data, | |
242 | Err(_) => return Vec::new(), | |
243 | }; | |
244 | ||
245 | data.keys().map(|fp| crate::tools::format::as_fingerprint(fp.bytes())).collect() | |
246 | } |