]>
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 | ||
b53f6379 DM |
52 | #[derive(Serialize, Deserialize)] |
53 | #[serde(rename_all="kebab-case")] | |
59e9ba01 | 54 | pub struct BackupManifest { |
b53f6379 DM |
55 | backup_type: String, |
56 | backup_id: String, | |
57 | backup_time: i64, | |
59e9ba01 | 58 | files: Vec<FileInfo>, |
4459ffe3 | 59 | #[serde(default="empty_value")] // to be compatible with < 0.8.0 backups |
b53f6379 | 60 | pub unprotected: Value, |
59e9ba01 DM |
61 | } |
62 | ||
1e8da0a7 DM |
63 | #[derive(PartialEq)] |
64 | pub enum ArchiveType { | |
65 | FixedIndex, | |
66 | DynamicIndex, | |
67 | Blob, | |
68 | } | |
69 | ||
70 | pub fn archive_type<P: AsRef<Path>>( | |
71 | archive_name: P, | |
72 | ) -> Result<ArchiveType, Error> { | |
73 | ||
74 | let archive_name = archive_name.as_ref(); | |
75 | let archive_type = match archive_name.extension().and_then(|ext| ext.to_str()) { | |
76 | Some("didx") => ArchiveType::DynamicIndex, | |
77 | Some("fidx") => ArchiveType::FixedIndex, | |
78 | Some("blob") => ArchiveType::Blob, | |
79 | _ => bail!("unknown archive type: {:?}", archive_name), | |
80 | }; | |
81 | Ok(archive_type) | |
82 | } | |
83 | ||
84 | ||
59e9ba01 DM |
85 | impl BackupManifest { |
86 | ||
87 | pub fn new(snapshot: BackupDir) -> Self { | |
b53f6379 DM |
88 | Self { |
89 | backup_type: snapshot.group().backup_type().into(), | |
90 | backup_id: snapshot.group().backup_id().into(), | |
91 | backup_time: snapshot.backup_time().timestamp(), | |
92 | files: Vec::new(), | |
93 | unprotected: json!({}), | |
94 | } | |
59e9ba01 DM |
95 | } |
96 | ||
f28d9088 | 97 | pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], crypt_mode: CryptMode) -> Result<(), Error> { |
1e8da0a7 | 98 | let _archive_type = archive_type(&filename)?; // check type |
f28d9088 | 99 | self.files.push(FileInfo { filename, size, csum, crypt_mode }); |
1e8da0a7 DM |
100 | Ok(()) |
101 | } | |
102 | ||
103 | pub fn files(&self) -> &[FileInfo] { | |
104 | &self.files[..] | |
59e9ba01 DM |
105 | } |
106 | ||
f06b820a DM |
107 | fn lookup_file_info(&self, name: &str) -> Result<&FileInfo, Error> { |
108 | ||
109 | let info = self.files.iter().find(|item| item.filename == name); | |
110 | ||
111 | match info { | |
112 | None => bail!("manifest does not contain file '{}'", name), | |
113 | Some(info) => Ok(info), | |
114 | } | |
115 | } | |
116 | ||
117 | pub fn verify_file(&self, name: &str, csum: &[u8; 32], size: u64) -> Result<(), Error> { | |
118 | ||
119 | let info = self.lookup_file_info(name)?; | |
120 | ||
121 | if size != info.size { | |
55919bf1 | 122 | bail!("wrong size for file '{}' ({} != {})", name, info.size, size); |
f06b820a DM |
123 | } |
124 | ||
125 | if csum != &info.csum { | |
126 | bail!("wrong checksum for file '{}'", name); | |
127 | } | |
128 | ||
129 | Ok(()) | |
130 | } | |
131 | ||
b53f6379 DM |
132 | // Generate cannonical json |
133 | fn to_canonical_json(value: &Value, output: &mut String) -> Result<(), Error> { | |
134 | match value { | |
135 | Value::Null => bail!("got unexpected null value"), | |
136 | Value::String(_) => { | |
137 | output.push_str(&serde_json::to_string(value)?); | |
138 | }, | |
139 | Value::Number(_) => { | |
140 | output.push_str(&serde_json::to_string(value)?); | |
141 | } | |
142 | Value::Bool(_) => { | |
143 | output.push_str(&serde_json::to_string(value)?); | |
144 | }, | |
145 | Value::Array(list) => { | |
146 | output.push('['); | |
147 | for (i, item) in list.iter().enumerate() { | |
148 | if i != 0 { output.push(','); } | |
149 | Self::to_canonical_json(item, output)?; | |
150 | } | |
151 | output.push(']'); | |
152 | } | |
153 | Value::Object(map) => { | |
154 | output.push('{'); | |
155 | let mut keys: Vec<String> = map.keys().map(|s| s.clone()).collect(); | |
156 | keys.sort(); | |
157 | for (i, key) in keys.iter().enumerate() { | |
158 | let item = map.get(key).unwrap(); | |
159 | if i != 0 { output.push(','); } | |
160 | ||
161 | output.push_str(&serde_json::to_string(&Value::String(key.clone()))?); | |
162 | output.push(':'); | |
163 | Self::to_canonical_json(item, output)?; | |
164 | } | |
165 | output.push('}'); | |
166 | } | |
2107a5ae | 167 | } |
b53f6379 DM |
168 | Ok(()) |
169 | } | |
170 | ||
171 | /// Compute manifest signature | |
172 | /// | |
173 | /// By generating a HMAC SHA256 over the canonical json | |
174 | /// representation, The 'unpreotected' property is excluded. | |
175 | pub fn signature(&self, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> { | |
3dacedce DM |
176 | Self::json_signature(&serde_json::to_value(&self)?, crypt_config) |
177 | } | |
178 | ||
179 | fn json_signature(data: &Value, crypt_config: &CryptConfig) -> Result<[u8; 32], Error> { | |
b53f6379 | 180 | |
3dacedce | 181 | let mut signed_data = data.clone(); |
b53f6379 DM |
182 | |
183 | signed_data.as_object_mut().unwrap().remove("unprotected"); // exclude | |
184 | ||
185 | let mut canonical = String::new(); | |
186 | Self::to_canonical_json(&signed_data, &mut canonical)?; | |
2107a5ae | 187 | |
b53f6379 DM |
188 | let sig = crypt_config.compute_auth_tag(canonical.as_bytes()); |
189 | ||
190 | Ok(sig) | |
2107a5ae DM |
191 | } |
192 | ||
b53f6379 | 193 | /// Converts the Manifest into json string, and add a signature if there is a crypt_config. |
dfa517ad | 194 | pub fn to_string(&self, crypt_config: Option<&CryptConfig>) -> Result<String, Error> { |
b53f6379 DM |
195 | |
196 | let mut manifest = serde_json::to_value(&self)?; | |
2107a5ae DM |
197 | |
198 | if let Some(crypt_config) = crypt_config { | |
b53f6379 | 199 | let sig = self.signature(crypt_config)?; |
2107a5ae DM |
200 | manifest["signature"] = proxmox::tools::digest_to_hex(&sig).into(); |
201 | } | |
202 | ||
b53f6379 DM |
203 | let manifest = serde_json::to_string_pretty(&manifest).unwrap().into(); |
204 | Ok(manifest) | |
59e9ba01 DM |
205 | } |
206 | ||
b53f6379 DM |
207 | /// Try to read the manifest. This verifies the signature if there is a crypt_config. |
208 | pub fn from_data(data: &[u8], crypt_config: Option<&CryptConfig>) -> Result<BackupManifest, Error> { | |
209 | let json: Value = serde_json::from_slice(data)?; | |
210 | let signature = json["signature"].as_str().map(String::from); | |
b53f6379 DM |
211 | |
212 | if let Some(ref crypt_config) = crypt_config { | |
213 | if let Some(signature) = signature { | |
3dacedce | 214 | let expected_signature = proxmox::tools::digest_to_hex(&Self::json_signature(&json, crypt_config)?); |
b53f6379 DM |
215 | if signature != expected_signature { |
216 | bail!("wrong signature in manifest"); | |
217 | } | |
218 | } else { | |
219 | // not signed: warn/fail? | |
220 | } | |
221 | } | |
3dacedce DM |
222 | |
223 | let manifest: BackupManifest = serde_json::from_value(json)?; | |
b53f6379 DM |
224 | Ok(manifest) |
225 | } | |
59e9ba01 | 226 | } |
b53f6379 | 227 | |
3dacedce | 228 | |
247a8ca5 DM |
229 | impl TryFrom<super::DataBlob> for BackupManifest { |
230 | type Error = Error; | |
231 | ||
232 | fn try_from(blob: super::DataBlob) -> Result<Self, Error> { | |
233 | let data = blob.decode(None) | |
234 | .map_err(|err| format_err!("decode backup manifest blob failed - {}", err))?; | |
235 | let json: Value = serde_json::from_slice(&data[..]) | |
236 | .map_err(|err| format_err!("unable to parse backup manifest json - {}", err))?; | |
3dacedce DM |
237 | let manifest: BackupManifest = serde_json::from_value(json)?; |
238 | Ok(manifest) | |
247a8ca5 DM |
239 | } |
240 | } | |
59e9ba01 | 241 | |
b53f6379 DM |
242 | |
243 | #[test] | |
244 | fn test_manifest_signature() -> Result<(), Error> { | |
245 | ||
246 | use crate::backup::{KeyDerivationConfig}; | |
247 | ||
248 | let pw = b"test"; | |
249 | ||
250 | let kdf = KeyDerivationConfig::Scrypt { | |
251 | n: 65536, | |
252 | r: 8, | |
253 | p: 1, | |
254 | salt: Vec::new(), | |
255 | }; | |
256 | ||
257 | let testkey = kdf.derive_key(pw)?; | |
258 | ||
259 | let crypt_config = CryptConfig::new(testkey)?; | |
260 | ||
261 | let snapshot: BackupDir = "host/elsa/2020-06-26T13:56:05Z".parse()?; | |
262 | ||
263 | let mut manifest = BackupManifest::new(snapshot); | |
264 | ||
265 | manifest.add_file("test1.img.fidx".into(), 200, [1u8; 32], CryptMode::Encrypt)?; | |
266 | manifest.add_file("abc.blob".into(), 200, [2u8; 32], CryptMode::None)?; | |
267 | ||
268 | manifest.unprotected["note"] = "This is not protected by the signature.".into(); | |
269 | ||
dfa517ad | 270 | let text = manifest.to_string(Some(&crypt_config))?; |
b53f6379 DM |
271 | |
272 | let manifest: Value = serde_json::from_str(&text)?; | |
273 | let signature = manifest["signature"].as_str().unwrap().to_string(); | |
274 | ||
275 | assert_eq!(signature, "d7b446fb7db081662081d4b40fedd858a1d6307a5aff4ecff7d5bf4fd35679e9"); | |
276 | ||
3dacedce | 277 | let manifest: BackupManifest = serde_json::from_value(manifest)?; |
b53f6379 DM |
278 | let expected_signature = proxmox::tools::digest_to_hex(&manifest.signature(&crypt_config)?); |
279 | ||
280 | assert_eq!(signature, expected_signature); | |
281 | ||
282 | Ok(()) | |
283 | } |