ios_backup_extractor 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ module IosBackupExtractor
2
+ class RawBackup
3
+ include NauktisUtils::Logging
4
+ attr_reader :info_plist
5
+ INFO_PLIST = 'Info.plist'
6
+ MANIFEST_PLIST = 'Manifest.plist'
7
+
8
+ def initialize(backup_directory)
9
+ @backup_directory = NauktisUtils::FileBrowser.ensure_valid_directory(backup_directory)
10
+ @info_plist = InfoPlist.new(File.join(@backup_directory, INFO_PLIST))
11
+ @manifest_plist = IosBackupExtractor.plist_file_to_hash(File.join(@backup_directory, MANIFEST_PLIST))
12
+ raise 'This looks like a very old backup (iOS 3?)' unless @manifest_plist.has_key? 'BackupKeyBag'
13
+ end
14
+
15
+ ##
16
+ # Creates a tar archive of the backup without touching any files.
17
+
18
+ def archive_raw(destination, options = {})
19
+ load!(options)
20
+ destination_directory = NauktisUtils::FileBrowser.ensure_valid_directory(destination)
21
+ backup_name = NauktisUtils::FileBrowser.sanitize_name("#{@info_plist.last_backup_date.strftime('%Y_%m_%d')}_#{@info_plist.product_type}_iOS#{@info_plist.product_version}_#{@info_plist.serial_number}_raw")
22
+ parent_folder = @backup_directory
23
+ NauktisUtils::Archiver.new do
24
+ add(parent_folder)
25
+ destination(destination_directory)
26
+ name(File.basename(backup_name))
27
+ end
28
+ end
29
+
30
+ ##
31
+ # Extracts the backup to +destination_directory+.
32
+
33
+ def extract_to(destination_directory, options = {})
34
+ load!(options)
35
+ options = {name: 'full'}.merge(options)
36
+ destination_directory = NauktisUtils::FileBrowser.ensure_valid_directory(destination_directory)
37
+ backup_name = NauktisUtils::FileBrowser.sanitize_name("#{@info_plist.last_backup_date.strftime('%Y_%m_%d')}_#{@info_plist.product_type}_iOS#{@info_plist.product_version}_#{@info_plist.serial_number}_#{options[:name]}")
38
+ parent_directory = File.expand_path(File.join(destination_directory, backup_name))
39
+ raise "Backup destination already exists. #{parent_directory}" if File.exist? parent_directory
40
+ FileUtils.mkdir(parent_directory)
41
+ logger.info(self.class.name) { "Starting backup extraction in directory #{parent_directory}" }
42
+ copy_files(destination_directory, options)
43
+ add_files_with_extensions(destination_directory)
44
+ logger.info(self.class.name) { "Backup extraction finished. #{IosBackupExtractor.file_count(parent_directory)} files extracted." }
45
+ parent_directory
46
+ end
47
+
48
+ ##
49
+ # Creates a tar archive of the backup with files extracted.
50
+
51
+ def archive_to(destination_directory, options = {})
52
+ load!(options)
53
+ Dir.mktmpdir(nil, options[:temp_folder]) do |dir|
54
+ parent_folder = extract_to(dir, options)
55
+ logger.debug(self.class.name) { "Starting archiving of #{parent_folder}" }
56
+ NauktisUtils::Archiver.new do
57
+ add(parent_folder)
58
+ destination(destination_directory)
59
+ name(File.basename(parent_folder))
60
+ compress(:bzip2) if options[:compress]
61
+ end
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Tells whether the backup is encrypted or not.
67
+
68
+ def is_encrypted?
69
+ @manifest_plist['IsEncrypted']
70
+ end
71
+
72
+ # Prints one line information about the backup
73
+ def to_s
74
+ @info_plist.to_s
75
+ end
76
+
77
+ private
78
+
79
+ ##
80
+ # Loads the backup.
81
+ # This operation is required before performing any action
82
+
83
+ def load!(options = {})
84
+ unless @loaded
85
+ logger.debug(self.class.name) { 'Loading backup' }
86
+ logger.info(self.class.name) { "Files in the original backup directory #{IosBackupExtractor.file_count(@backup_directory)}" }
87
+
88
+ if is_encrypted?
89
+ logger.info(self.class.name) { 'Encrypted backup' }
90
+ major, minor = info_plist.versions
91
+ @keybag = Keybag.create_with_backup_manifest(@manifest_plist, options.fetch(:password), major, minor)
92
+ end
93
+
94
+ @loaded = true
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Returns true if the backup should include the file with +domain+ and +file_path+.
100
+ # This is useful for partial backups.
101
+
102
+ def should_include?(domain, file_path, options)
103
+ # Check filters
104
+ return false unless options[:domain_filter].nil? or domain =~ options[:domain_filter]
105
+ return false unless options[:file_path_filter].nil? or file_path =~ options[:file_path_filter]
106
+ return false unless options[:domain_except_filter].nil? or not (domain =~ options[:domain_except_filter])
107
+ return false unless options[:file_path_except_filter].nil? or not (file_path =~ options[:file_path_except_filter])
108
+ true
109
+ end
110
+
111
+ def copy_file_from_backup(backup_file, destination)
112
+ file_in, file_out = prepare_file_copy_from_backup(backup_file, destination)
113
+ FileUtils.cp(file_in, file_out)
114
+ end
115
+
116
+ def copy_enc_file_from_backup(backup_file, destination, key)
117
+ file_in, file_out = prepare_file_copy_from_backup(backup_file, destination)
118
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
119
+ cipher.decrypt
120
+ cipher.key = key
121
+ buf = ''
122
+ File.open(file_out, 'wb') do |outf|
123
+ File.open(file_in, 'rb') do |inf|
124
+ while inf.read(4096, buf)
125
+ outf << cipher.update(buf)
126
+ end
127
+ outf << cipher.final
128
+ end
129
+ end
130
+ end
131
+
132
+ def prepare_file_copy_from_backup(backup_file, destination)
133
+ # Prepare the source file
134
+ backup_file = File.join(@backup_directory, backup_file) unless backup_file.start_with?(@backup_directory)
135
+ backup_file = File.expand_path(backup_file)
136
+ raise "File #{backup_file} doesn't exist in your backup source" unless File.exists?(backup_file)
137
+
138
+ # Prepare destination
139
+ destination = File.expand_path(destination)
140
+
141
+ # TODO do that only if a flag is set.
142
+ if File.exists?(destination)
143
+ # Handle case sensitivity issues.
144
+ current = File.basename(destination)
145
+ existing = Dir.entries(File.dirname(destination))
146
+ if not existing.include?(current) and existing.any? { |e| e.downcase == current.downcase }
147
+ newdestination = ''
148
+ i = 0
149
+ loop do
150
+ i += 1
151
+ newdestination = File.join(File.dirname(destination), "#{current}_#{'cs' * i}")
152
+ break unless File.exists?(newdestination)
153
+ end
154
+ logger.warn(self.class.name) { "Case sensitivity issue with #{destination}. Using #{newdestination}" }
155
+ destination = newdestination
156
+ else
157
+ raise "File #{backup_file} already exists at #{destination}"
158
+ end
159
+ end
160
+
161
+ logger.debug(self.class.name) { "Copying #{backup_file} to #{destination}" }
162
+ FileUtils.mkdir_p(File.dirname(destination))
163
+
164
+ [backup_file, destination]
165
+ end
166
+
167
+ ##
168
+ # Adds all files with extension to the backup
169
+
170
+ def add_files_with_extensions(destination_directory)
171
+ Dir.entries(@backup_directory).each do |entry|
172
+ path = File.expand_path(File.join(@backup_directory, entry))
173
+ unless FileTest.directory? path
174
+ unless File.extname(path).empty?
175
+ logger.info(self.class.name) { "Keeping #{File.basename(path)} in the backup." }
176
+ FileUtils.cp(NauktisUtils::FileBrowser.ensure_valid_file(path), destination_directory)
177
+ end
178
+ end
179
+ end
180
+ raise "#{INFO_PLIST} was not added" unless File.exists?(File.join(destination_directory, INFO_PLIST))
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,85 @@
1
+ module IosBackupExtractor
2
+ class RawBackup10 < RawBackup
3
+ MANIFEST_DB = 'Manifest.db'
4
+
5
+ private
6
+
7
+ def load!(options = {})
8
+ super(options)
9
+
10
+ # Grab a copy of the Manifest database
11
+ manifest_dir = Dir.mktmpdir
12
+ major, minor = info_plist.versions
13
+ if is_encrypted? and major >= 10 and minor >= 2
14
+ protection_class = @manifest_plist['ManifestKey'][0..3].unpack('V')[0]
15
+ key = @keybag.unwrap_key_for_class(protection_class, @manifest_plist['ManifestKey'][4..-1])
16
+ copy_enc_file_from_backup(MANIFEST_DB, File.join(manifest_dir, MANIFEST_DB), key)
17
+ else
18
+ copy_file_from_backup(MANIFEST_DB, File.join(manifest_dir, MANIFEST_DB))
19
+ end
20
+
21
+ @manifest = SQLite3::Database.new(File.join(manifest_dir, MANIFEST_DB))
22
+ end
23
+
24
+ # Copy a file from the backup to destination
25
+ def copy_files(destination_directory, options = {})
26
+ destination_directory = NauktisUtils::FileBrowser.ensure_valid_directory(destination_directory)
27
+
28
+ logger.info(self.class.name) do
29
+ count = @manifest.execute('SELECT COUNT(fileID) FROM Files WHERE flags == 1')
30
+ "Files in the Manifest: #{count[0][0]}"
31
+ end
32
+
33
+ @manifest.execute('SELECT * FROM Files') do |row|
34
+ f = {
35
+ file_id: row[0],
36
+ domain: row[1],
37
+ file_path: row[2]
38
+ }
39
+ flag = row[3]
40
+
41
+ # Check filters
42
+ next unless should_include?(f[:domain], f[:file_path], options)
43
+
44
+ backup_file = File.expand_path(File.join(@backup_directory, f[:file_id][0..1], f[:file_id]))
45
+
46
+ # Folders
47
+ if flag == 2
48
+ if File.exists?(backup_file)
49
+ raise "Directories should not exist in the original backup... #{f[:file_id]}"
50
+ else
51
+ next
52
+ end
53
+ end
54
+
55
+ # Symlink
56
+ if flag == 4
57
+ if File.exists?(backup_file)
58
+ raise "Symlinks should not exist in the original backup... #{f[:file_id]}"
59
+ else
60
+ next
61
+ end
62
+ end
63
+
64
+ if flag == 16
65
+ if File.exists?(backup_file)
66
+ raise "Flag 16 should not exist in the original backup... #{f[:file_id]}"
67
+ else
68
+ next
69
+ end
70
+ end
71
+
72
+ destination = File.expand_path(File.join(destination_directory, f[:domain], f[:file_path]))
73
+ data = CFPropertyList.native_types(CFPropertyList::List.new(data: row[4]).value)
74
+
75
+ file_properties = data['$objects'][1]
76
+ if not file_properties['EncryptionKey'].nil? and not @keybag.nil?
77
+ key = @keybag.unwrap_key_for_class(file_properties['ProtectionClass'], data['$objects'][file_properties['EncryptionKey']]['NS.data'][4..-1])
78
+ copy_enc_file_from_backup(backup_file, destination, key)
79
+ else
80
+ copy_file_from_backup(backup_file, destination)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,52 @@
1
+ module IosBackupExtractor
2
+ class RawBackup4 < RawBackup
3
+ MANIFEST_MBDB = 'Manifest.mbdb'
4
+
5
+ private
6
+
7
+ def load!(options = {})
8
+ super(options)
9
+ @mbdb = MBDB.new(File.join(@backup_directory, MANIFEST_MBDB))
10
+ end
11
+
12
+ # Copy a file from the backup to destination
13
+ def copy_files(destination_directory, options = {})
14
+ destination_directory = NauktisUtils::FileBrowser.ensure_valid_directory(destination_directory)
15
+
16
+ @mbdb.files.each do |f|
17
+ if f[:type] == '-'
18
+ # Check filters
19
+ continue unless should_include?(f[:domain], f[:file_path], options)
20
+
21
+ destination = File.expand_path(File.join(destination_directory, f[:domain], f[:file_path]))
22
+ raise "File #{destination} already exists" if File.exists?(destination)
23
+
24
+ source = File.expand_path(File.join(@backup_directory, f[:file_id]))
25
+ raise "File #{source} doesn't exist in your backup source" unless File.exists?(source)
26
+
27
+ logger.debug(self.class.name) { "Extracting #{destination}" }
28
+ FileUtils.mkdir_p(File.dirname(destination))
29
+
30
+ if not f[:encryption_key].nil? and not @keybag.nil?
31
+ key = @keybag.unwrap_key_for_class(f[:protection_class], f[:encryption_key][4..-1])
32
+
33
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
34
+ cipher.decrypt
35
+ cipher.key = key
36
+ buf = ''
37
+ File.open(destination, 'wb') do |outf|
38
+ File.open(source, 'rb') do |inf|
39
+ while inf.read(4096, buf)
40
+ outf << cipher.update(buf)
41
+ end
42
+ outf << cipher.final
43
+ end
44
+ end
45
+ else
46
+ FileUtils.cp(source, destination)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module IosBackupExtractor
2
+ VERSION = '1.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,176 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ios_backup_extractor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nauktis
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-01-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nauktis_utils
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aes_key_wrap
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: CFPropertyList
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: Ruby script to extract iOS backups.
126
+ email:
127
+ - nauktis@users.noreply.github.com
128
+ executables:
129
+ - ios
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - ".travis.yml"
136
+ - Gemfile
137
+ - LICENSE.md
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - exe/ios
143
+ - ios_backup_extractor.gemspec
144
+ - lib/ios_backup_extractor.rb
145
+ - lib/ios_backup_extractor/backup_retriever.rb
146
+ - lib/ios_backup_extractor/info_plist.rb
147
+ - lib/ios_backup_extractor/keybag.rb
148
+ - lib/ios_backup_extractor/mbdb.rb
149
+ - lib/ios_backup_extractor/raw_backup.rb
150
+ - lib/ios_backup_extractor/raw_backup10.rb
151
+ - lib/ios_backup_extractor/raw_backup4.rb
152
+ - lib/ios_backup_extractor/version.rb
153
+ homepage: https://github.com/Nauktis/ios_backup_extractor
154
+ licenses: []
155
+ metadata: {}
156
+ post_install_message:
157
+ rdoc_options: []
158
+ require_paths:
159
+ - lib
160
+ required_ruby_version: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ required_rubygems_version: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ requirements: []
171
+ rubyforge_project:
172
+ rubygems_version: 2.5.1
173
+ signing_key:
174
+ specification_version: 4
175
+ summary: Ruby script to extract iOS backups.
176
+ test_files: []