]>
Commit | Line | Data |
---|---|---|
f7d4e4b5 | 1 | use anyhow::{bail, format_err, Error}; |
59e9ba01 | 2 | use std::convert::TryFrom; |
1e8da0a7 | 3 | use std::path::Path; |
59e9ba01 DM |
4 | |
5 | use serde_json::{json, Value}; | |
b53f6379 | 6 | use ::serde::{Deserialize, Serialize}; |
59e9ba01 | 7 | |
2107a5ae | 8 | use crate::backup::{BackupDir, CryptMode, CryptConfig}; |
59e9ba01 DM |
9 | |
10 | pub const MANIFEST_BLOB_NAME: &str = "index.json.blob"; | |
96d65fbc | 11 | pub const CLIENT_LOG_BLOB_NAME: &str = "client.log.blob"; |
59e9ba01 | 12 | |
b53f6379 DM |
13 | mod hex_csum { |
14 | use serde::{self, Deserialize, Serializer, Deserializer}; | |
15 | ||
16 | pub fn serialize<S>( | |
17 | csum: &[u8; 32], | |
18 | serializer: S, | |
19 | ) -> Result<S::Ok, S::Error> | |
20 | where | |
21 | S: Serializer, | |
22 | { | |
23 | let s = proxmox::tools::digest_to_hex(csum); | |
24 | serializer.serialize_str(&s) | |
25 | } | |
26 | ||
27 | pub fn deserialize<'de, D>( | |
28 | deserializer: D, | |
29 | ) -> Result<[u8; 32], D::Error> | |
30 | where | |
31 | D: Deserializer<'de>, | |
32 | { | |
33 | let s = String::deserialize(deserializer)?; | |
34 | proxmox::tools::hex_to_digest(&s).map_err(serde::de::Error::custom) | |
35 | } | |
36 | } | |
37 | ||
4459ffe3 DM |
38 | fn crypt_mode_none() -> CryptMode { CryptMode::None } |
39 | fn empty_value() -> Value { json!({}) } | |
40 | ||
b53f6379 DM |
41 | #[derive(Serialize, Deserialize)] |
42 | #[serde(rename_all="kebab-case")] | |
1e8da0a7 DM |
43 | pub struct FileInfo { |
44 | pub filename: String, | |
4459ffe3 | 45 | #[serde(default="crypt_mode_none")] // to be compatible with < 0.8.0 backups |
f28d9088 | 46 | pub crypt_mode: CryptMode, |
1e8da0a7 | 47 | pub size: u64, |
b53f6379 | 48 | #[serde(with = "hex_csum")] |
1e8da0a7 | 49 | pub csum: [u8; 32], |
59e9ba01 DM |
50 | } |
51 | ||
14f6c9cb FG |
52 | impl FileInfo { |
53 | ||
54 | /// Return expected CryptMode of referenced chunks | |
55 | /// | |
56 | /// Encrypted Indices should only reference encrypted chunks, while signed or plain indices | |
57 | /// should only reference plain chunks. | |
58 | pub fn chunk_crypt_mode (&self) -> CryptMode { | |
59 | match self.crypt_mode { | |
60 | CryptMode::Encrypt => CryptMode::Encrypt, | |
61 | CryptMode::SignOnly | CryptMode::None => CryptMode::None, | |
62 | } | |
63 | } | |
64 | } | |
65 | ||
b53f6379 DM |
66 | #[derive(Serialize, Deserialize)] |
67 | #[serde(rename_all="kebab-case")] | |
59e9ba01 | 68 | pub struct BackupManifest { |
b53f6379 DM |
69 | backup_type: String, |
70 | backup_id: String, | |
71 | backup_time: i64, | |
59e9ba01 | 72 | files: Vec<FileInfo>, |
4459ffe3 | 73 | #[serde(default="empty_value")] // to be compatible with < 0.8.0 backups |
b53f6379 | 74 | pub unprotected: Value, |
882c0823 | 75 | pub signature: Option<String>, |
59e9ba01 DM |
76 | } |
77 | ||
1e8da0a7 DM |
78 | #[derive(PartialEq)] |
79 | pub enum ArchiveType { | |
80 | FixedIndex, | |
81 | DynamicIndex, | |
82 | Blob, | |
83 | } | |
84 | ||
85 | pub fn archive_type<P: AsRef<Path>>( | |
86 | archive_name: P, | |
87 | ) -> Result<ArchiveType, Error> { | |
88 | ||
89 | let archive_name = archive_name.as_ref(); | |
90 | let archive_type = match archive_name.extension().and_then(|ext| ext.to_str()) { | |
91 | Some("didx") => ArchiveType::DynamicIndex, | |
92 | Some("fidx") => ArchiveType::FixedIndex, | |
93 | Some("blob") => ArchiveType::Blob, | |
94 | _ => bail!("unknown archive type: {:?}", archive_name), | |
95 | }; | |
96 | Ok(archive_type) | |
97 | } | |
98 | ||
99 | ||
59e9ba01 DM |
100 | impl BackupManifest { |
101 | ||
102 | pub fn new(snapshot: BackupDir) -> Self { | |
b53f6379 DM |
103 | Self { |
104 | backup_type: snapshot.group().backup_type().into(), | |
105 | backup_id: snapshot.group().backup_id().into(), | |
6a7be83e | 106 | backup_time: snapshot.backup_time(), |
b53f6379 DM |
107 | files: Vec::new(), |
108 | unprotected: json!({}), | |
882c0823 | 109 | signature: None, |
b53f6379 | 110 | } |
59e9ba01 DM |
111 | } |
112 | ||
f28d9088 | 113 | pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], crypt_mode: CryptMode) -> Result<(), Error> { |
1e8da0a7 | 114 | let _archive_type = archive_type(&filename)?; // check type |
f28d9088 | 115 | self.files.push(FileInfo { filename, size, csum, crypt_mode }); |
1e8da0a7 DM |
116 | Ok(()) |
117 | } | |
118 | ||
119 | pub fn files(&self) -> &[FileInfo] { | |
120 | &self.files[..] | |
59e9ba01 DM |
121 | } |
122 | ||
3a3af6e2 | 123 | pub fn lookup_file_info(&self, name: &str) -> Result<&FileInfo, Error> { |
f06b820a DM |
124 | |
125 | let info = self.files.iter().find(|item| item.filename == name); | |
126 | ||
127 | match info { | |
128 | None => bail!("manifest does not contain file '{}'", name), | |
129 | Some(info) => Ok(info), | |
130 | } | |
131 | } | |
132 | ||
133 | pub fn verify_file(&self, name: &str, csum: &[u8; 32], size: u64) -> Result<(), Error> { | |
134 | ||
135 | let info = self.lookup_file_info(name)?; | |
136 | ||
137 | if size != info.size { | |
55919bf1 | 138 | bail!("wrong size for file '{}' ({} != {})", name, info.size, size); |
f06b820a DM |
139 | } |
140 | ||
141 | if csum != &info.csum { | |
142 | bail!("wrong checksum for file '{}'", name); | |
143 | } | |
144 | ||
145 | Ok(()) | |
146 | } | |
147 | ||
1ffe0301 | 148 | // Generate canonical json |
20a4e4e2 WB |
149 | fn to_canonical_json(value: &Value) -> Result<Vec<u8>, Error> { |
150 | let mut data = Vec::new(); | |
151 | Self::write_canonical_json(value, &mut data)?; | |
152 | Ok(data) | |
153 | } | |
154 | ||
155 | fn write_canonical_json(value: &Value, output: &mut Vec<u8>) -> Result<(), Error> { | |
b53f6379 DM |
156 | match value { |
157 | Value::Null => bail!("got unexpected null value"), | |
20a4e4e2 WB |
158 | Value::String(_) | Value::Number(_) | Value::Bool(_) => { |
159 | serde_json::to_writer(output, &value)?; | |
b53f6379 | 160 | } |
b53f6379 | 161 | Value::Array(list) => { |
20a4e4e2 WB |
162 | output.push(b'['); |
163 | let mut iter = list.iter(); | |
164 | if let Some(item) = iter.next() { | |
165 | Self::write_canonical_json(item, output)?; | |
166 | for item in iter { | |
167 | output.push(b','); | |
168 | Self::write_canonical_json(item, output)?; | |
169 | } | |
b53f6379 | 170 | } |
20a4e4e2 | 171 | output.push(b']'); |
b53f6379 DM |
172 | } |
173 | Value::Object(map) => { | |
20a4e4e2 WB |
174 | output.push(b'{'); |
175 | let mut keys: Vec<&str> = map.keys().map(String::as_str).collect(); | |
b53f6379 | 176 | keys.sort(); |
20a4e4e2 WB |
177 | let mut iter = keys.into_iter(); |
178 | if let Some(key) = iter.next() { | |
bccdc5fa | 179 | serde_json::to_writer(&mut *output, &key)?; |
20a4e4e2 WB |
180 | output.push(b':'); |
181 | Self::write_canonical_json(&map[key], output)?; | |
182 | for key in iter { | |
183 | output.push(b','); | |
bccdc5fa | 184 | serde_json::to_writer(&mut *output, &key)?; |
20a4e4e2 WB |
185 | output.push(b':'); |
186 | Self::write_canonical_json(&map[key], output)?; | |
187 | } | |
b53f6379 | 188 | } |
20a4e4e2 | 189 | output.push(b'}'); |
b53f6379 | 190 | } |
2107a5ae | 191 | } |
b53f6379 DM |
192 | Ok(()) |
193 | } | |
194 | ||
195 | /// Compute manifest signature | |
196 | /// | |
197 | /// By generating a HMAC SHA256 over the canonical json | |
198 | /// representation, The 'unpreotected' property is excluded. | |
199 | pub fn signature(&self, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> { | |
3dacedce DM |
200 | Self::json_signature(&serde_json::to_value(&self)?, crypt_config) |
201 | } | |
202 | ||
203 | fn json_signature(data: &Value, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> { | |
b53f6379 | 204 | |
3dacedce | 205 | let mut signed_data = data.clone(); |
b53f6379 DM |
206 | |
207 | signed_data.as_object_mut().unwrap().remove("unprotected"); // exclude | |
62593aba | 208 | signed_data.as_object_mut().unwrap().remove("signature"); // exclude |
b53f6379 | 209 | |
20a4e4e2 | 210 | let canonical = Self::to_canonical_json(&signed_data)?; |
2107a5ae | 211 | |
20a4e4e2 | 212 | let sig = crypt_config.compute_auth_tag(&canonical); |
b53f6379 DM |
213 | |
214 | Ok(sig) | |
2107a5ae DM |
215 | } |
216 | ||
b53f6379 | 217 | /// Converts the Manifest into json string, and add a signature if there is a crypt_config. |
dfa517ad | 218 | pub fn to_string(&self, crypt_config: Option<&CryptConfig>) -> Result<String, Error> { |
b53f6379 DM |
219 | |
220 | let mut manifest = serde_json::to_value(&self)?; | |
2107a5ae DM |
221 | |
222 | if let Some(crypt_config) = crypt_config { | |
b53f6379 | 223 | let sig = self.signature(crypt_config)?; |
2107a5ae DM |
224 | manifest["signature"] = proxmox::tools::digest_to_hex(&sig).into(); |
225 | } | |
226 | ||
b53f6379 DM |
227 | let manifest = serde_json::to_string_pretty(&manifest).unwrap().into(); |
228 | Ok(manifest) | |
59e9ba01 DM |
229 | } |
230 | ||
b53f6379 DM |
231 | /// Try to read the manifest. This verifies the signature if there is a crypt_config. |
232 | pub fn from_data(data: &[u8], crypt_config: Option<&CryptConfig>) -> Result<BackupManifest, Error> { | |
233 | let json: Value = serde_json::from_slice(data)?; | |
234 | let signature = json["signature"].as_str().map(String::from); | |
b53f6379 DM |
235 | |
236 | if let Some(ref crypt_config) = crypt_config { | |
237 | if let Some(signature) = signature { | |
3dacedce | 238 | let expected_signature = proxmox::tools::digest_to_hex(&Self::json_signature(&json, crypt_config)?); |
b53f6379 DM |
239 | if signature != expected_signature { |
240 | bail!("wrong signature in manifest"); | |
241 | } | |
242 | } else { | |
243 | // not signed: warn/fail? | |
244 | } | |
245 | } | |
3dacedce DM |
246 | |
247 | let manifest: BackupManifest = serde_json::from_value(json)?; | |
b53f6379 DM |
248 | Ok(manifest) |
249 | } | |
59e9ba01 | 250 | } |
b53f6379 | 251 | |
3dacedce | 252 | |
247a8ca5 DM |
253 | impl TryFrom<super::DataBlob> for BackupManifest { |
254 | type Error = Error; | |
255 | ||
256 | fn try_from(blob: super::DataBlob) -> Result<Self, Error> { | |
8819d1f2 FG |
257 | // no expected digest available |
258 | let data = blob.decode(None, None) | |
247a8ca5 DM |
259 | .map_err(|err| format_err!("decode backup manifest blob failed - {}", err))?; |
260 | let json: Value = serde_json::from_slice(&data[..]) | |
261 | .map_err(|err| format_err!("unable to parse backup manifest json - {}", err))?; | |
3dacedce DM |
262 | let manifest: BackupManifest = serde_json::from_value(json)?; |
263 | Ok(manifest) | |
247a8ca5 DM |
264 | } |
265 | } | |
59e9ba01 | 266 | |
b53f6379 DM |
267 | |
268 | #[test] | |
269 | fn test_manifest_signature() -> Result<(), Error> { | |
270 | ||
271 | use crate::backup::{KeyDerivationConfig}; | |
272 | ||
273 | let pw = b"test"; | |
274 | ||
275 | let kdf = KeyDerivationConfig::Scrypt { | |
276 | n: 65536, | |
277 | r: 8, | |
278 | p: 1, | |
279 | salt: Vec::new(), | |
280 | }; | |
281 | ||
282 | let testkey = kdf.derive_key(pw)?; | |
283 | ||
284 | let crypt_config = CryptConfig::new(testkey)?; | |
285 | ||
286 | let snapshot: BackupDir = "host/elsa/2020-06-26T13:56:05Z".parse()?; | |
287 | ||
288 | let mut manifest = BackupManifest::new(snapshot); | |
289 | ||
290 | manifest.add_file("test1.img.fidx".into(), 200, [1u8; 32], CryptMode::Encrypt)?; | |
291 | manifest.add_file("abc.blob".into(), 200, [2u8; 32], CryptMode::None)?; | |
292 | ||
293 | manifest.unprotected["note"] = "This is not protected by the signature.".into(); | |
294 | ||
dfa517ad | 295 | let text = manifest.to_string(Some(&crypt_config))?; |
b53f6379 DM |
296 | |
297 | let manifest: Value = serde_json::from_str(&text)?; | |
298 | let signature = manifest["signature"].as_str().unwrap().to_string(); | |
299 | ||
300 | assert_eq!(signature, "d7b446fb7db081662081d4b40fedd858a1d6307a5aff4ecff7d5bf4fd35679e9"); | |
301 | ||
3dacedce | 302 | let manifest: BackupManifest = serde_json::from_value(manifest)?; |
b53f6379 DM |
303 | let expected_signature = proxmox::tools::digest_to_hex(&manifest.signature(&crypt_config)?); |
304 | ||
305 | assert_eq!(signature, expected_signature); | |
306 | ||
307 | Ok(()) | |
308 | } |