11require 'fileutils'
2+ require 'cfpropertylist'
23require 'pathname'
34require_relative 'AppleBackup.rb'
5+ require_relative 'AppleBackupHashedManifestPlist.rb'
46require_relative 'AppleNote.rb'
57require_relative 'AppleNoteStore.rb'
68
@@ -11,63 +13,127 @@ class AppleBackupHashed < AppleBackup
1113
1214 ##
1315 # Creates a new AppleBackupHashed. Expects a Pathname +root_folder+ that represents the root
14- # of the backup and a Pathname +output_folder+ which will hold the results of this run.
16+ # of the backup, a Pathname +output_folder+ which will hold the results of this run, and
17+ # an AppleDecrypter +decrypter+ to assist in decrypting files.
1518 # Immediately sets the NoteStore database file to be the appropriate hashed file.
16- def initialize ( root_folder , output_folder )
19+ def initialize ( root_folder , output_folder , decrypter = AppleDecrypter . new )
1720
18- super ( root_folder , AppleBackup ::HASHED_BACKUP_TYPE , output_folder )
21+ super ( root_folder , AppleBackup ::HASHED_BACKUP_TYPE , output_folder , decrypter )
1922
2023 @hashed_backup_manifest_database = nil
2124
2225 # Check to make sure we're all good
2326 if self . valid?
27+
2428 puts "Created a new AppleBackup from iTunes backup: #{ @root_folder } "
2529
26- # Copy the modern NoteStore to our output directory
30+ # Snag the manifest.plist file
31+ @manifest_plist = AppleBackupHashedManifestPlist . new ( ( @root_folder + "Manifest.plist" ) , @decrypter , @logger )
32+
33+ if ( @manifest_plist . encrypted? and !@manifest_plist . can_decrypt? )
34+ @logger . error ( "Manifest Plist file cannot be decrypted, likely due to a bad password." )
35+ puts "Manifest PList file cannot be decrypted, have you included any passwords?"
36+ exit
37+ end
38+
39+ # Define where the modern notes live
2740 hashed_note_store = @root_folder + "4f" + "4f98687d8ab0d6d1a371110e6b7300f6e465bef2"
2841 hashed_note_store_wal = @root_folder + "7d" + "7dc1d0fa6cd437c0ad9b9b573ea59c5e62373e92"
2942 hashed_note_store_shm = @root_folder + "55" + "55901f4cbd89916628b4ec30bf19717aca78fb2c"
3043
31- FileUtils . cp ( hashed_note_store , @note_store_modern_location )
32- FileUtils . cp ( hashed_note_store_wal , @output_folder + "NoteStore.sqlite-wal" ) if hashed_note_store_wal . exist?
33- FileUtils . cp ( hashed_note_store_shm , @output_folder + "NoteStore.sqlite-shm" ) if hashed_note_store_shm . exist?
34- modern_note_version = AppleNoteStore . guess_ios_version ( @note_store_modern_location )
44+ # Define where the Manifest file lives
45+ manifest_db = @root_folder + "Manifest.db"
46+ manifest_db_wal = @root_folder + "Manifest.db-wal"
47+ manifest_db_shm = @root_folder + "Manifest.db-shm"
3548
36- # Copy the legacy NoteStore to our output directory
49+ # Define where the legacy notes live
3750 hashed_legacy_note_store = @root_folder + "ca" + "ca3bc056d4da0bbf88b5fb3be254f3b7147e639c"
3851 hashed_legacy_note_store_wal = @root_folder + "12" + "12be33d156731173c5ec6ea09ab02f07a98179ed"
3952 hashed_legacy_note_store_shm = @root_folder + "ef" + "efaa1bfb59fcb943689733e2ca1595db52462fb9"
4053
54+ # Copy the NoteStore.sqlite file
55+ FileUtils . cp ( hashed_note_store , @note_store_modern_location )
56+ FileUtils . cp ( hashed_note_store_wal , @output_folder + "NoteStore.sqlite-wal" ) if hashed_note_store_wal . exist?
57+ FileUtils . cp ( hashed_note_store_shm , @output_folder + "NoteStore.sqlite-shm" ) if hashed_note_store_shm . exist?
58+
59+ # Copy the legacy NoteStore to our output directory
4160 FileUtils . cp ( hashed_legacy_note_store , @note_store_legacy_location )
4261 FileUtils . cp ( hashed_legacy_note_store_wal , @output_folder + "notes.sqlite-wal" ) if hashed_legacy_note_store_wal . exist?
4362 FileUtils . cp ( hashed_legacy_note_store_shm , @output_folder + "notes.sqlite-shm" ) if hashed_legacy_note_store_shm . exist?
63+
64+ # Copy the Manifest.db to our output directory in case we want to look up files
65+ FileUtils . cp ( manifest_db , @output_folder + "Manifest.db" )
66+ FileUtils . cp ( manifest_db_wal , @output_folder + "Manifest.db-wal" ) if manifest_db_wal . exist?
67+ FileUtils . cp ( manifest_db_shm , @output_folder + "manifest.db-shm" ) if manifest_db_shm . exist?
68+
69+ # Check if we have to decrypt the relevant file(s)
70+ if @manifest_plist . encrypted?
71+ @logger . debug ( "Detected encrypted iTunes backup. Attempting to decrypt." )
72+
73+ # Snag the encrypted database into memory
74+ encrypted_data = ''
75+ File . open ( @output_folder + "Manifest.db" , 'rb' ) do |file |
76+ encrypted_data = file . read
77+ end
78+
79+ # Fetch the right AppleProtectionClass from the manifest plist
80+ protection_class = @manifest_plist . get_class_by_id ( @manifest_plist . manifest_key_class )
81+
82+ # Bail out if we don't have the right key for some reason
83+ if !protection_class
84+ @logger . error ( "Unable to locate the appropriate protection class to decrypt the Manifest.db. Unfortunately, this is the end." )
85+ puts "Unable to decrypt the Manifest.db file, we cannot continue."
86+ exit
87+ end
88+
89+ # Unwrap the key protecting the Manifest.db file
90+ manifest_key = @decrypter . aes_key_unwrap ( @manifest_plist . manifest_key , protection_class . unwrapped_key )
91+
92+ # Decrypt the database into memory
93+ decrypted_manifest = @decrypter . aes_cbc_decrypt ( manifest_key , encrypted_data )
94+
95+ # Write the file out to where we expect the manifest to be
96+ File . open ( @output_folder + "Manifest.db" , 'wb' ) do |output |
97+ @logger . debug ( "Wrote out decrypted Manifest database to #{ @output_folder + "Manifest.db" } " )
98+ output . write ( decrypted_manifest )
99+ end
100+
101+ @hashed_backup_manifest_database = SQLite3 ::Database . new ( ( @output_folder + "Manifest.db" ) . to_s , { results_as_hash : true } )
102+
103+ # Overwrite the other critical files
104+ decrypt_in_place ( "NoteStore.sqlite" )
105+ decrypt_in_place ( "notes.sqlite" )
106+ else
107+ @hashed_backup_manifest_database = SQLite3 ::Database . new ( ( @output_folder + "Manifest.db" ) . to_s , { results_as_hash : true } )
108+ end
109+
110+ modern_note_version = AppleNoteStore . guess_ios_version ( @note_store_modern_location )
44111 legacy_note_version = AppleNoteStore . guess_ios_version ( @note_store_legacy_location )
45112
46- # Copy the Manifest.db to our output directry in case we want to look up files
47- manifest_db = @root_folder + "Manifest.db"
48- manifest_db_wal = @root_folder + "Manifest.db-wal"
49- manifest_db_shm = @root_folder + "Manifest.db-shm"
50- FileUtils . cp ( manifest_db , @output_folder )
51- FileUtils . cp ( manifest_db_wal , @output_folder ) if manifest_db_wal . exist?
52- FileUtils . cp ( manifest_db_shm , @output_folder ) if manifest_db_shm . exist?
53113
54114 # Create the AppleNoteStore objects
55115 create_and_add_notestore ( @note_store_modern_location , modern_note_version )
56116 create_and_add_notestore ( @note_store_legacy_location , legacy_note_version )
57- @hashed_backup_manifest_database = SQLite3 ::Database . new ( ( @output_folder + "Manifest.db" ) . to_s , { results_as_hash : true } )
58117
59118 # Rerun the check for an Accounts folder now that the database is open
60119 @uses_account_folder = check_for_accounts_folder
61120 end
62121 end
63122
123+ ##
124+ # This method is a helper to identify if this is an encrypted iTunes backup.
125+ def is_encrypted?
126+ return ( @manifest_plist and @manifest_plist . encrypted? )
127+ end
128+
64129 ##
65130 # This method returns true if it is a valid backup of the specified type. For a HASHED_BACKUP_TYPE,
66131 # that means it has a Manifest.db at the root level.
67132 def valid?
68133 return ( @root_folder . directory? and ( @root_folder + "Manifest.db" ) . file? )
69134 end
70135
136+
71137 ##
72138 # This method overrides the default check_for_accounts_folder to determine
73139 # if this backup uses an accounts folder or not. It takes no arguments and
@@ -100,4 +166,120 @@ def get_real_file_path(filename)
100166 return nil
101167 end
102168
169+ ##
170+ # This method returns a binary plist object that represents the data in the Files.file column.
171+ # It expects a String +filename+ to look up. For hashed backups, that involves checking Manifest.db
172+ # to get the appropriate hash value.
173+ def get_file_plist ( filename )
174+ @hashed_backup_manifest_database . execute ( "SELECT file FROM Files WHERE (relativePath=? AND domain='AppDomainGroup-group.com.apple.notes') OR (relativePath LIKE ? AND domain='HomeDomain')" , filename ) do |row |
175+ file_plist = row [ "file" ]
176+ tmp_plist = CFPropertyList ::List . new
177+ tmp_plist . load_binary_str ( file_plist )
178+ return CFPropertyList . native_types ( tmp_plist . value )
179+ end
180+
181+ # If we get this far, consider if it is a legacy notes file, unsure if this is the best way to go about it
182+ filename = "Library/Notes/#{ filename } "
183+ @hashed_backup_manifest_database . execute ( "SELECT file FROM Files WHERE relativePath=? AND domain='HomeDomain'" , filename ) do |row |
184+ file_plist = row [ "file" ]
185+ tmp_plist = CFPropertyList ::List . new
186+ tmp_plist . load_binary_str ( file_plist )
187+ return CFPropertyList . native_types ( tmp_plist . value )
188+ end
189+
190+ # If we get here, we just need to give up
191+ return nil
192+ end
193+
194+ ##
195+ # This method fetches the encryption key for a file from the file's plist in Manifest.db.
196+ # It expects a CFProperty +plist+ and returns the wrapped encryption key as a binary string.
197+ def get_file_encryption_key ( plist )
198+ # Find the root object ID
199+ tmp_root = plist [ "$top" ] [ "root" ]
200+
201+ # Use the root object ID to find the Encryption Key index
202+ tmp_key_position = plist [ "$objects" ] [ tmp_root ] [ "EncryptionKey" ]
203+
204+ # Get the data for the encryption key, this has the protection class at the start
205+ tmp_wrapped_key = plist [ "$objects" ] [ tmp_key_position ] [ "NS.data" ]
206+
207+ # Return the key itself
208+ return tmp_wrapped_key [ 4 , tmp_wrapped_key . length - 4 ]
209+ end
210+
211+ ##
212+ # This method pulls the relevant protection class from the Manifest.db file plist
213+ # for an encrypted file. It expects a CFPropertyList +plist+ and returns the protection
214+ # class as an Integer.
215+ def get_file_protection_class ( plist )
216+ # Find the root object ID
217+ tmp_root = plist [ "$top" ] [ "root" ]
218+
219+ # Use the root object ID to find the Encryption Key index
220+ tmp_key_position = plist [ "$objects" ] [ tmp_root ] [ "EncryptionKey" ]
221+
222+ # Get the data for the encryption key, this has the protection class at the start
223+ tmp_wrapped_key = plist [ "$objects" ] [ tmp_key_position ] [ "NS.data" ]
224+
225+ # Return the key itself
226+ return tmp_wrapped_key [ 0 , 4 ] . reverse . unpack ( "N" ) [ 0 ]
227+ end
228+
229+ ##
230+ # This method pulls the expected file size from the Manifest.db file plist
231+ # for an encrypted file. It expects a CFPropertyList +plist+ and returns the file size
232+ # as an Integer.
233+ def get_file_expected_size ( plist )
234+ # Find the root object ID
235+ tmp_root = plist [ "$top" ] [ "root" ]
236+
237+ # Use the root object ID to find the Encryption Key index
238+ return plist [ "$objects" ] [ tmp_root ] [ "Size" ]
239+ end
240+
241+ ##
242+ # This method is used to decrypt an iTunes encrypted backup file in place.
243+ # It expects the file to already have been copied to the output folder and to receive the
244+ # +filename+ as a String. It checks the filename in Manifest.db, looks up the corresponding
245+ # encryption key, and uses that to decrypt the file contents, overwriting was was in output.
246+ def decrypt_in_place ( filename , folder = '' )
247+ @logger . debug ( "Attempting to decrypt in place #{ filename } " )
248+
249+ target_destination = @output_folder + folder + filename
250+ return if !target_destination . exist?
251+
252+ # Snag the File Plist for this file from the Manifest.db file
253+ tmp_plist = get_file_plist ( filename )
254+ if !tmp_plist
255+ @logger . error ( "Unable to find the file plist for #{ filename } ." )
256+ return
257+ end
258+
259+ tmp_wrapped_key = get_file_encryption_key ( tmp_plist )
260+ tmp_class = get_file_protection_class ( tmp_plist )
261+ tmp_size = get_file_expected_size ( tmp_plist )
262+
263+ # Fetch the protection class from the manifest Plist to get its unwrapped key
264+ tmp_protection_class = @manifest_plist . get_class_by_id ( tmp_class )
265+ tmp_unwrapped_key = @decrypter . aes_key_unwrap ( tmp_wrapped_key , tmp_protection_class . unwrapped_key )
266+
267+ # Actually decrypt the file itself
268+ decrypted_file = @decrypter . aes_cbc_decrypt ( tmp_unwrapped_key , File . read ( @output_folder + folder + filename ) )
269+
270+ if !decrypted_file
271+ @logger . error ( "Failed to decrypt #{ target_destination } " )
272+ return
273+ end
274+
275+ # Overwrite the results
276+ File . open ( target_destination , 'wb' ) do |output |
277+ @logger . debug ( "Wrote out decrypted #{ filename } to #{ target_destination } " )
278+ # Only write the first tmp_size bytes, don't write the padding that decrypting introduced.
279+ # This ensures encrypted files can be decrypted, as they were encrypted prior to the padding
280+ # being introduced.
281+ output . write ( decrypted_file [ 0 , tmp_size ] )
282+ end
283+ end
284+
103285end
0 commit comments