app-info 3.3.2 → 3.4.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.
@@ -30,25 +30,39 @@ module AppInfo
30
30
  # V3.1 Signature ID 0x1b93ad61
31
31
  V3_1_BLOCK_ID = [0x61, 0xad, 0x93, 0x1b].freeze
32
32
 
33
- attr_reader :certificates, :digests
33
+ attr_reader :certificates, :digests, :certificate_lineage
34
34
 
35
35
  def version
36
36
  Version::V3
37
37
  end
38
38
 
39
- def verify
39
+ def verify(sdk: nil, **_options)
40
40
  begin
41
41
  signers_block = singers_block(V3_1_BLOCK_ID)
42
42
  rescue NotFoundError
43
43
  signers_block = singers_block(V3_BLOCK_ID)
44
44
  end
45
45
 
46
- @certificates, @digests = verified_certs(signers_block)
46
+ @certificates, @digests = verified_certs(signers_block, sdk: sdk)
47
+ @verified = true
48
+ rescue SecurityError => e
49
+ @certificates ||= []
50
+ @digests ||= {}
51
+ code = if e.message.include?('content digest')
52
+ :content_digest_mismatch
53
+ elsif e.message.start_with?('SDK range')
54
+ :sdk_range_mismatch
55
+ elsif e.message.start_with?('Certificate lineage')
56
+ :certificate_lineage_invalid
57
+ else
58
+ :signature_invalid
59
+ end
60
+ add_verification_error(code, e.message)
47
61
  end
48
62
 
49
63
  private
50
64
 
51
- def verified_certs(signers_block)
65
+ def verified_certs(signers_block, sdk: nil)
52
66
  unless (signers = length_prefix_block(signers_block))
53
67
  raise SecurityError, 'Not found signers'
54
68
  end
@@ -56,7 +70,7 @@ module AppInfo
56
70
  certificates = []
57
71
  content_digests = {}
58
72
  loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
59
- signer_certs, signer_digests = extract_signer_data(signer)
73
+ signer_certs, signer_digests = extract_signer_data(signer, sdk: sdk)
60
74
  certificates.concat(signer_certs)
61
75
  content_digests.merge!(signer_digests)
62
76
  end
@@ -65,13 +79,15 @@ module AppInfo
65
79
  [certificates, content_digests]
66
80
  end
67
81
 
68
- def extract_signer_data(signer)
82
+ def extract_signer_data(signer, sdk: nil)
69
83
  # raw data
70
84
  signed_data = length_prefix_block(signer)
71
85
 
72
- # TODO: verify min_sdk and max_sdk
73
- min_sdk = signer.read(UINT32_SIZE)
74
- max_sdk = signer.read(UINT32_SIZE)
86
+ min_sdk = signer.read(UINT32_SIZE).unpack1('V')
87
+ max_sdk = signer.read(UINT32_SIZE).unpack1('V')
88
+ if min_sdk > max_sdk || (sdk && !sdk.between?(min_sdk, max_sdk))
89
+ raise SecurityError, "SDK range #{min_sdk}..#{max_sdk} does not include #{sdk}"
90
+ end
75
91
 
76
92
  signatures = length_prefix_block(signer)
77
93
  public_key = length_prefix_block(signer, raw: true)
@@ -102,12 +118,8 @@ module AppInfo
102
118
  'Signature algorithms don\'t match between digests and signatures records'
103
119
  end
104
120
 
105
- previous_digest = content_digests.fetch(algorithems_digest)
121
+ verify_content_digest(content_digests, algorithems_digest)
106
122
  content_digests[algorithems_digest] = content_digest
107
- if previous_digest && previous_digest[:content] != content_digest
108
- raise SecurityError,
109
- 'Signature algorithms don\'t match between digests and signatures records'
110
- end
111
123
 
112
124
  certificates = length_prefix_block(signed_data)
113
125
  certs = signed_data_certs(certificates)
@@ -119,10 +131,139 @@ module AppInfo
119
131
  end
120
132
 
121
133
  additional_attrs = length_prefix_block(signed_data)
122
- verify_additional_attrs(additional_attrs, certs)
134
+ @certificate_lineage = verify_v3_additional_attrs(additional_attrs, certs, pkey)
123
135
 
124
136
  [certs, content_digests]
125
137
  end
138
+
139
+ def verify_v3_additional_attrs(attrs, certs, public_key)
140
+ lineage = nil
141
+ loop_length_prefix_io(
142
+ attrs, name: 'Additional Attributes', raw: true, ignore_left_size_precheck: true
143
+ ) do |raw_attr|
144
+ if raw_attr.bytesize < UINT32_SIZE
145
+ raise SecurityError,
146
+ 'Certificate lineage attribute is malformed'
147
+ end
148
+
149
+ attr = StringIO.new(raw_attr)
150
+ id = attr.read(UINT32_SIZE).unpack1('V')
151
+ if id == SIG_STRIPPING_PROTECTION_ATTR_ID.pack('C*').unpack1('V')
152
+ raise SecurityError,
153
+ 'V2 signature indicates APK is signed using APK Signature Scheme v3, ' \
154
+ 'but none was found. Signature stripped?'
155
+ elsif id == SIG_PROOF_OF_ROTATION_ATTR_ID.pack('C*').unpack1('V')
156
+ raise SecurityError, 'Certificate lineage attribute is duplicated' if lineage
157
+
158
+ lineage = parse_certificate_lineage(attr.read, certs, public_key)
159
+ end
160
+ end
161
+ lineage
162
+ end
163
+
164
+ def parse_certificate_lineage(bytes, certs, public_key)
165
+ input = StringIO.new(bytes)
166
+ version = input.read(UINT32_SIZE)&.unpack1('V')
167
+ raise SecurityError, 'Certificate lineage has an invalid version' unless version == 1
168
+
169
+ nodes = []
170
+ previous_certificate = nil
171
+ previous_algorithm = nil
172
+ seen = {}
173
+ until input.eof?
174
+ node = parse_lineage_node(input)
175
+ verify_lineage_node(node, previous_certificate, previous_algorithm, seen)
176
+ nodes << node
177
+ previous_certificate = node[:certificate]
178
+ previous_algorithm = node[:signature_algorithm_id]
179
+ end
180
+
181
+ if nodes.empty? || nodes.last[:certificate].public_key.to_der != public_key.to_der
182
+ raise SecurityError, 'Certificate lineage does not end at the APK signer'
183
+ end
184
+ unless nodes.last[:certificate].to_der == certs.first.to_der
185
+ raise SecurityError, 'Certificate lineage does not match the APK certificate'
186
+ end
187
+
188
+ nodes
189
+ rescue AppInfo::Android::Signature::SecurityError
190
+ raise
191
+ rescue StandardError => e
192
+ raise SecurityError, "Certificate lineage is malformed: #{e.message}"
193
+ end
194
+
195
+ def parse_lineage_node(input)
196
+ node_io = StringIO.new(length_prefix_block(input, raw: true))
197
+ signed_data = length_prefix_block(node_io, raw: true)
198
+ flags = node_io.read(UINT32_SIZE)&.unpack1('V')
199
+ signature_algorithm_id = node_io.read(UINT32_SIZE)&.unpack1('V')
200
+ signature = length_prefix_block(node_io, raw: true)
201
+ unless flags && signature_algorithm_id && node_io.eof?
202
+ raise SecurityError,
203
+ 'Certificate lineage node is malformed'
204
+ end
205
+
206
+ signed_io = StringIO.new(signed_data)
207
+ certificate_der = length_prefix_block(signed_io, raw: true)
208
+ parent_algorithm_id = signed_io.read(UINT32_SIZE)&.unpack1('V')
209
+ unless parent_algorithm_id && signed_io.eof?
210
+ raise SecurityError,
211
+ 'Certificate lineage signed data is malformed'
212
+ end
213
+
214
+ {
215
+ certificate: AppInfo::Certificate.parse(certificate_der),
216
+ certificate_der: certificate_der,
217
+ flags: flags,
218
+ signature_algorithm_id: signature_algorithm_id,
219
+ parent_signature_algorithm_id: parent_algorithm_id,
220
+ signature: signature,
221
+ signed_data: signed_data
222
+ }
223
+ end
224
+
225
+ def verify_lineage_node(node, previous_certificate, previous_algorithm, seen)
226
+ if seen[node[:certificate_der]]
227
+ raise SecurityError, 'Certificate lineage contains duplicate certificates'
228
+ end
229
+
230
+ parent_algorithm = node[:parent_signature_algorithm_id]
231
+ if previous_certificate
232
+ unless parent_algorithm == previous_algorithm
233
+ raise SecurityError,
234
+ 'Certificate lineage algorithm chain is invalid'
235
+ end
236
+
237
+ verify_lineage_signature(previous_certificate, parent_algorithm, node[:signature],
238
+ node[:signed_data])
239
+ elsif parent_algorithm != 0 || !node[:signature].empty?
240
+ raise SecurityError, 'Certificate lineage first node is invalid'
241
+ end
242
+ seen[node[:certificate_der]] = true
243
+ end
244
+
245
+ def verify_lineage_signature(certificate, algorithm_id, signature, signed_data)
246
+ algorithm = [algorithm_id].pack('V').unpack('C*')
247
+ digest_name = algorithm_match(algorithm)
248
+ unless digest_name && algorithm_method(algorithm)
249
+ raise SecurityError,
250
+ "Certificate lineage uses unsupported signature algorithm #{algorithm_id}"
251
+ end
252
+
253
+ options = if algorithm_method(algorithm) == :rsa &&
254
+ [SIG_RSA_PSS_WITH_SHA256, SIG_RSA_PSS_WITH_SHA512].include?(algorithm)
255
+ { rsa_padding_mode: :pss, rsa_pss_saltlen: OpenSSL::Digest.new(digest_name).digest_length }
256
+ else
257
+ {}
258
+ end
259
+ verified = certificate.public_key.verify(
260
+ OpenSSL::Digest.new(digest_name), signature, signed_data, **options
261
+ )
262
+ raise SecurityError, 'Certificate lineage signature did not verify' unless verified
263
+ rescue OpenSSL::PKey::PKeyError => e
264
+ raise SecurityError,
265
+ "Certificate lineage signature could not be verified: #{e.message}"
266
+ end
126
267
  end
127
268
 
128
269
  register(Version::V3, V3)
data/lib/app_info/dsym.rb CHANGED
@@ -56,7 +56,9 @@ module AppInfo
56
56
 
57
57
  dest_path = ::File.join(base_path, file_path)
58
58
  FileUtils.mkdir_p(::File.dirname(dest_path))
59
- entry.extract(dest_path) unless ::File.exist?(dest_path)
59
+ next if ::File.exist?(dest_path)
60
+
61
+ entry.extract(file_path, destination_directory: base_path)
60
62
  end
61
63
  end
62
64
  }.call
data/lib/app_info/hap.rb CHANGED
@@ -42,6 +42,50 @@ module AppInfo
42
42
  @module_info ||= JSON.parse(::File.read(module_info_path))
43
43
  end
44
44
 
45
+ alias module_data module_info
46
+
47
+ # @return [Hash]
48
+ def app_info
49
+ module_info['app'] || {}
50
+ end
51
+
52
+ # @return [Hash]
53
+ def module_metadata
54
+ module_info['module'] || {}
55
+ end
56
+
57
+ # @return [String, nil]
58
+ def main_element
59
+ module_metadata['mainElement']
60
+ end
61
+
62
+ # @return [Integer, nil]
63
+ def min_api_version
64
+ app_info['minAPIVersion']
65
+ end
66
+
67
+ # @return [Integer, nil]
68
+ def target_api_version
69
+ app_info['targetAPIVersion']
70
+ end
71
+
72
+ # @return [String, nil]
73
+ def compile_sdk_version
74
+ app_info['compileSdkVersion']
75
+ end
76
+
77
+ # @return [String, nil]
78
+ def api_release_type
79
+ app_info['apiReleaseType']
80
+ end
81
+
82
+ # @return [Hash]
83
+ def profiles
84
+ @profiles ||= profile_paths.to_h do |path|
85
+ [::File.basename(path, '.json'), JSON.parse(::File.read(path))]
86
+ end
87
+ end
88
+
45
89
  # @return [String]
46
90
  def module_info_path
47
91
  @module_info_path ||= ::File.join(contents, 'module.json')
@@ -49,12 +93,40 @@ module AppInfo
49
93
 
50
94
  # @return [String]
51
95
  def name
52
- # TODO: The application display name should be determined by looking up
53
- # the value of the variable named in the "label" field of the "module.json"
54
- # file within the "resources.index" file.
55
- pack_info.bundle_name
96
+ resource_value(app_info['label']) || pack_info.bundle_name
97
+ end
98
+
99
+ private
100
+
101
+ def resource_value(reference)
102
+ return reference unless reference.is_a?(String) && reference.start_with?('$string:')
103
+
104
+ resource_strings[reference.delete_prefix('$string:')]
105
+ end
106
+
107
+ def resource_strings
108
+ @resource_strings ||= begin
109
+ values = ::File.binread(resources_index_path).split("\x00")
110
+ values.each_with_index.filter_map do |value, index|
111
+ next unless value.match?(/\A[ -~]+\z/)
112
+
113
+ key = value
114
+ value = values[0...index].reverse.find { |candidate| candidate.match?(/\A[ -~]+\z/) }
115
+ next unless value
116
+
117
+ [key, value.force_encoding(Encoding::UTF_8)]
118
+ end.to_h
119
+ rescue Errno::ENOENT
120
+ {}
121
+ end
122
+ end
123
+
124
+ def resources_index_path
125
+ ::File.join(contents, 'resources.index')
56
126
  end
57
127
 
128
+ public
129
+
58
130
  def clear!
59
131
  return unless @contents
60
132
 
@@ -69,5 +141,9 @@ module AppInfo
69
141
  @icons_path = nil
70
142
  @icons = nil
71
143
  end
144
+
145
+ def profile_paths
146
+ Dir.glob(::File.join(contents, 'resources', '**', 'profile', '*.json'))
147
+ end
72
148
  end
73
149
  end
data/lib/app_info/happ.rb CHANGED
@@ -3,7 +3,20 @@
3
3
  module AppInfo
4
4
  # parser for HarmonyOS .APP file
5
5
  class HAPP < HarmonyOS
6
- def_delegators :default_entry, :icons
6
+ def_delegators :default_entry, :icons, :module_info, :app_info, :module_metadata,
7
+ :main_element, :min_api_version, :target_api_version, :compile_sdk_version,
8
+ :api_release_type, :profiles, :permissions, :use_permissions,
9
+ :features, :use_features, :device_types, :components, :activities,
10
+ :services, :schemes, :deep_links, :architectures, :native_codes
11
+
12
+ # @return [Array<HAP>]
13
+ def modules
14
+ @modules ||= pack_info.packages.filter_map do |package|
15
+ path = ::File.join(contents, "#{package['name']}.hap")
16
+ HAP.new(path) if ::File.file?(path)
17
+ end
18
+ end
19
+
7
20
  # @return [HAP]
8
21
  def default_entry
9
22
  hap_path = ::File.join(contents, "#{default_entry_name}.hap")
@@ -39,8 +52,10 @@ module AppInfo
39
52
 
40
53
  @default_entry_name = nil
41
54
  @default_entry&.clear!
55
+ @modules&.each(&:clear!)
42
56
 
43
57
  @default_entry = nil
58
+ @modules = nil
44
59
  end
45
60
  end
46
61
  end
@@ -9,6 +9,105 @@ module AppInfo
9
9
 
10
10
  def_delegators :pack_info, :build_version, :release_version, :bundle_id
11
11
 
12
+ alias identifier bundle_id
13
+ alias bundle_name bundle_id
14
+
15
+ # @return [String, nil]
16
+ def version
17
+ release_version || build_version
18
+ end
19
+
20
+ # @return [String, nil]
21
+ def display_name
22
+ name
23
+ end
24
+
25
+ # @return [Array<String>]
26
+ def permissions
27
+ values = module_info.dig('module', 'requestPermissions')
28
+ values = module_info['requestPermissions'] if values.nil?
29
+ Array(values).filter_map { |permission| permission_value(permission) }
30
+ end
31
+ alias use_permissions permissions
32
+
33
+ # @return [Array<String>]
34
+ def features
35
+ Array(module_info.dig('module', 'deviceTypes'))
36
+ end
37
+ alias use_features features
38
+
39
+ # @return [Array<String>]
40
+ def device_types
41
+ features
42
+ end
43
+
44
+ # @return [Array<Hash>]
45
+ def components
46
+ abilities + extension_abilities
47
+ end
48
+
49
+ # @return [Array<Hash>]
50
+ def activities
51
+ abilities.reject { |ability| ability['type'] == 'service' }
52
+ end
53
+
54
+ # @return [Array<Hash>]
55
+ def services
56
+ extension_abilities.select { |ability| ability['type'] == 'service' }
57
+ end
58
+
59
+ # @return [Array<String>]
60
+ def schemes
61
+ skills.flat_map { |skill| Array(skill['uris']) }.filter_map { |uri| uri['scheme'] }
62
+ end
63
+
64
+ # @return [Array<Hash>]
65
+ def deep_links
66
+ skills.flat_map { |skill| Array(skill['uris']) }
67
+ end
68
+ alias url_schemes schemes
69
+
70
+ # @return [Array<String>]
71
+ def architectures
72
+ native_codes
73
+ end
74
+
75
+ # @return [Array<String>]
76
+ def native_codes
77
+ Dir.glob(::File.join(contents, '**', 'libs', '*')).filter_map do |path|
78
+ ::File.basename(path) if ::File.directory?(path)
79
+ end.uniq
80
+ end
81
+
82
+ # @return [Array<AppInfo::HAP>]
83
+ def modules
84
+ [self]
85
+ end
86
+
87
+ def phone?
88
+ device_types.include?('phone')
89
+ end
90
+
91
+ def tablet?
92
+ device_types.include?('tablet')
93
+ end
94
+
95
+ def tv?
96
+ device_types.include?('tv')
97
+ end
98
+
99
+ def wearable?
100
+ device_types.any? { |type| %w[wearable watch].include?(type) }
101
+ end
102
+
103
+ def car?
104
+ device_types.include?('car')
105
+ end
106
+
107
+ def two_in_one?
108
+ device_types.include?('2in1') || device_types.include?('2-in-1')
109
+ end
110
+
12
111
  # return file size
13
112
  # @example Read file size in integer
14
113
  # aab.size # => 3618865
@@ -61,5 +160,29 @@ module AppInfo
61
160
  def clear!
62
161
  not_implemented_error!(__method__)
63
162
  end
163
+
164
+ private
165
+
166
+ def module_info
167
+ return {} unless respond_to?(:module_data, true)
168
+
169
+ module_data
170
+ end
171
+
172
+ def abilities
173
+ Array(module_info.dig('module', 'abilities'))
174
+ end
175
+
176
+ def extension_abilities
177
+ Array(module_info.dig('module', 'extensionAbilities'))
178
+ end
179
+
180
+ def skills
181
+ components.flat_map { |component| Array(component['skills']) }
182
+ end
183
+
184
+ def permission_value(permission)
185
+ permission.is_a?(Hash) ? permission['name'] : permission
186
+ end
64
187
  end
65
188
  end
@@ -19,7 +19,9 @@ module AppInfo::Helper
19
19
  zip_file.each do |f|
20
20
  f_path = ::File.join(base_path, f.name)
21
21
  FileUtils.mkdir_p(::File.dirname(f_path))
22
- zip_file.extract(f, f_path) unless ::File.exist?(f_path)
22
+ next if ::File.exist?(f_path)
23
+
24
+ zip_file.extract(f, f.name, destination_directory: base_path)
23
25
  end
24
26
  end
25
27
  end
@@ -104,6 +104,20 @@ module AppInfo::Helper
104
104
  content_digests
105
105
  end
106
106
 
107
+ def verify_content_digest(content_digests, digest_name)
108
+ expected = content_digests[digest_name]&.fetch(:content)
109
+ unless expected
110
+ raise AppInfo::Android::Signature::SecurityError,
111
+ 'Signature algorithms don\'t match between digests and signatures records'
112
+ end
113
+
114
+ actual = AppInfo::Android::Signature.compute_content_digest(@parser, digest_name)
115
+ return if expected.string == actual
116
+
117
+ raise AppInfo::Android::Signature::SecurityError,
118
+ "#{digest_name} content digest did not verify"
119
+ end
120
+
107
121
  # FIXME: this code not work, need fix.
108
122
  def verify_additional_attrs(attrs, _certs)
109
123
  loop_length_prefix_io(
@@ -166,12 +180,10 @@ module AppInfo::Helper
166
180
  SIG_VERITY_DSA_WITH_SHA256 = [0x25, 0x04, 0x00, 0x00].freeze # 0x0425
167
181
 
168
182
  SIG_STRIPPING_PROTECTION_ATTR_ID = [0x0d, 0xf0, 0xef, 0xbe].freeze # 0xbeeff00d
183
+ SIG_PROOF_OF_ROTATION_ATTR_ID = [0x8c, 0x6f, 0xa0, 0x3b].freeze # 0x3ba06f8c
169
184
 
170
185
  def best_algorithem(algorithems)
171
- methods = algorithems.map { |algorithem| algorithem[:method] }
172
- best_method = methods.max { |a, b| algorithem_priority(a) <=> algorithem_priority(b) }
173
- best_method_index = methods.index(best_method)
174
- algorithems[best_method_index]
186
+ algorithems.max_by { |algorithem| algorithem_priority(algorithem[:id]) || -1 }
175
187
  end
176
188
 
177
189
  def compare_algorithem(source, target)
data/lib/app_info/pe.rb CHANGED
@@ -119,8 +119,8 @@ module AppInfo
119
119
 
120
120
  # @return [Hash{String => String}] imports imports of libraries
121
121
  def imports
122
- @imports ||= pe.imports.each_with_object({}) do |import, obj|
123
- obj[import.module_name] = import.first_thunk.map(&:name).compact
122
+ @imports ||= pe.imports.to_h do |import|
123
+ [import.module_name, import.first_thunk.map(&:name).compact]
124
124
  end
125
125
  end
126
126
 
@@ -188,15 +188,20 @@ module AppInfo
188
188
  # @return [String] binary_file path
189
189
  def binary_file
190
190
  @binary_file ||= lambda {
191
- file_io = ::File.open(@file, 'rb')
192
- return @file unless file_io.read(100) =~ Helper::FileTypeDetection::ZIP_RETGEX
191
+ zip_file = ::File.open(@file, 'rb') do |file|
192
+ file.read(100) =~ Helper::FileTypeDetection::ZIP_RETGEX
193
+ end
194
+ return @file unless zip_file
193
195
 
194
196
  zip_file = Zip::File.open(@file)
195
197
  zip_entry = zip_file.glob('*.exe').first
196
198
  raise NotFoundError, 'Not found .exe file in archive file' if zip_entry.nil?
197
199
 
198
200
  exe_file = tempdir(zip_entry.name, prefix: 'pe-exe', system: true)
199
- zip_entry.extract(exe_file)
201
+ zip_entry.extract(
202
+ ::File.basename(exe_file),
203
+ destination_directory: ::File.dirname(exe_file)
204
+ )
200
205
  zip_file.close
201
206
 
202
207
  exe_file
@@ -308,7 +313,7 @@ module AppInfo
308
313
  @raw.each do |item|
309
314
  next unless item.is_a?(PEdump::VS_VERSIONINFO)
310
315
 
311
- versions = item[:Children].select { |v| v.is_a?(PEdump::StringFileInfo) }
316
+ versions = item[:Children].grep(PEdump::StringFileInfo)
312
317
  next if versions.empty?
313
318
 
314
319
  @info = versions[0][:Children][0][:Children]
@@ -163,9 +163,9 @@ module AppInfo
163
163
  newdata[pos] = data[pos, 1]
164
164
  pos += 1
165
165
  (0...width).each do |_|
166
- newdata[pos + 0] = data[pos + 2, 1]
166
+ newdata[pos] = data[pos + 2, 1]
167
167
  newdata[pos + 1] = data[pos + 1, 1]
168
- newdata[pos + 2] = data[pos + 0, 1]
168
+ newdata[pos + 2] = data[pos, 1]
169
169
  newdata[pos + 3] = data[pos + 3, 1]
170
170
  pos += 4
171
171
  end
@@ -43,8 +43,8 @@ module AppInfo
43
43
  end
44
44
 
45
45
  def define_packages(doc)
46
- @packages = doc.package.each_with_object({}) do |package, obj|
47
- obj[package.package_name] = Resources::Package.new(package)
46
+ @packages = doc.package.to_h do |package|
47
+ [package.package_name, Resources::Package.new(package)]
48
48
  end
49
49
  end
50
50
 
@@ -158,7 +158,7 @@ module AppInfo
158
158
  include Helper::Protobuf
159
159
  extend Forwardable
160
160
 
161
- attr_reader :locale, :config, :original_value, :value, :type
161
+ attr_reader :config, :original_value, :value, :type
162
162
 
163
163
  def initialize(doc, package)
164
164
  @package = package
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppInfo
4
- VERSION = '3.3.2'
4
+ VERSION = '3.4.0'
5
5
  end