From 9325f5773be6fe25a26c1040e4bdbcb50125ec9f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 09:32:07 -0700 Subject: [PATCH 01/13] Rename methods, add private `unwrap_key_for_entry()` --- README.md | 16 ++-- src/backup/mod.rs | 121 +++++++++++++++++-------------- src/backup/models/manifest_db.rs | 38 +++++----- 3 files changed, 97 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index cf8b548..adbad5d 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,10 @@ fn main() -> Result<(), Box> { // Initialize a backup session for a device UDID with a password let udid_folder = "/Users/you/Library/Application Support/MobileSync/Backup/DEVICE_UDID"; let auth = Authentication::Password("your_password".into()); - let backup = Backup::new(udid_folder, &auth)?; + let backup = Backup::open(udid_folder, &auth)?; // List all files in the backup - let entries = backup.get_backup_files_list()?; + let entries = backup.files()?; for entry in &entries { println!("{} - {}/{}", entry.file_id, entry.domain, entry.relative_path); } @@ -58,7 +58,7 @@ fn main() -> Result<(), Box> { } // Get the derived key for use elsewhere: - let derived_key = backup.get_decryption_key_hex(); + let derived_key = backup.decryption_key_hex(); Ok(()) } @@ -75,7 +75,7 @@ fn main() -> Result<(), Box> { let udid_folder = "/path/to/backup"; let hex_key = "abcdef0123456789..."; let auth = Authentication::DerivedKey(hex_key.to_string()); - let backup = Backup::new(udid_folder, &auth)?; + let backup = Backup::open(udid_folder, &auth)?; // ... proceed as normal Ok(()) } @@ -108,7 +108,7 @@ fn main() -> Result<(), Box> { let udid_folder = "/path/to/backup"; let hex_key = "abcdef0123456789..."; let auth = Authentication::DerivedKey(hex_key.to_string()); - let backup = Backup::new(udid_folder, &auth)?; + let backup = Backup::open(udid_folder, &auth)?; println!("Device: {} (iOS {})", backup.lockdown().device_name, @@ -127,13 +127,17 @@ fn main() -> Result<(), Box> { use crabapple::{Backup, Authentication}; use crabapple::error::BackupError; -match Backup::new("/bad/path", &Authentication::Password("pass".into())) { +match Backup::open("/bad/path", &Authentication::Password("pass".into())) { Ok(b) => println!("Loaded backup successfully"), Err(BackupError::ManifestPlistNotFound(path)) => eprintln!("Missing Manifest.plist: {}", path), Err(err) => eprintln!("Error initializing backup: {}", err), } ``` +## Targeted Versions + +This library targets the current latest public release for iOS. It should work with backups from iOS 10.2 or later, but all features may not be available. + ## Crabapple Tree ![My Crabapple Tree](/resources/crabapple.png) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index d0f5155..a29bdd3 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -37,6 +37,7 @@ use crate::{ /// /// Provides methods to initialize, configure, and extract data from a backup, /// including metadata loading, manifest database access, and file decryption. +#[derive(Debug)] pub struct Backup { /// Filesystem path to the specific device backup folder pub backup_path: PathBuf, @@ -64,7 +65,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -72,7 +73,7 @@ impl Backup { /// println!("UDID: {}", backup.udid()?); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn new>(backup_path: P, auth: &Authentication) -> Result { + pub fn open>(backup_path: P, auth: &Authentication) -> Result { let device_backup_path = backup_path.as_ref().to_path_buf(); if !device_backup_path.is_dir() { return Err(BackupError::InvalidBackupRoot( @@ -118,7 +119,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -144,7 +145,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -167,7 +168,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -189,7 +190,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -210,7 +211,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -220,7 +221,7 @@ impl Backup { /// println!("App: {}", app.bundle_id); /// } /// # Ok::<(), crabapple::error::BackupError>(()) - pub fn apps(&self) -> &Vec { + pub fn apps(&self) -> &[Application] { &self.manifest.manifest_data.applications } @@ -235,18 +236,18 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// if let Some(key_hex) = backup.get_decryption_key_hex() { + /// if let Some(key_hex) = backup.decryption_key_hex() { /// println!("Key: {}", key_hex); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` #[must_use] - pub fn get_decryption_key_hex(&self) -> Option { + pub fn decryption_key_hex(&self) -> Option { self.manifest .main_decryption_key .as_ref() @@ -263,18 +264,18 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// if let Some(key) = backup.get_decryption_key() { + /// if let Some(key) = backup.decryption_key() { /// println!("Key: {:?}", key); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` #[must_use] - pub fn get_decryption_key(&self) -> Option { + pub fn decryption_key(&self) -> Option { self.manifest.main_decryption_key.clone() } @@ -306,7 +307,7 @@ impl Backup { /// * `WirelessDomain` /// /// # Returns - /// A [`Vec`] containing each unique domain present in the backup. + /// A [`HashSet`] containing each unique domain present in the backup. /// /// # Errors /// Returns [`BackupError::ManifestDbNotFound`] if the manifest database is unavailable, @@ -317,7 +318,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -335,24 +336,21 @@ impl Backup { /// # Returns /// A [`Path`] pointing to the location of the manifest database file. /// - /// # Errors - /// Returns [`BackupError::ManifestDbNotFound`] if the manifest database information is missing. - /// /// # Examples /// /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// let db_path = backup.get_manifest_db_path(); + /// let db_path = backup.manifest_db_path(); /// println!("Manifest.db path: {:?}", db_path); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn get_manifest_db_path(&self) -> &Path { + pub fn manifest_db_path(&self) -> &Path { &self.manifest_db.db_path } @@ -366,18 +364,18 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; /// - /// let files = backup.get_backup_files_list()?; + /// let files = backup.files()?; /// for file in files { /// println!("{:?}", file); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn get_backup_files_list(&self) -> Result> { + pub fn files(&self) -> Result> { query_all_files(&self.db) } @@ -395,7 +393,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -410,16 +408,18 @@ impl Backup { } /// Access parsed `Manifest.plist` metadata. - #[must_use] /// /// # Returns /// A reference to the parsed [`Manifest`] object. + #[must_use] pub fn manifest(&self) -> &ManifestData { &self.manifest.manifest_data } /// Decrypt the file represented by [`BackupFileEntry`], returning plaintext bytes. /// + /// All operations are performed in memory, and the decrypted data is returned as a byte vector. + /// /// # Arguments /// * `entry` - A [`BackupFileEntry`] containing metadata and encrypted file ID. /// @@ -434,7 +434,7 @@ impl Backup { /// ```no_run /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -450,23 +450,18 @@ impl Backup { .join(&entry.file_id[0..2]) .join(&entry.file_id); - let data = read(&source)?; - - // TODO: this is repeated in `Backup::decrypt_entry_stream`, clean it up - if let Some(encryption_key) = &entry.metadata.encryption_key { - let class_key_entry = self - .manifest - .get_class_key(entry.metadata.protection_class)?; + let ciphertext = read(&source)?; - let key = aes_kw_unwrap(&class_key_entry.key, &encryption_key.file_key)?; - - aes_decrypt_cbc_with_padding(&data, &key) - } else { - Ok(data) + if self.is_encrypted() { + let key = self.unwrap_key_for_entry(entry)?; + return aes_decrypt_cbc_with_padding(&ciphertext, &key); } + Err(BackupError::NotEncrypted) } - /// Decrypt a file stream using AES-256-CBC with PKCS7 padding. + /// Decrypt the file represented by [`BackupFileEntry`], returning a streaming reader. + /// + /// All operations are streamed from the disk, and the decrypted data is returned as a reader. /// /// # Arguments /// * `entry` - A [`BackupFileEntry`] containing metadata and encrypted file ID. @@ -483,7 +478,7 @@ impl Backup { /// use std::{fs::File, io::copy}; /// use crabapple::{Backup, Authentication}; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()), /// )?; @@ -500,18 +495,38 @@ impl Backup { ) -> Result>> { let ciphertext = File::open(self.backup_path.join(entry.source()))?; - // TODO: this is repeated in `Backup::decrypt_entry`, clean it up - if let Some(encryption_key) = &entry.metadata.encryption_key { - let class_key_entry = self - .manifest - .get_class_key(entry.metadata.protection_class)?; - - let key = aes_kw_unwrap(&class_key_entry.key, &encryption_key.file_key)?; - + if self.is_encrypted() { + let key = self.unwrap_key_for_entry(entry)?; return AesCbcDecryptReader::from(ciphertext, &key); } - Err(BackupError::KeyUnwrapFailed( - entry.metadata.protection_class, - )) + Err(BackupError::NotEncrypted) + } + + /// Unwrap the encryption key for a specific file entry. + /// + /// # Arguments + /// * `entry` - A [`BackupFileEntry`] containing metadata and encrypted file ID. + /// + /// # Returns + /// A streaming reader implementing `std::io::Read` that yields plaintext as it's read. + /// + /// # Errors + /// Returns [`BackupError::Crypto`] on decryption errors or missing keys. + fn unwrap_key_for_entry(&self, entry: &BackupFileEntry) -> Result { + let class_key_entry = self + .manifest + .get_class_key(entry.metadata.protection_class)?; + + let key = aes_kw_unwrap( + &class_key_entry.key, + &entry + .metadata + .encryption_key + .as_ref() + .ok_or(BackupError::NotEncrypted)? + .file_key, + )?; + + Ok(key) } } diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index 22014c8..17b2848 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -23,6 +23,7 @@ use crate::{ }; /// 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, @@ -50,12 +51,12 @@ impl ManifestDb { /// use crabapple::{Backup, Authentication}; /// use crabapple::backup::models::manifest_db; /// - /// let backup = Backup::new( + /// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()) /// )?; /// - /// let db_path = backup.get_manifest_db_path(); + /// let db_path = backup.manifest_db_path(); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn new(db_path: &Path, manifest: &Manifest) -> Result { @@ -64,16 +65,17 @@ impl ManifestDb { } 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(), - ) - })?; + 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 @@ -83,11 +85,9 @@ impl ManifestDb { // 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. - // TODO: this is repeated in `Backup::decrypt_entry`, clean it up let manifest_file_key = FileKeyPair::new(manifest_key_bytes)?; - let class_key_entry = - manifest.get_class_key(manifest_file_key.protection_class_id)?; + 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))?; @@ -96,7 +96,7 @@ impl ManifestDb { let db_bytes = File::open(db_path)?; let mut decrypted_manifest_db_stream = AesCbcDecryptReader::from(&db_bytes, &key)?; - // Write decrypted Manifest.db into the platform-specific temporary directory + // 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)?; @@ -164,7 +164,7 @@ impl Drop for ManifestDb { /// use crabapple::{Backup, Authentication}; /// use crabapple::backup::models::manifest_db; /// -/// let backup = Backup::new( +/// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()) /// )?; @@ -206,7 +206,7 @@ pub fn query_all_domains(conn: &Connection) -> Result> { /// use crabapple::{Backup, Authentication}; /// use crabapple::backup::models::manifest_db; /// -/// let backup = Backup::new( +/// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()) /// )?; @@ -263,7 +263,7 @@ pub fn query_all_files(conn: &Connection) -> Result> { /// use crabapple::{Backup, Authentication}; /// use crabapple::backup::models::manifest_db; /// -/// let backup = Backup::new( +/// let backup = Backup::open( /// "/path/to/backup", /// &Authentication::Password("pass".into()) /// )?; From c3a8420f3035cdafc63a63ad62115d44d109b72e Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 13:07:33 -0700 Subject: [PATCH 02/13] Rename function, use correct errors --- README.md | 2 +- src/backup/mod.rs | 12 ++++++------ src/backup/models/manifest_db.rs | 13 +++++++------ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index adbad5d..8d93f40 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ fn main() -> Result<(), Box> { let backup = Backup::open(udid_folder, &auth)?; // List all files in the backup - let entries = backup.files()?; + let entries = backup.entries()?; for entry in &entries { println!("{} - {}/{}", entry.file_id, entry.domain, entry.relative_path); } diff --git a/src/backup/mod.rs b/src/backup/mod.rs index a29bdd3..49e02bf 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -26,7 +26,7 @@ use crate::{ lockdown::ManifestLockdownInfo, manifest_plist::{Manifest, ManifestData}, }, - manifest_db::{ManifestDb, query_all_domains, query_all_files, query_file_by_id}, + manifest_db::{ManifestDb, query_all_domains, query_all_entries, query_file_by_id}, }, util::hex::hex_encode, }, @@ -369,14 +369,14 @@ impl Backup { /// &Authentication::Password("pass".into()), /// )?; /// - /// let files = backup.files()?; - /// for file in files { - /// println!("{:?}", file); + /// let entries = backup.entries()?; + /// for entry in entries { + /// println!("{:?}", entry); /// } /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` - pub fn files(&self) -> Result> { - query_all_files(&self.db) + pub fn entries(&self) -> Result> { + query_all_entries(&self.db) } /// Get a single file entry by its file ID. diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index 17b2848..ec1dde6 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -211,11 +211,11 @@ pub fn query_all_domains(conn: &Connection) -> Result> { /// &Authentication::Password("pass".into()) /// )?; /// -/// let files = manifest_db::query_all_files(&backup.db)?; -/// println!("File count: {}", files.len()); +/// let entries = manifest_db::query_all_entries(&backup.db)?; +/// println!("File count: {}", entries.len()); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` -pub fn query_all_files(conn: &Connection) -> Result> { +pub fn query_all_entries(conn: &Connection) -> Result> { let mut stmt = conn.prepare("SELECT rowid, fileID, domain, relativePath, flags, file FROM Files")?; let mut rows = stmt.query([])?; @@ -227,11 +227,12 @@ pub fn query_all_files(conn: &Connection) -> Result> { .blob_open(rusqlite::DatabaseName::Main, "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 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::InvalidTlvData("Failed to parse MBFile from plist".to_string()) + BackupError::PlistParseError("Failed to parse `MBFile` from plist".to_string()) })?; entries.push(BackupFileEntry { From 27568883c1520b7dbcbc04610e259c3a2864c154 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 17:10:51 -0700 Subject: [PATCH 03/13] Remove drop --- src/backup/models/manifest_db.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index ec1dde6..92fa00b 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -135,21 +135,6 @@ impl ManifestDb { } } -impl Drop for ManifestDb { - fn drop(&mut self) { - if self.is_temporary { - // 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 - ); - } - } - } -} - /// Query all unique domains present in the `Manifest.db`. /// /// # Arguments From f6187dfe04e8aab6a2a8b8852aae9e6f27cab868 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 17:10:59 -0700 Subject: [PATCH 04/13] Add `DatabaseClosed` Error type --- src/error.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/error.rs b/src/error.rs index c3971f5..d967377 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,9 @@ pub enum BackupError { /// `SQLite` database error (e.g., opening or querying Manifest.db). Database(rusqlite::Error), + /// The database was already closed + DatabaseClosed, + /// Cryptographic operation failed, with a descriptive message. Crypto(String), @@ -77,6 +80,7 @@ impl fmt::Display for BackupError { match self { BackupError::Io(err) => write!(f, "IO error: {err}"), BackupError::Database(err) => write!(f, "SQLite database error: {err}"), + BackupError::DatabaseClosed => write!(f, "Manifest.db was already closed!"), BackupError::Crypto(msg) => write!(f, "Cryptography error: {msg}"), BackupError::ConversionFailed(why) => { write!(f, "Conversion failed: {why}") From 607499df5fdb76d33c1cc738d6b1cd2f1a401833 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 17:11:24 -0700 Subject: [PATCH 05/13] Properly remove temporary manifest on drop --- src/backup/mod.rs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 49e02bf..4881690 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod util; use std::{ collections::HashSet, - fs::{File, read}, + fs::{File, read, remove_file}, io::BufReader, path::{Path, PathBuf}, }; @@ -46,7 +46,7 @@ pub struct Backup { /// Decrypted manifest database details pub manifest_db: ManifestDb, /// Connection to the manifest database - pub db: Connection, + pub db: Option, } impl Backup { @@ -105,7 +105,7 @@ impl Backup { backup_path: device_backup_path, manifest, manifest_db, - db: conn, + db: Some(conn), }) } @@ -328,7 +328,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn query_all_domains(&self) -> Result> { - query_all_domains(&self.db) + query_all_domains(self.db.as_ref().ok_or(BackupError::DatabaseClosed)?) } /// Get the filesystem path to the decrypted (or raw) `Manifest.db` file. @@ -376,7 +376,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn entries(&self) -> Result> { - query_all_entries(&self.db) + query_all_entries(self.db.as_ref().ok_or(BackupError::DatabaseClosed)?) } /// Get a single file entry by its file ID. @@ -403,8 +403,11 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn get_file(&self, file_id: &str) -> Result { - query_file_by_id(&self.db, file_id)? - .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) + query_file_by_id( + self.db.as_ref().ok_or(BackupError::DatabaseClosed)?, + file_id, + )? + .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) } /// Access parsed `Manifest.plist` metadata. @@ -530,3 +533,22 @@ impl Backup { Ok(key) } } + +impl Drop for Backup { + fn drop(&mut self) { + if self.manifest_db.is_temporary { + if let Some(conn) = self.db.take() { + conn.close().ok(); + + // Remove the file, ignoring errors if any + if let Err(e) = remove_file(&self.manifest_db.db_path) { + eprintln!( + "warning: failed to remove temporary `Manifest.db` file at {}: {}", + self.manifest_db.db_path.display(), + e + ); + } + } + } + } +} From 56b881c2af73f36f0027bdd1a07ffb40431cb8e5 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 17:17:45 -0700 Subject: [PATCH 06/13] Update examples --- src/backup/models/manifest_db.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index 92fa00b..9d48213 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -2,7 +2,7 @@ use std::{ collections::HashSet, - fs::{File, remove_file}, + fs::File, io::copy, path::{Path, PathBuf}, }; @@ -154,7 +154,7 @@ impl ManifestDb { /// &Authentication::Password("pass".into()) /// )?; /// -/// let domains = manifest_db::query_all_domains(&backup.db)?; +/// let domains = manifest_db::query_all_domains(backup.db.as_ref().unwrap())?; /// println!("Domains: {:?}", domains); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` @@ -196,7 +196,7 @@ pub fn query_all_domains(conn: &Connection) -> Result> { /// &Authentication::Password("pass".into()) /// )?; /// -/// let entries = manifest_db::query_all_entries(&backup.db)?; +/// let entries = manifest_db::query_all_entries(backup.db.as_ref().unwrap())?; /// println!("File count: {}", entries.len()); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` @@ -254,7 +254,7 @@ pub fn query_all_entries(conn: &Connection) -> Result> { /// &Authentication::Password("pass".into()) /// )?; /// -/// if let Some(entry) = manifest_db::query_file_by_id(&backup.db, "fileid")? { +/// if let Some(entry) = manifest_db::query_file_by_id(backup.db.as_ref().unwrap(), "fileid")? { /// println!("Found file: {}", entry.file_id); /// } /// # Ok::<(), crabapple::error::BackupError>(()) From 70cc59bb24f9622d15e907f8a369ee1b90e3b22a Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 17:25:59 -0700 Subject: [PATCH 07/13] Remove warning --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 8d93f40..9f02705 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ Inspired by [`imessage-exporter`](https://github.com/ReagentX/imessage-exporter), `crabapple` provides a flexible foundation for any project that needs to access iOS backup data. -## ⚠️ Warning ⚠️ - -This library is currently in beta state and should not be used in production code. - ## Features - Load and parse the backup's `Manifest.plist` to obtain metadata, device info, and encryption parameters From e48c0e0297744cce6dc884bd3f8e6caf8ecf8502 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 20:15:04 -0700 Subject: [PATCH 08/13] Add function to get the current manifest database connection --- src/backup/mod.rs | 32 +++++++++++++++++++++++++------- src/backup/models/manifest_db.rs | 6 +++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 4881690..0f5e02f 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -109,6 +109,27 @@ impl Backup { }) } + /// Returns the current database connection, if available. + /// + /// # Returns + /// An [`Result`] representing the current database connection. + /// + /// # Examples + /// ```no_run + /// use crabapple::{Backup, Authentication}; + /// + /// let backup = Backup::open( + /// "/path/to/backup", + /// &Authentication::Password("pass".into()), + /// )?; + /// + /// let db = backup.db()?; + /// println!("Database connection: {:?}", db); + /// # Ok::<(), crabapple::error::BackupError>(()) + pub fn db(&self) -> Result<&Connection> { + self.db.as_ref().ok_or(BackupError::DatabaseClosed) + } + /// Returns the current device `UDID` (the backup folder name). /// /// # Errors @@ -328,7 +349,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn query_all_domains(&self) -> Result> { - query_all_domains(self.db.as_ref().ok_or(BackupError::DatabaseClosed)?) + query_all_domains(self.db()?) } /// Get the filesystem path to the decrypted (or raw) `Manifest.db` file. @@ -376,7 +397,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn entries(&self) -> Result> { - query_all_entries(self.db.as_ref().ok_or(BackupError::DatabaseClosed)?) + query_all_entries(self.db()?) } /// Get a single file entry by its file ID. @@ -403,11 +424,8 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn get_file(&self, file_id: &str) -> Result { - query_file_by_id( - self.db.as_ref().ok_or(BackupError::DatabaseClosed)?, - file_id, - )? - .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) + query_file_by_id(self.db()?, file_id)? + .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) } /// Access parsed `Manifest.plist` metadata. diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index 9d48213..2b464c3 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -154,7 +154,7 @@ impl ManifestDb { /// &Authentication::Password("pass".into()) /// )?; /// -/// let domains = manifest_db::query_all_domains(backup.db.as_ref().unwrap())?; +/// let domains = manifest_db::query_all_domains(backup.db().unwrap())?; /// println!("Domains: {:?}", domains); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` @@ -196,7 +196,7 @@ pub fn query_all_domains(conn: &Connection) -> Result> { /// &Authentication::Password("pass".into()) /// )?; /// -/// let entries = manifest_db::query_all_entries(backup.db.as_ref().unwrap())?; +/// let entries = manifest_db::query_all_entries(backup.db().unwrap())?; /// println!("File count: {}", entries.len()); /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` @@ -254,7 +254,7 @@ pub fn query_all_entries(conn: &Connection) -> Result> { /// &Authentication::Password("pass".into()) /// )?; /// -/// if let Some(entry) = manifest_db::query_file_by_id(backup.db.as_ref().unwrap(), "fileid")? { +/// if let Some(entry) = manifest_db::query_file_by_id(backup.db().unwrap(), "fileid")? { /// println!("Found file: {}", entry.file_id); /// } /// # Ok::<(), crabapple::error::BackupError>(()) From a39592209eeed71bb44376f8019c09de40b07586 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 20:26:45 -0700 Subject: [PATCH 09/13] Move manifest database queries to the `ManifestDb` struct; move `Drop` for cleanup --- src/backup/mod.rs | 40 +--- src/backup/models/manifest_db.rs | 337 +++++++++++++++++-------------- 2 files changed, 195 insertions(+), 182 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 0f5e02f..32bfc7b 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod util; use std::{ collections::HashSet, - fs::{File, read, remove_file}, + fs::{File, read}, io::BufReader, path::{Path, PathBuf}, }; @@ -26,7 +26,7 @@ use crate::{ lockdown::ManifestLockdownInfo, manifest_plist::{Manifest, ManifestData}, }, - manifest_db::{ManifestDb, query_all_domains, query_all_entries, query_file_by_id}, + manifest_db::ManifestDb, }, util::hex::hex_encode, }, @@ -45,8 +45,6 @@ pub struct Backup { pub manifest: Manifest, /// Decrypted manifest database details pub manifest_db: ManifestDb, - /// Connection to the manifest database - pub db: Option, } impl Backup { @@ -98,18 +96,14 @@ impl Backup { let manifest = Manifest::from_manifest_data(manifest_data, auth)?; let manifest_db = ManifestDb::new(&device_backup_path.join("Manifest.db"), &manifest)?; - // Create a connection to the manifest database - let conn = manifest_db.try_get_connection()?; - Ok(Self { backup_path: device_backup_path, manifest, manifest_db, - db: Some(conn), }) } - /// Returns the current database connection, if available. + /// Returns the current manifest database connection, if available. /// /// # Returns /// An [`Result`] representing the current database connection. @@ -127,7 +121,7 @@ impl Backup { /// println!("Database connection: {:?}", db); /// # Ok::<(), crabapple::error::BackupError>(()) pub fn db(&self) -> Result<&Connection> { - self.db.as_ref().ok_or(BackupError::DatabaseClosed) + self.manifest_db.db() } /// Returns the current device `UDID` (the backup folder name). @@ -349,7 +343,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn query_all_domains(&self) -> Result> { - query_all_domains(self.db()?) + self.manifest_db.query_all_domains() } /// Get the filesystem path to the decrypted (or raw) `Manifest.db` file. @@ -397,7 +391,7 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn entries(&self) -> Result> { - query_all_entries(self.db()?) + self.manifest_db.query_all_entries() } /// Get a single file entry by its file ID. @@ -424,7 +418,8 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn get_file(&self, file_id: &str) -> Result { - query_file_by_id(self.db()?, file_id)? + self.manifest_db + .query_file_by_id(file_id)? .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) } @@ -551,22 +546,3 @@ impl Backup { Ok(key) } } - -impl Drop for Backup { - fn drop(&mut self) { - if self.manifest_db.is_temporary { - if let Some(conn) = self.db.take() { - conn.close().ok(); - - // Remove the file, ignoring errors if any - if let Err(e) = remove_file(&self.manifest_db.db_path) { - eprintln!( - "warning: failed to remove temporary `Manifest.db` file at {}: {}", - self.manifest_db.db_path.display(), - e - ); - } - } - } - } -} diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index 2b464c3..b512287 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -2,7 +2,7 @@ use std::{ collections::HashSet, - fs::File, + fs::{File, remove_file}, io::copy, path::{Path, PathBuf}, }; @@ -33,6 +33,8 @@ pub struct ManifestDb { pub connection_string: String, /// Optional hex-encoded decryption key used to decrypt the database. pub decryption_key: Option, + /// Connection to the manifest database + pub conn: Option, } impl ManifestDb { @@ -106,10 +108,11 @@ impl ManifestDb { })?; Self { - db_path: tmp_path, + 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 { @@ -117,176 +120,210 @@ impl ManifestDb { 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) } - /// Open a `SQLite` connection to the manifest database. + /// Returns the current manifest database connection, if available. /// /// # Returns - /// A [`rusqlite::Connection`] to the database file specified by `db_path`. + /// An [`Result`] representing the current database connection. /// - /// # Errors - /// Returns [`BackupError::Database`] if opening the connection fails. - pub fn try_get_connection(&self) -> Result { - rusqlite::Connection::open(&self.db_path).map_err(BackupError::Database) + /// # Examples + /// ```no_run + /// use crabapple::{Backup, Authentication}; + /// + /// let backup = Backup::open( + /// "/path/to/backup", + /// &Authentication::Password("pass".into()), + /// )?; + /// + /// let db = backup.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 = manifest_db::query_all_domains(backup.db().unwrap())?; -/// println!("Domains: {:?}", domains); -/// # Ok::<(), crabapple::error::BackupError>(()) -/// ``` -pub fn query_all_domains(conn: &Connection) -> Result> { - let mut stmt = conn.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)?); + /// 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> { + 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) } - 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 = manifest_db::query_all_entries(backup.db().unwrap())?; -/// println!("File count: {}", entries.len()); -/// # Ok::<(), crabapple::error::BackupError>(()) -/// ``` -pub fn query_all_entries(conn: &Connection) -> Result> { - let mut stmt = - conn.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)?; + /// 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> { + 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 = conn - .blob_open(rusqlite::DatabaseName::Main, "Files", "file", file_id, true) - .map_err(BackupError::Database)?; + let blob = self + .db()? + .blob_open(rusqlite::DatabaseName::Main, "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 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()) - })?; + 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 - }); + 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) } - 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) = manifest_db::query_file_by_id(backup.db().unwrap(), "fileid")? { -/// println!("Found file: {}", entry.file_id); -/// } -/// # Ok::<(), crabapple::error::BackupError>(()) -/// ``` -pub fn query_file_by_id(conn: &Connection, path: &str) -> Result> { - // Path in DB is typically Domain-RelativePath - let mut stmt = conn.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)?; + /// 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> { + // 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 = conn - .blob_open(rusqlite::DatabaseName::Main, "Files", "file", file_id, true) - .map_err(BackupError::Database)?; + let blob = self + .db()? + .blob_open(rusqlite::DatabaseName::Main, "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 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) + } + } +} - let mbfile = MBFile::from_plist(&plist).map_err(|_| { - BackupError::InvalidTlvData("Failed to parse MBFile from plist".to_string()) - })?; +impl Drop for ManifestDb { + fn drop(&mut self) { + if self.is_temporary { + if let Some(conn) = self.conn.take() { + conn.close().ok(); - 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) + // 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 + ); + } + } + } } } From 2eb4bead593896df8e554ebf91e576ed75b72187 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 20:35:51 -0700 Subject: [PATCH 10/13] Add docs --- src/backup/mod.rs | 3 +++ src/backup/models/manifest_db.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 32bfc7b..f60d243 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -108,6 +108,9 @@ impl Backup { /// # Returns /// An [`Result`] representing the current database connection. /// + /// # Errors + /// Returns [`BackupError::DatabaseClosed`] if the manifest database connection is closed. + /// /// # Examples /// ```no_run /// use crabapple::{Backup, Authentication}; diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index b512287..de13881 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -132,6 +132,9 @@ impl ManifestDb { /// # Returns /// An [`Result`] representing the current database connection. /// + /// # Errors + /// Returns [`BackupError::DatabaseClosed`] if the manifest database connection is closed. + /// /// # Examples /// ```no_run /// use crabapple::{Backup, Authentication}; From 3a3567791938a2c13b98d3057941fc7eb02b8b52 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 20:36:42 -0700 Subject: [PATCH 11/13] Remove unused --- src/backup/mod.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index f60d243..981aeaa 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -103,30 +103,6 @@ impl Backup { }) } - /// Returns the current manifest database connection, if available. - /// - /// # Returns - /// An [`Result`] 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.db()?; - /// println!("Database connection: {:?}", db); - /// # Ok::<(), crabapple::error::BackupError>(()) - pub fn db(&self) -> Result<&Connection> { - self.manifest_db.db() - } - /// Returns the current device `UDID` (the backup folder name). /// /// # Errors From 4b456e38edea81a285212b3ae50e2b3e27adc1c2 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 20:37:40 -0700 Subject: [PATCH 12/13] Fix docs --- src/backup/mod.rs | 2 -- src/backup/models/manifest_db.rs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 981aeaa..f75210d 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -12,8 +12,6 @@ use std::{ path::{Path, PathBuf}, }; -use rusqlite::Connection; - use crate::{ backup::{ crypto::{AesCbcDecryptReader, aes_decrypt_cbc_with_padding, aes_kw_unwrap}, diff --git a/src/backup/models/manifest_db.rs b/src/backup/models/manifest_db.rs index de13881..c8485dd 100644 --- a/src/backup/models/manifest_db.rs +++ b/src/backup/models/manifest_db.rs @@ -144,7 +144,7 @@ impl ManifestDb { /// &Authentication::Password("pass".into()), /// )?; /// - /// let db = backup.db()?; + /// let db = backup.manifest_db.db()?; /// println!("Database connection: {:?}", db); /// # Ok::<(), crabapple::error::BackupError>(()) pub fn db(&self) -> Result<&Connection> { From c97d9120589221cff646bae297d346a3a1ce862e Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 20 May 2025 20:44:33 -0700 Subject: [PATCH 13/13] Fail early in decryption methods if the backup is not encrypted --- src/backup/mod.rs | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/backup/mod.rs b/src/backup/mod.rs index f75210d..40df327 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -400,15 +400,6 @@ impl Backup { .ok_or_else(|| BackupError::FileNotFoundInBackup(file_id.to_string())) } - /// Access parsed `Manifest.plist` metadata. - /// - /// # Returns - /// A reference to the parsed [`Manifest`] object. - #[must_use] - pub fn manifest(&self) -> &ManifestData { - &self.manifest.manifest_data - } - /// Decrypt the file represented by [`BackupFileEntry`], returning plaintext bytes. /// /// All operations are performed in memory, and the decrypted data is returned as a byte vector. @@ -438,6 +429,10 @@ impl Backup { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn decrypt_entry(&self, entry: &BackupFileEntry) -> Result> { + if !self.is_encrypted() { + return Err(BackupError::NotEncrypted); + } + let source = self .backup_path .join(&entry.file_id[0..2]) @@ -445,11 +440,8 @@ impl Backup { let ciphertext = read(&source)?; - if self.is_encrypted() { - let key = self.unwrap_key_for_entry(entry)?; - return aes_decrypt_cbc_with_padding(&ciphertext, &key); - } - Err(BackupError::NotEncrypted) + let key = self.unwrap_key_for_entry(entry)?; + aes_decrypt_cbc_with_padding(&ciphertext, &key) } /// Decrypt the file represented by [`BackupFileEntry`], returning a streaming reader. @@ -486,13 +478,14 @@ impl Backup { &self, entry: &BackupFileEntry, ) -> Result>> { + if !self.is_encrypted() { + return Err(BackupError::NotEncrypted); + } + let ciphertext = File::open(self.backup_path.join(entry.source()))?; - if self.is_encrypted() { - let key = self.unwrap_key_for_entry(entry)?; - return AesCbcDecryptReader::from(ciphertext, &key); - } - Err(BackupError::NotEncrypted) + let key = self.unwrap_key_for_entry(entry)?; + AesCbcDecryptReader::from(ciphertext, &key) } /// Unwrap the encryption key for a specific file entry.