app-info 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'macho'
4
+ require 'pngdefry'
5
+ require 'fileutils'
6
+ require 'forwardable'
7
+ require 'cfpropertylist'
8
+ require 'app_info/util'
9
+
10
+ module AppInfo
11
+ # IPA parser
12
+ class IPA
13
+ extend Forwardable
14
+
15
+ attr_reader :file
16
+
17
+ # iOS Export types
18
+ module ExportType
19
+ DEBUG = 'Debug'
20
+ ADHOC = 'AdHoc'
21
+ INHOUSE = 'inHouse'
22
+ RELEASE = 'Release'
23
+ UNKOWN = nil
24
+ end
25
+
26
+ def initialize(file)
27
+ @file = file
28
+ end
29
+
30
+ def size(humanable = false)
31
+ AppInfo::Util.file_size(@file, humanable)
32
+ end
33
+
34
+ def os
35
+ AppInfo::Platform::IOS
36
+ end
37
+ alias file_type os
38
+
39
+ def_delegators :info, :iphone?, :ipad?, :universal?, :build_version, :name,
40
+ :release_version, :identifier, :bundle_id, :display_name,
41
+ :bundle_name, :icons, :min_sdk_version, :device_type
42
+
43
+ def_delegators :mobileprovision, :devices, :team_name, :team_identifier,
44
+ :profile_name, :expired_date
45
+
46
+ def distribution_name
47
+ "#{profile_name} - #{team_name}" if profile_name && team_name
48
+ end
49
+
50
+ def release_type
51
+ if stored?
52
+ ExportType::RELEASE
53
+ else
54
+ build_type
55
+ end
56
+ end
57
+
58
+ def build_type
59
+ if mobileprovision?
60
+ if devices
61
+ ExportType::ADHOC
62
+ else
63
+ ExportType::INHOUSE
64
+ end
65
+ else
66
+ ExportType::DEBUG
67
+ end
68
+ end
69
+
70
+ def archs
71
+ return unless File.exist?(bundle_path)
72
+
73
+ file = MachO.open(bundle_path)
74
+ case file
75
+ when MachO::MachOFile
76
+ [file.cpusubtype]
77
+ else
78
+ file.machos.each_with_object([]) do |arch, obj|
79
+ obj << arch.cpusubtype
80
+ end
81
+ end
82
+ end
83
+ alias architectures archs
84
+
85
+ def stored?
86
+ metadata? ? true : false
87
+ end
88
+
89
+ def hide_developer_certificates
90
+ mobileprovision.delete('DeveloperCertificates') if mobileprovision?
91
+ end
92
+
93
+ def mobileprovision
94
+ return unless mobileprovision?
95
+ return @mobileprovision if @mobileprovision
96
+
97
+ @mobileprovision = MobileProvision.new(mobileprovision_path)
98
+ end
99
+
100
+ def mobileprovision?
101
+ File.exist?mobileprovision_path
102
+ end
103
+
104
+ def mobileprovision_path
105
+ filename = 'embedded.mobileprovision'
106
+ @mobileprovision_path ||= File.join(@file, filename)
107
+ unless File.exist?(@mobileprovision_path)
108
+ @mobileprovision_path = File.join(app_path, filename)
109
+ end
110
+
111
+ @mobileprovision_path
112
+ end
113
+
114
+ def metadata
115
+ return unless metadata?
116
+
117
+ @metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
118
+ end
119
+
120
+ def metadata?
121
+ File.exist?(metadata_path)
122
+ end
123
+
124
+ def metadata_path
125
+ @metadata_path ||= File.join(contents, 'iTunesMetadata.plist')
126
+ end
127
+
128
+ def bundle_path
129
+ @bundle_path ||= File.join(app_path, info.bundle_name)
130
+ end
131
+
132
+ def info
133
+ @info ||= InfoPlist.new(app_path)
134
+ end
135
+
136
+ def app_path
137
+ @app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
138
+ end
139
+
140
+ def cleanup!
141
+ return unless @contents
142
+
143
+ FileUtils.rm_rf(@contents)
144
+
145
+ @contents = nil
146
+ @icons = nil
147
+ @app_path = nil
148
+ @metadata = nil
149
+ @metadata_path = nil
150
+ @info = nil
151
+ end
152
+
153
+ private
154
+
155
+ def contents
156
+ @contents ||= Util.unarchive(@file, path: 'ios')
157
+ end
158
+
159
+ def icons_root_path
160
+ iphone = 'CFBundleIcons'
161
+ ipad = 'CFBundleIcons~ipad'
162
+
163
+ case device_type
164
+ when 'iPhone'
165
+ [iphone]
166
+ when 'iPad'
167
+ [ipad]
168
+ when 'Universal'
169
+ [iphone, ipad]
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cfpropertylist'
4
+ require 'app_info/util'
5
+
6
+ module AppInfo
7
+ # iOS Info.plist parser
8
+ class InfoPlist
9
+ def initialize(app_path)
10
+ @app_path = app_path
11
+ end
12
+
13
+ def build_version
14
+ info.try(:[], 'CFBundleVersion')
15
+ end
16
+
17
+ def release_version
18
+ info.try(:[], 'CFBundleShortVersionString')
19
+ end
20
+
21
+ def identifier
22
+ info.try(:[], 'CFBundleIdentifier')
23
+ end
24
+ alias bundle_id identifier
25
+
26
+ def name
27
+ display_name || bundle_name
28
+ end
29
+
30
+ def display_name
31
+ info.try(:[], 'CFBundleDisplayName')
32
+ end
33
+
34
+ def bundle_name
35
+ info.try(:[], 'CFBundleName')
36
+ end
37
+
38
+ #
39
+ # Extract the Minimum OS Version from the Info.plist
40
+ #
41
+ def min_sdk_version
42
+ info.try(:[], 'MinimumOSVersion')
43
+ end
44
+
45
+ def icons
46
+ return @icons if @icons
47
+
48
+ @icons = []
49
+ icons_root_path.each do |name|
50
+ icon_array = info.try(:[], name)
51
+ .try(:[], 'CFBundlePrimaryIcon')
52
+ .try(:[], 'CFBundleIconFiles')
53
+
54
+ next if icon_array.nil? || icon_array.empty?
55
+
56
+ icon_array.each do |items|
57
+ Dir.glob(File.join(@app_path, "#{items}*")).find_all.each do |file|
58
+ dict = {
59
+ name: File.basename(file),
60
+ file: file,
61
+ dimensions: Pngdefry.dimensions(file)
62
+ }
63
+
64
+ @icons.push(dict)
65
+ end
66
+ end
67
+ end
68
+
69
+ @icons
70
+ end
71
+
72
+ def device_type
73
+ device_family = info.try(:[], 'UIDeviceFamily')
74
+ if device_family.length == 1
75
+ case device_family
76
+ when [1]
77
+ 'iPhone'
78
+ when [2]
79
+ 'iPad'
80
+ end
81
+ elsif device_family.length == 2 && device_family == [1, 2]
82
+ 'Universal'
83
+ end
84
+ end
85
+
86
+ def iphone?
87
+ device_type == 'iPhone'
88
+ end
89
+
90
+ def ipad?
91
+ device_type == 'iPad'
92
+ end
93
+
94
+ def universal?
95
+ device_type == 'Universal'
96
+ end
97
+
98
+ def release_type
99
+ if stored?
100
+ 'Store'
101
+ else
102
+ build_type
103
+ end
104
+ end
105
+
106
+ def [](key)
107
+ info.try(:[], key.to_s)
108
+ end
109
+
110
+ def method_missing(method_name, *args, &block)
111
+ info.try(:[], Util.format_key(method_name)) ||
112
+ info.send(method_name) ||
113
+ super
114
+ end
115
+
116
+ def respond_to_missing?(method_name, *args)
117
+ info.key?(Util.format_key(method_name)) ||
118
+ info.respond_to?(method_name) ||
119
+ super
120
+ end
121
+
122
+ private
123
+
124
+ def info
125
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
126
+ end
127
+
128
+ def info_path
129
+ File.join(@app_path, 'Info.plist')
130
+ end
131
+
132
+ IPHONE_KEY = 'CFBundleIcons'
133
+ IPAD_KEY = 'CFBundleIcons~ipad'
134
+
135
+ def icons_root_path
136
+ case device_type
137
+ when 'iPhone'
138
+ [IPHONE_KEY]
139
+ when 'iPad'
140
+ [IPAD_KEY]
141
+ when 'Universal'
142
+ [IPHONE_KEY, IPAD_KEY]
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'cfpropertylist'
5
+ require 'app_info/util'
6
+
7
+ module AppInfo
8
+ # .mobileprovision file parser
9
+ class MobileProvision
10
+ def initialize(path)
11
+ @path = path
12
+ end
13
+
14
+ def name
15
+ mobileprovision.try(:[], 'Name')
16
+ end
17
+
18
+ def app_name
19
+ mobileprovision.try(:[], 'AppIDName')
20
+ end
21
+
22
+ def type
23
+ return :development if development?
24
+ return :adhoc if adhoc?
25
+ return :appstore if appstore?
26
+ return :inhouse if inhouse?
27
+ end
28
+
29
+ def platforms
30
+ return unless platforms = mobileprovision.try(:[], 'Platform')
31
+
32
+ platforms.map { |v|
33
+ v = 'macOS' if v == 'OSX'
34
+ v.downcase.to_sym
35
+ }
36
+ end
37
+
38
+ def platform
39
+ platforms[0]
40
+ end
41
+
42
+ def devices
43
+ mobileprovision.try(:[], 'ProvisionedDevices')
44
+ end
45
+
46
+ def team_identifier
47
+ mobileprovision.try(:[], 'TeamIdentifier')
48
+ end
49
+
50
+ def team_name
51
+ mobileprovision.try(:[], 'TeamName')
52
+ end
53
+
54
+ def profile_name
55
+ mobileprovision.try(:[], 'Name')
56
+ end
57
+
58
+ def created_date
59
+ mobileprovision.try(:[], 'CreationDate')
60
+ end
61
+
62
+ def expired_date
63
+ mobileprovision.try(:[], 'ExpirationDate')
64
+ end
65
+
66
+ def entitlements
67
+ mobileprovision.try(:[], 'Entitlements')
68
+ end
69
+
70
+ def developer_certs
71
+ certs = mobileprovision.try(:[], 'DeveloperCertificates')
72
+ return if certs.empty?
73
+
74
+ certs.each_with_object([]) do |cert, obj|
75
+ obj << DeveloperCertificate.new(cert)
76
+ end
77
+ end
78
+
79
+ # Detect is development type of mobileprovision
80
+ #
81
+ # related link: https://stackoverflow.com/questions/1003066/what-does-get-task-allow-do-in-xcode
82
+ def development?
83
+ case platform.downcase.to_sym
84
+ when :ios
85
+ entitlements['get-task-allow'] == true
86
+ when :macos
87
+ !devices.nil?
88
+ else
89
+ raise Error, "Not implement with platform: #{platform}"
90
+ end
91
+ end
92
+
93
+ # Detect app store type
94
+ #
95
+ # related link: https://developer.apple.com/library/archive/qa/qa1830/_index.html
96
+ def appstore?
97
+ case platform.downcase.to_sym
98
+ when :ios
99
+ !development? && entitlements.key?('beta-reports-active')
100
+ when :macos
101
+ !development?
102
+ else
103
+ raise Error, "Not implement with platform: #{platform}"
104
+ end
105
+ end
106
+
107
+ def adhoc?
108
+ return false if platform == :macos # macOS no need adhoc
109
+
110
+ !development? && !devices.nil?
111
+ end
112
+
113
+ def inhouse?
114
+ return false if platform == :macos # macOS no need adhoc
115
+
116
+ !development? && !adhoc? && !appstore?
117
+ end
118
+
119
+ # Enabled Capabilites
120
+ #
121
+ # Related link: https://developer.apple.com/support/app-capabilities/
122
+ def enabled_capabilities
123
+ capabilities = []
124
+ if adhoc? || appstore?
125
+ capabilities << 'In-App Purchase' << 'GameKit'
126
+ end
127
+
128
+ entitlements.each do |key, value|
129
+ case key
130
+ when 'aps-environment'
131
+ capabilities << 'Push Notifications'
132
+ when 'com.apple.developer.applesignin'
133
+ capabilities << 'Sign In with Apple'
134
+ when 'com.apple.developer.siri'
135
+ capabilities << 'SiriKit'
136
+ when 'com.apple.security.application-groups'
137
+ capabilities << 'App Groups'
138
+ when 'com.apple.developer.associated-domains'
139
+ capabilities << 'Associated Domains'
140
+ when 'com.apple.developer.default-data-protection'
141
+ capabilities << 'Data Protection'
142
+ when 'com.apple.developer.networking.networkextension'
143
+ capabilities << 'Network Extensions'
144
+ when 'com.apple.developer.networking.vpn.api'
145
+ capabilities << 'Personal VPN'
146
+ when 'com.apple.developer.healthkit', 'com.apple.developer.healthkit.access'
147
+ capabilities << 'HealthKit' unless capabilities.include?('HealthKit')
148
+ when 'com.apple.developer.icloud-services', 'com.apple.developer.icloud-container-identifiers'
149
+ capabilities << 'iCloud' unless capabilities.include?('iCloud')
150
+ when 'com.apple.developer.in-app-payments'
151
+ capabilities << 'Apple Pay'
152
+ when 'com.apple.developer.homekit'
153
+ capabilities << 'HomeKit'
154
+ when 'com.apple.developer.user-fonts'
155
+ capabilities << 'Fonts'
156
+ when 'com.apple.developer.pass-type-identifiers'
157
+ capabilities << 'Wallet'
158
+ when 'inter-app-audio'
159
+ capabilities << 'Inter-App Audio'
160
+ when 'com.apple.developer.networking.multipath'
161
+ capabilities << 'Multipath'
162
+ when 'com.apple.developer.authentication-services.autofill-credential-provider'
163
+ capabilities << 'AutoFill Credential Provider'
164
+ when 'com.apple.developer.networking.wifi-info'
165
+ capabilities << 'Access WiFi Information'
166
+ when 'com.apple.external-accessory.wireless-configuration'
167
+ capabilities << 'Wireless Accessory Configuration'
168
+ when 'com.apple.developer.kernel.extended-virtual-addressing'
169
+ capabilities << 'Extended Virtual Address Space'
170
+ when 'com.apple.developer.nfc.readersession.formats'
171
+ capabilities << 'NFC Tag Reading'
172
+ when 'com.apple.developer.ClassKit-environment'
173
+ capabilities << 'ClassKit'
174
+ when 'com.apple.developer.networking.HotspotConfiguration'
175
+ capabilities << 'Hotspot'
176
+ when 'com.apple.developer.devicecheck.appattest-environment'
177
+ capabilities << 'App Attest'
178
+ when 'com.apple.developer.coremedia.hls.low-latency'
179
+ capabilities << 'Low Latency HLS'
180
+ when 'com.apple.developer.associated-domains.mdm-managed'
181
+ capabilities << 'MDM Managed Associated Domains'
182
+ # macOS Only
183
+ when 'com.apple.developer.maps'
184
+ capabilities << 'Maps'
185
+ when 'com.apple.developer.system-extension.install'
186
+ capabilities << 'System Extension'
187
+ when 'com.apple.developer.networking.custom-protocol'
188
+ capabilities << 'Custom Network Protocol'
189
+ end
190
+ end
191
+
192
+ capabilities
193
+ end
194
+
195
+ def [](key)
196
+ mobileprovision.try(:[], key.to_s)
197
+ end
198
+
199
+ def empty?
200
+ mobileprovision.nil?
201
+ end
202
+
203
+ def mobileprovision
204
+ return @mobileprovision = nil unless File.exist?(@path)
205
+
206
+ data = File.read(@path)
207
+ data = strip_plist_wrapper(data) unless bplist?(data)
208
+ list = CFPropertyList::List.new(data: data).value
209
+ @mobileprovision = CFPropertyList.native_types(list)
210
+ rescue CFFormatError
211
+ @mobileprovision = nil
212
+ end
213
+
214
+ def method_missing(method_name, *args, &block)
215
+ mobileprovision.try(:[], Util.format_key(method_name)) ||
216
+ mobileprovision.send(method_name) ||
217
+ super
218
+ end
219
+
220
+ def respond_to_missing?(method_name, *args)
221
+ mobileprovision.key?(Util.format_key(method_name)) ||
222
+ mobileprovision.respond_to?(method_name) ||
223
+ super
224
+ end
225
+
226
+ private
227
+
228
+ def bplist?(raw)
229
+ raw[0..5] == 'bplist'
230
+ end
231
+
232
+ def strip_plist_wrapper(raw)
233
+ end_tag = '</plist>'
234
+ start_point = raw.index('<?xml version=')
235
+ end_point = raw.index(end_tag) + end_tag.size - 1
236
+ raw[start_point..end_point]
237
+ end
238
+
239
+ # Developer Certificate
240
+ class DeveloperCertificate
241
+ attr_reader :raw
242
+
243
+ def initialize(data)
244
+ @raw = OpenSSL::X509::Certificate.new(data)
245
+ end
246
+
247
+ def name
248
+ @raw.subject.to_a.find { |name, _, _| name == 'CN' }[1]
249
+ end
250
+
251
+ def created_date
252
+ @raw.not_after
253
+ end
254
+
255
+ def expired_date
256
+ @raw.not_before
257
+ end
258
+ end
259
+ end
260
+ end