1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
//! Module for loading, decrypting, and querying the `Manifest.db` of an iOS backup.
use std::{
collections::HashSet,
fs::{File, remove_file},
io::copy,
path::{Path, PathBuf},
};
use plist::Value;
use rusqlite::Connection;
use crate::{
backup::{
crypto::{AesCbcDecryptReader, aes_kw_unwrap},
models::{
file::{BackupFileEntry, FileKeyPair, MBFile},
manifest::manifest_plist::Manifest,
},
util::hex::hex_encode,
},
error::{BackupError, Result},
};
/// Represents the backup's `Manifest.db`, decrypted if necessary, and holds decryption info.
#[derive(Debug)]
pub struct ManifestDb {
/// Path to the `SQLite` database file.
pub db_path: PathBuf,
/// Whether `db_path` points to a temporary decrypted file.
pub is_temporary: bool,
/// Connection string (usually the file path).
pub connection_string: String,
/// Optional hex-encoded decryption key used to decrypt the database.
pub decryption_key: Option<String>,
/// Connection to the manifest database
pub conn: Option<Connection>,
}
impl ManifestDb {
/// Open (and decrypt if necessary) the backup's `Manifest.db`, returning a [`ManifestDb`].
///
/// # Arguments
/// * `db_path` - Filesystem path to the `Manifest.db` file.
/// * `manifest_data` - Data derived from the backup's `Manifest.plist`.
///
/// # Errors
/// Returns [`BackupError::ManifestDbNotFound`] if the DB file is missing, or [`BackupError::Crypto`] on decryption errors.
///
/// # Examples
///
/// ```no_run
/// use crabapple::{Backup, Authentication};
/// use crabapple::backup::models::manifest_db;
///
/// let backup = Backup::open(
/// "/path/to/backup",
/// &Authentication::Password("pass".into())
/// )?;
///
/// let db_path = backup.manifest_db_path();
/// # Ok::<(), crabapple::error::BackupError>(())
/// ```
pub fn new(db_path: &Path, manifest: &Manifest) -> Result<Self> {
if !db_path.exists() {
return Err(BackupError::ManifestDbNotFound);
}
let decrypted_db_info = if manifest.manifest_data.is_encrypted {
let manifest_key_bytes =
manifest
.manifest_data
.manifest_key
.as_ref()
.ok_or_else(|| {
BackupError::Crypto(
"ManifestKey data not found in PlistInfo for encrypted Manifest.db"
.to_string(),
)
})?;
// The first 4 bytes of `manifest_key_bytes` are interpreted as a little-endian
// `u32` protection class identifier. The remainder is treated as an AES-key-wrapped
// file key (RFC 3394).
//
// 1. Parse out the protection class ID.
// 2. Look up the corresponding unwrapped class key in `class_keys`.
// 3. Unwrap the file-specific AES key using AES-Key-Wrap.
// 4. Decrypt `ciphertext` with AES-256-CBC (zero IV), stripping PKCS#7 padding.
let manifest_file_key = FileKeyPair::new(manifest_key_bytes)?;
let class_key_entry = manifest.get_class_key(manifest_file_key.protection_class_id)?;
let key = aes_kw_unwrap(&class_key_entry.key, &manifest_file_key.file_key)
.map_err(|_| BackupError::KeyUnwrapFailed(manifest_file_key.protection_class_id))?;
// Decrypt the Manifest.db using the unwrapped key
let db_bytes = File::open(db_path)?;
let mut decrypted_manifest_db_stream = AesCbcDecryptReader::from(&db_bytes, &key)?;
// Write decrypted Manifest.db into a platform-specific temporary directory
let tmp_path = std::env::temp_dir().join("crabapple-Manifest.db");
let mut file = File::create(&tmp_path)?;
// Stream-decrypt directly into the temp file
copy(&mut decrypted_manifest_db_stream, &mut file).map_err(|e| {
BackupError::Crypto(format!("Failed writing decrypted Manifest.db: {e}"))
})?;
Self {
db_path: tmp_path.clone(),
is_temporary: true,
connection_string: db_path.to_string_lossy().into_owned(), // Path for direct open
decryption_key: Some(hex_encode(&key)),
conn: Some(Connection::open(tmp_path).map_err(BackupError::Database)?),
}
} else {
Self {
db_path: db_path.to_path_buf(),
is_temporary: false,
connection_string: db_path.to_string_lossy().into_owned(),
decryption_key: None,
conn: Some(Connection::open(db_path).map_err(BackupError::Database)?),
}
};
Ok(decrypted_db_info)
}
/// Returns the current manifest database connection, if available.
///
/// # Returns
/// An [`Result<Connection>`] representing the current database connection.
///
/// # Errors
/// Returns [`BackupError::DatabaseClosed`] if the manifest database connection is closed.
///
/// # Examples
/// ```no_run
/// use crabapple::{Backup, Authentication};
///
/// let backup = Backup::open(
/// "/path/to/backup",
/// &Authentication::Password("pass".into()),
/// )?;
///
/// let db = backup.manifest_db.db()?;
/// println!("Database connection: {:?}", db);
/// # Ok::<(), crabapple::error::BackupError>(())
pub fn db(&self) -> Result<&Connection> {
self.conn.as_ref().ok_or(BackupError::DatabaseClosed)
}
/// Query all unique domains present in the `Manifest.db`.
///
/// # Arguments
/// * `conn` - An open [`rusqlite::Connection`] to the manifest database.
///
/// # Errors
/// Returns `BackupError::Database` on query failures.
///
/// # Examples
///
/// ```no_run
/// use crabapple::{Backup, Authentication};
/// use crabapple::backup::models::manifest_db;
///
/// let backup = Backup::open(
/// "/path/to/backup",
/// &Authentication::Password("pass".into())
/// )?;
///
/// let domains = backup.manifest_db.query_all_domains()?;
/// println!("Domains: {:?}", domains);
/// # Ok::<(), crabapple::error::BackupError>(())
/// ```
pub fn query_all_domains(&self) -> Result<HashSet<String>> {
let mut stmt = self.db()?.prepare(
"SELECT DISTINCT
CASE
WHEN INSTR(domain, '-') > 0
THEN SUBSTR(domain, 1, INSTR(domain, '-') - 1)
ELSE
domain
END AS domain
FROM Files;",
)?;
let mut rows = stmt.query([])?;
let mut domains = HashSet::new();
while let Some(row) = rows.next()? {
domains.insert(row.get(0)?);
}
Ok(domains)
}
/// Query all file entries from the `Manifest.db`.
///
/// # Arguments
/// * `conn` - An open rusqlite `Connection`.
///
/// # Errors
/// Returns [`BackupError::Database`] if the `SQL` query or blob reading fails.
///
/// # Examples
///
/// ```no_run
/// use crabapple::{Backup, Authentication};
/// use crabapple::backup::models::manifest_db;
///
/// let backup = Backup::open(
/// "/path/to/backup",
/// &Authentication::Password("pass".into())
/// )?;
///
/// let entries = backup.manifest_db.query_all_entries()?;
/// println!("File count: {}", entries.len());
/// # Ok::<(), crabapple::error::BackupError>(())
/// ```
pub fn query_all_entries(&self) -> Result<Vec<BackupFileEntry>> {
let mut stmt = self
.db()?
.prepare("SELECT rowid, fileID, domain, relativePath, flags, file FROM Files")?;
let mut rows = stmt.query([])?;
let mut entries = Vec::new();
while let Some(row) = rows.next()? {
let file_id = row.get(0)?;
let blob = self
.db()?
.blob_open(rusqlite::MAIN_DB, "Files", "file", file_id, true)
.map_err(BackupError::Database)?;
let plist = Value::from_reader(blob).map_err(|_| {
BackupError::PlistParseError("Failed to parse `file` plist".to_string())
})?;
let mbfile = MBFile::from_plist(&plist).map_err(|_| {
BackupError::PlistParseError("Failed to parse `MBFile` from plist".to_string())
})?;
entries.push(BackupFileEntry {
file_id: row.get(1)?,
domain: row.get(2)?,
relative_path: row.get(3)?,
flags: row.get(4)?,
metadata: mbfile, // Store the plist as metadata
});
}
Ok(entries)
}
/// Query a single file entry by its file ID in the `Manifest.db`.
///
/// # Arguments
/// * `conn` - An open rusqlite `Connection`.
/// * `path` - The `fileID` to look up in the `Files` table.
///
/// # Returns
/// `Ok(Some(entry))` if found, `Ok(None)` if not found.
///
/// # Errors
/// Returns [`BackupError::Database`] on query failures.
///
/// # Examples
///
/// ```no_run
/// use crabapple::{Backup, Authentication};
/// use crabapple::backup::models::manifest_db;
///
/// let backup = Backup::open(
/// "/path/to/backup",
/// &Authentication::Password("pass".into())
/// )?;
///
/// if let Some(entry) = backup.manifest_db.query_file_by_id("fileid")? {
/// println!("Found file: {}", entry.file_id);
/// }
/// # Ok::<(), crabapple::error::BackupError>(())
/// ```
pub fn query_file_by_id(&self, path: &str) -> Result<Option<BackupFileEntry>> {
// Path in DB is typically Domain-RelativePath
let mut stmt = self.db()?.prepare(
"SELECT rowid, fileID, domain, relativePath, flags, file FROM Files WHERE fileID = ?",
)?;
let mut rows = stmt.query([path])?;
if let Some(row) = rows.next()? {
let file_id = row.get(0)?;
let blob = self
.db()?
.blob_open(rusqlite::MAIN_DB, "Files", "file", file_id, true)
.map_err(BackupError::Database)?;
let plist = Value::from_reader(blob).map_err(|_| {
BackupError::InvalidTlvData("Failed to parse file plist".to_string())
})?;
let mbfile = MBFile::from_plist(&plist).map_err(|_| {
BackupError::InvalidTlvData("Failed to parse MBFile from plist".to_string())
})?;
Ok(Some(BackupFileEntry {
file_id: row.get(1)?,
domain: row.get(2)?,
relative_path: row.get(3)?,
flags: row.get(4)?,
metadata: mbfile, // Store the plist as metadata
}))
} else {
Ok(None)
}
}
}
impl Drop for ManifestDb {
fn drop(&mut self) {
if self.is_temporary
&& let Some(conn) = self.conn.take()
{
conn.close().ok();
// Remove the file, ignoring errors if any
if let Err(e) = remove_file(&self.db_path) {
eprintln!(
"warning: failed to remove temporary `Manifest.db` file at {}: {}",
self.db_path.display(),
e
);
}
}
}
}