diff --git a/Cargo.lock b/Cargo.lock index b30dea1..468edcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bitflags" @@ -69,18 +69,18 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.24" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cipher" @@ -181,9 +181,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "foldhash", ] @@ -234,9 +234,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libsqlite3-sys" @@ -251,9 +251,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "num-conv" @@ -292,9 +292,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed" dependencies = [ "base64", "indexmap", @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] @@ -406,9 +406,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "subtle" @@ -418,9 +418,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", diff --git a/src/backup/crypto.rs b/src/backup/crypto.rs index 367b781..92f81d6 100644 --- a/src/backup/crypto.rs +++ b/src/backup/crypto.rs @@ -20,7 +20,7 @@ use sha2::Sha256; use crate::{ backup::models::{ file::WrappedKey, - keyring::{EncryptionKey, ProtectionClassKey}, + keyring::{ClassKeyData, EncryptionKey, ProtectionClassKey}, manifest::manifest_plist::ManifestData, }, error::{BackupError, Result}, @@ -108,40 +108,43 @@ pub(crate) fn unlock_keys_from_manifest( .as_ref() .ok_or_else(|| BackupError::Crypto("BackupKeyBag not found in PlistInfo".to_string()))?; - for (class_id, class_key_data) in &key_ring.class_keys { - // Skip classes without WPKY - let Some(wpky) = &class_key_data.wpky else { - continue; - }; - - // Check wrap flags for passcode protection (bit 0x02) - let Some(wrap_bytes) = &class_key_data.wrap else { - continue; - }; - - // Parse wrap flag as big-endian u32 - let wrap_val = u32::from_be_bytes( - wrap_bytes - .as_slice() - .try_into() - .map_err(|_| BackupError::KeyUnwrapFailed(*class_id))?, - ); - - if wrap_val & 0x02 == 0 { - continue; // Skip keys not protected by passcode - } + // Iterate over each class key in the key ring + for (&class_id, class_key_data) in &key_ring.class_keys { + match class_key_data { + // Unwrapped key data with wrapped key + ClassKeyData { + wpky: Some(wpky), + wrap: Some(wrap_bytes), + .. + } => { + // Ensure wrap_bytes is exactly 4 bytes (u32) + let wrap_val = u32::from_be_bytes( + wrap_bytes + .as_slice() + .try_into() + .map_err(|_| BackupError::KeyUnwrapFailed(class_id))?, + ); + // Check if the key is wrapped with AES Key Wrap + if wrap_val & 0x02 == 0 { + // Not wrapped with AES Key Wrap, skip + continue; + } - // Unwrap class key using AES key wrap (RFC 3394) - let unwrapped = aes_kw_unwrap(master_key, &WrappedKey::from(wpky.clone())) - .map_err(|_| BackupError::KeyUnwrapFailed(*class_id))?; - - unlocked_keys.insert( - *class_id, - ProtectionClassKey { - class_id: *class_id, - key: unwrapped, - }, - ); + // Create the Key Encryption Key (KEK) from the master key + let unwrapped = aes_kw_unwrap(master_key, &WrappedKey::from(wpky.clone())) + .map_err(|_| BackupError::KeyUnwrapFailed(class_id))?; + + // Insert the unwrapped key into the map + unlocked_keys.insert( + class_id, + ProtectionClassKey { + class_id, + key: unwrapped, + }, + ); + } + _ => continue, + } } Ok(unlocked_keys) } diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 40df327..afa6097 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -477,7 +477,7 @@ impl Backup { pub fn decrypt_entry_stream( &self, entry: &BackupFileEntry, - ) -> Result>> { + ) -> Result>> { if !self.is_encrypted() { return Err(BackupError::NotEncrypted); } diff --git a/src/backup/models/manifest/manifest_plist.rs b/src/backup/models/manifest/manifest_plist.rs index a46fe46..a49ecfc 100644 --- a/src/backup/models/manifest/manifest_plist.rs +++ b/src/backup/models/manifest/manifest_plist.rs @@ -68,35 +68,39 @@ impl Manifest { /// # Ok::<(), crabapple::error::BackupError>(()) /// ``` pub fn from_manifest_data(manifest_data: ManifestData, auth: &Authentication) -> Result { - let (main_decryption_key, unlocked_class_keys) = if manifest_data.is_encrypted { - let backup_key_ring = manifest_data.key_ring.as_ref().ok_or_else(|| { - BackupError::MissingPlistKey( - "BackupKeyBag (required for encrypted backup)".to_string(), - ) - })?; - - let master_key = match auth { - Authentication::Password(password) => derive_key_from_password( + // Determine decryption strategy based on whether the backup is encrypted and the provided authentication. + let (main_decryption_key, unlocked_class_keys) = match (manifest_data.is_encrypted, auth) { + (true, Authentication::Password(password)) => { + let backup_key_ring = manifest_data.key_ring.as_ref().ok_or_else(|| { + BackupError::MissingPlistKey( + "BackupKeyBag (required for encrypted backup)".to_string(), + ) + })?; + let master_key = derive_key_from_password( password.as_bytes(), &backup_key_ring.dpsl, backup_key_ring.dpic, &backup_key_ring.salt, backup_key_ring.iter, - )?, - Authentication::DerivedKey(key_hex) => hex_decode(key_hex)?.into(), - Authentication::None => return Err(BackupError::PasswordOrKeyIncorrect), - }; - - let unlocked_keys_map = unlock_keys_from_manifest(&master_key, &manifest_data) - .map_err(|_| BackupError::PasswordOrKeyIncorrect)?; - - (Some(master_key), Some(unlocked_keys_map)) - } else { - // Error if the backup is not encrypted but an authentication method is provided - if !matches!(auth, Authentication::None) { - return Err(BackupError::NotEncrypted); + )?; + let unlocked_keys_map = unlock_keys_from_manifest(&master_key, &manifest_data) + .map_err(|_| BackupError::PasswordOrKeyIncorrect)?; + (Some(master_key), Some(unlocked_keys_map)) + } + (true, Authentication::DerivedKey(key_hex)) => { + manifest_data.key_ring.as_ref().ok_or_else(|| { + BackupError::MissingPlistKey( + "BackupKeyBag (required for encrypted backup)".to_string(), + ) + })?; + let master_key = hex_decode(key_hex)?.into(); + let unlocked_keys_map = unlock_keys_from_manifest(&master_key, &manifest_data) + .map_err(|_| BackupError::PasswordOrKeyIncorrect)?; + (Some(master_key), Some(unlocked_keys_map)) } - (None, None) + (true, Authentication::None) => return Err(BackupError::PasswordOrKeyRequired), + (false, Authentication::None) => (None, None), + (false, _) => return Err(BackupError::NotEncrypted), }; Ok(Self { diff --git a/src/error.rs b/src/error.rs index d967377..220f88c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,7 +63,12 @@ pub enum BackupError { KeyUnwrapFailed(u32), /// Cryptographic data had an unexpected length. - InvalidCryptoDataLength { expected: usize, actual: usize }, + InvalidCryptoDataLength { + /// The expected length of the data in bytes. + expected: usize, + /// The actual length of the data in bytes. + actual: usize, + }, /// Invalid TLV data encountered. InvalidTlvData(String),