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