app-info 3.3.1 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e1a690f39cd0bfe4c6fe3f5eee14f3cd0af7a5be9635875bb6bda0ca7fa1e47
4
- data.tar.gz: c07b7ae5e8d2d995a5c1d32cc7e0de1fb5e6680450936c3e08344687ed14669f
3
+ metadata.gz: 8240bf7236451c5971d0ee28d067689605f6470b0f64cd3ff21d54f3dac415c7
4
+ data.tar.gz: bb88f65d0f66be67b0f94ddfa4d1061346b974b1974eb36b7be7c2fbe8be3c73
5
5
  SHA512:
6
- metadata.gz: b9f5504987f1173bc82880b974f735fba28d352223f8940aea3e497cf7c2ba9350481c52a9ca95cd9b5fdd3d68d273947e6e50201834c61210a153b8d7554cdb
7
- data.tar.gz: c43deb531c6fa974a5da4f0816526c099e7bd20f1d7cde286cb941df596e84932cddc044e403f3737124f2ab6b43f2250c840b6fac3fc339bfd17c452d560315
6
+ metadata.gz: 9bea1da034bd88f84768c0d899cf5b03d4bc746e90fc1252d55e01d9006095287c83b23f3e5e865fb97386eb49d659f4ac475c8812c04bad30b39ee4b7a78f5a
7
+ data.tar.gz: 7ffeba0577eefc612f8963fd85005ecc763df7bf03f368807276a5537d00902e2ed2ce48da4297c331cc2b688c5e56024716c8a8ecf9fdbdc08f7167b1f809b2
@@ -30,8 +30,7 @@ jobs:
30
30
  strategy:
31
31
  fail-fast: false
32
32
  matrix:
33
- # https://endoflife.date/ruby
34
- ruby: [ ruby-3.1, ruby-3.2, ruby-3.3 ]
33
+ ruby: [ ruby-3.1, ruby-3.2, ruby-3.3, ruby-3.4, ruby-4.0 ]
35
34
  experimental: [ false ]
36
35
  include:
37
36
  - ruby: head
data/.rubocop.yml CHANGED
@@ -84,6 +84,15 @@ Style/Documentation:
84
84
  Style/ClassAndModuleChildren:
85
85
  Enabled: false
86
86
 
87
+ Style/OneClassPerFile:
88
+ Exclude:
89
+ - 'lib/app_info/core_ext/object/try.rb'
90
+ - 'lib/app_info/core_ext/string/inflector.rb'
91
+
92
+ Naming/PredicateMethod:
93
+ Exclude:
94
+ - 'lib/app_info/png_uncrush.rb'
95
+
87
96
  Style/PerlBackrefs:
88
97
  Exclude:
89
98
  - 'lib/app_info/core_ext/string/inflector.rb'
data/CHANGELOG.md CHANGED
@@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
9
9
 
10
10
  > List all changes before release a new version.
11
11
 
12
+ ## [3.4.0] (2026-09-27)
13
+
14
+ ### Added
15
+
16
+ - Add Android verify v2, v3 signatures and certificate lineage.
17
+ - Add deeper HarmonyOS `.hap`/`.app` metadata parsing: `module_info`, `app_info`, `module_metadata`, `profiles`, `permissions`, `features`, device type detection and more.
18
+
19
+ ### Fixed
20
+
21
+ - Upgrade rubyzip >= 3.4.0 to fix path traversal vulnerability issue.
22
+ - Fix archive extraction with rubyzip 3.x by passing `destination_directory` instead of an absolute entry path.
23
+ - Fix Android v2/v3 signature verification with rubyzip 3.x by parsing the end of central directory directly.
24
+
25
+ ## [3.3.2] (2025-05-23)
26
+
27
+ ### Added
28
+
29
+ - Add determine mobile provision file content.
30
+
12
31
  ## [3.3.1] (2025-04-27)
13
32
 
14
33
  ### Fixed
@@ -71,7 +90,7 @@ Dropped Ruby 2.5 ~ 3.0 support (no changes required.).
71
90
 
72
91
  - Add `AppInfo::File` base class for all parsers.
73
92
  - Add `AppInfo::Certifiate` X509 certificate wrapped and apply in Android/MobileProvision.
74
- - Re-organize categories `.platform` to `.manufacturer`, `.platform` and `.device` for all parsers.
93
+ - Re-organize categories `.platform` to `.manufacturer`, `.platform` and `.device` for all parsers.
75
94
  - Remove `.sign_version` method in Android parser.
76
95
  - Rename `.file_type` to `.format` method in all parers and return a `AppInfo::Format` type.
77
96
  - Remove duplice `AppInfo::AndroidDevice` class.
@@ -215,6 +234,7 @@ Dropped Ruby 2.5 ~ 3.0 support (no changes required.).
215
234
 
216
235
  - Force write all icon data with `ASCII-8BIT`
217
236
  - Force convert developer cert name to `UTF-8`
237
+
218
238
  ## [2.6.1] (2021-08-26)
219
239
 
220
240
  ### Fixed
@@ -296,6 +316,7 @@ Dropped Ruby 2.5 ~ 3.0 support (no changes required.).
296
316
  ### Changed
297
317
 
298
318
  - Change `IPA::ExportType::INHOUSE` to `IPA::ExportType::ENTERPRISE` and change the value. #[24](https://github.com/icyleaf/app-info/pull/24)
319
+
299
320
  ### Added
300
321
 
301
322
  - Add `plugins`, `frameworks` to `AppInfo::IPA`. #[25](https://github.com/icyleaf/app-info/pull/25)
@@ -373,7 +394,10 @@ Dropped Ruby 2.5 ~ 3.0 support (no changes required.).
373
394
 
374
395
  - Updated dependency of CFPropertly list be a range between 2.3.4. (thanks @[cschroed](https://github.com/cschroed))
375
396
 
376
- [Unreleased]: https://github.com/icyleaf/app-info/compare/v3.3.0..HEAD
397
+ [Unreleased]: https://github.com/icyleaf/app-info/compare/v3.4.0..HEAD
398
+ [3.4.0]: https://github.com/icyleaf/app-info/compare/v3.3.2...v3.4.0
399
+ [3.3.2]: https://github.com/icyleaf/app-info/compare/v3.3.1...v3.3.2
400
+ [3.3.1]: https://github.com/icyleaf/app-info/compare/v3.3.0...v3.3.1
377
401
  [3.3.0]: https://github.com/icyleaf/app-info/compare/v3.2.0...v3.3.0
378
402
  [3.2.0]: https://github.com/icyleaf/app-info/compare/v3.1.4...v3.2.0
379
403
  [3.1.4]: https://github.com/icyleaf/app-info/compare/v3.1.2...v3.1.4
@@ -417,4 +441,3 @@ Dropped Ruby 2.5 ~ 3.0 support (no changes required.).
417
441
  [1.1.2]: https://github.com/icyleaf/app-info/compare/v1.0.5...v1.1.2
418
442
  [1.1.0]: https://github.com/icyleaf/app-info/compare/v1.0.5...v1.1.0
419
443
  [1.0.5]: https://github.com/icyleaf/app-info/compare/v0.9.0...v1.0.5
420
-
data/README.md CHANGED
@@ -7,23 +7,23 @@
7
7
 
8
8
  Teardown tool for mobile app (iOS: ipa, Android: apk/aab, HarmonyOS: .hap/.app file), macOS app, dSYM.zip file and Windows PE file.
9
9
 
10
- Analysis metedata like version, name, icon etc.
10
+ Analysis metadata like version, name, icon etc.
11
11
 
12
12
  ## Support
13
13
 
14
14
  - Android file
15
15
  - `.apk`
16
- - `.aab` (Androld App Bundle)
16
+ - `.aab` (Android App Bundle)
17
17
  - iOS, Apple TV file
18
18
  - `.ipa`
19
19
  - `Info.plist` file
20
20
  - `.mobileprovision`/`.provisionprofile` file
21
- - HarmonyOS file (basic)
21
+ - HarmonyOS file
22
22
  - `.hap`
23
23
  - `.app`
24
- - macOS App file (archived by starnd pkzip format)
24
+ - macOS App file (archived by standard pkzip format)
25
25
  - `.app.zip`
26
- - dSYMs file (archived by starnd pkzip format)
26
+ - dSYMs file (archived by standard pkzip format)
27
27
  - `.dSYM.zip`
28
28
  - Windows PE file
29
29
  - `.exe`
@@ -71,6 +71,8 @@ parser = AppInfo.parse('ipad.ipa')
71
71
  parser = AppInfo.parse('apple-tv.ipa')
72
72
  parser = AppInfo.parse('android.apk')
73
73
  parser = AppInfo.parse('android.aab')
74
+ parser = AppInfo.parse('harmony.hap')
75
+ parser = AppInfo.parse('harmony.app')
74
76
  parser = AppInfo.parse('u-u-i-d.mobileprovision')
75
77
  parser = AppInfo.parse('macOS.App.zip')
76
78
  parser = AppInfo.parse('App.dSYm.zip')
@@ -81,6 +83,8 @@ parser = AppInfo.parse('win.zip')
81
83
  parser = AppInfo::IPA.new('iphone-ipad-unversal-appletv.ipa')
82
84
  parser = AppInfo::APK.new('android.apk')
83
85
  parser = AppInfo::AAB.new('android.aab')
86
+ parser = AppInfo::HAP.new('harmony.hap')
87
+ parser = AppInfo::HAPP.new('harmony.app')
84
88
  parser = AppInfo::InfoPlist.new('Info.plist')
85
89
  parser = AppInfo::MobileProvision.new('uuid.mobileprovision')
86
90
  parser = AppInfo::Macos.new('App.dSYm.zip')
@@ -145,7 +149,7 @@ ipa.plugins
145
149
  ipa.url_schemes
146
150
  # => [{:name=>"Web", :role=>"Editor", :schemes=>["app-info", "app_info"]}]
147
151
 
148
- # get query schemes
152
+ # get url schemes
149
153
  ipa.url_schemes
150
154
  # => ["twitter", "instagram", "www-x-callback"]
151
155
 
@@ -252,28 +256,129 @@ android.signatures
252
256
 
253
257
  ### HarmonyOS
254
258
 
255
- Accept `.hap` and `.app` HarmonyOS file. Only metabase, except resources mapping.
259
+ Accept `.hap` and `.app` HarmonyOS file. Only metadata, except resources mapping.
256
260
 
257
261
  ```ruby
258
262
  hap = AppInfo.parse('app.hap')
259
263
 
260
264
  # get app file size
261
- android.size
262
- # => 2013213
265
+ hap.size
266
+ # => 142283
263
267
 
264
- # get app file size in human reable.
265
- android.size(human_size: true)
266
- # => 21 MB
268
+ # get app file size in human readable.
269
+ hap.size(human_size: true)
270
+ # => "138.95 KB"
267
271
 
268
272
  # get app release version
269
- android.release_version
270
- # => 1.0
273
+ hap.release_version
274
+ # => "1.0.0"
271
275
 
272
- # get app package name
273
- android.bundle_id
274
- # => com.icyleaf.AppInfoDemo
276
+ # get app build version
277
+ hap.build_version
278
+ # => 1000000
279
+
280
+ # get app version (release_version, fallback to build_version)
281
+ hap.version
282
+ # => "1.0.0"
283
+
284
+ # get app package name (alias: identifier, bundle_name)
285
+ hap.bundle_id
286
+ # => "com.example.myapplication"
287
+
288
+ # get app display name (alias: name)
289
+ hap.display_name
290
+ # => "MyApplication"
291
+
292
+ # get app icons
293
+ hap.icons
294
+ # => [{:name=>"app_icon.png", :file=>"/temp/dir/resources/base/media/app_icon.png", :dimensions=>[41, 41]}]
295
+
296
+ # get the whole parsed module.json (alias: module_data)
297
+ hap.module_info
298
+ # => {"app" => {...}, "module" => {...}}
299
+
300
+ # get app section of module.json
301
+ hap.app_info
302
+ # => {"minAPIVersion" => 50000012, "targetAPIVersion" => 50000012, "label" => "$string:app_name", "bundleName" => "com.example.myapplication", ...}
303
+
304
+ # get module section of module.json
305
+ hap.module_metadata
306
+ # => {"mainElement" => "EntryAbility", "deviceTypes" => ["phone", "tablet", "2in1"], "abilities" => [...], ...}
307
+
308
+ # get main entry ability name
309
+ hap.main_element
310
+ # => "EntryAbility"
311
+
312
+ # get api version
313
+ hap.min_api_version
314
+ # => 50000012
315
+ hap.target_api_version
316
+ # => 50000012
317
+
318
+ # get compile sdk version and release type
319
+ hap.compile_sdk_version
320
+ # => "5.0.0.25"
321
+ hap.api_release_type
322
+ # => "Beta1"
323
+
324
+ # get resource profile files (name => parsed json)
325
+ hap.profiles
326
+ # => {"main_pages" => {"src" => ["pages/Index"]}, "backup_config" => {"allowToBackupRestore" => true}}
327
+
328
+ # get requested permissions (alias: use_permissions)
329
+ hap.permissions
330
+ # => []
331
+
332
+ # get supported device types (alias: features, use_features)
333
+ hap.device_types
334
+ # => ["phone", "tablet", "2in1"]
335
+
336
+ # detect device type
337
+ hap.phone?
338
+ # => true
339
+ hap.tablet?
340
+ # => true
341
+ hap.tv?
342
+ # => false
343
+ hap.wearable?
344
+ # => false
345
+ hap.car?
346
+ # => false
347
+ hap.two_in_one?
348
+ # => true
349
+
350
+ # get abilities and services
351
+ hap.activities
352
+ hap.services
353
+ hap.components
354
+
355
+ # get deep links host and schemes
356
+ hap.deep_links
357
+ hap.schemes
358
+
359
+ # get native architectures (alias: architectures)
360
+ hap.native_codes
361
+ # => []
362
+ ```
363
+
364
+ The `.app` file is a bundle that contains one or more `.hap` modules:
365
+
366
+ ```ruby
367
+ app = AppInfo.parse('app.app')
368
+
369
+ # get all hap modules
370
+ app.modules
371
+ # => [#<AppInfo::HAP:...>]
372
+
373
+ # get the default (entry) module
374
+ app.default_entry
375
+ # => #<AppInfo::HAP:...>
376
+ app.default_entry_name
377
+ # => "entry-default"
275
378
  ```
276
379
 
380
+ All metadata methods above (such as `module_info`, `app_info`, `module_metadata`, `profiles`, `device_types`, ...) are also available on the `.app` parser and are delegated to its default entry module. The `module_data` alias is only defined for `.hap`.
381
+
277
382
  ### macOS
278
383
 
279
384
  Only accept zipped macOS file.
@@ -417,9 +522,9 @@ It is possible to use this gem as a command line interface to parse mobile app:
417
522
  ```
418
523
  > app-info
419
524
 
420
- app-info (2.7.0)> p = AppInfo.parse('/path/to/app')
525
+ app-info (3.4.0)> p = AppInfo.parse('/path/to/app')
421
526
  => #<AppInfo::APK::......>
422
- app-info (2.7.0)> p.name
527
+ app-info (3.4.0)> p.name
423
528
  => "AppName"
424
529
  ```
425
530
 
data/app_info.gemspec CHANGED
@@ -25,8 +25,8 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency 'base64', '~> 0.2.0'
26
26
  spec.add_dependency 'image_size', '>= 1.5', '< 3.5'
27
27
  spec.add_dependency 'ruby-macho', '>= 1.4', '< 5'
28
- spec.add_dependency 'android_parser', '>= 2.7', '< 3.0'
29
- spec.add_dependency 'rubyzip', '>= 1.2', '< 3.0'
28
+ spec.add_dependency 'android_parser', '>= 2.7.1', '< 3.0'
29
+ spec.add_dependency "rubyzip", ">= 3.4.0"
30
30
  spec.add_dependency 'uuidtools', '>= 2.1.5', '< 2.3.0'
31
31
  spec.add_dependency 'icns', '~> 0.2.0'
32
32
  spec.add_dependency 'pedump', '~> 0.6.2'
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'digest'
4
+
3
5
  module AppInfo
4
6
  class Android < File
5
- # Android Signature
6
- #
7
- # Support digest and length:
8
- #
9
- # RSA:1024、2048、4096、8192、16384
10
- # EC:NIST P-256、P-384、P-521
11
- # DSA:1024、2048、3072
12
7
  module Signature
13
8
  class VersionError < Error; end
14
9
  class SecurityError < Error; end
@@ -21,69 +16,36 @@ module AppInfo
21
16
  V4 = 4
22
17
  end
23
18
 
24
- # All registerd verions to verify
25
- #
26
- # key is the version
27
- # value is the class
28
19
  @versions = {}
29
20
 
30
21
  class << self
31
- # Verify Android Signature
32
- #
33
- # @example Get unverified v1 certificates, verified v2 certificates,
34
- # and not found v3 certificate
35
- #
36
- # signature.versions(parser)
37
- # # => [
38
- # # {
39
- # # version: 1,
40
- # # verified: false,
41
- # # certificates: [<AppInfo::Certificate>, ...],
42
- # # verifier: AppInfo::Androig::Signature
43
- # # },
44
- # # {
45
- # # version: 2,
46
- # # verified: false,
47
- # # certificates: [<AppInfo::Certificate>, ...],
48
- # # verifier: AppInfo::Androig::Signature
49
- # # },
50
- # # {
51
- # # version: 3
52
- # # }
53
- # # ]
54
- # @todo version 4 no implantation yet
55
- # @param [AppInfo::File] parser
56
- # @param [Version, Integer] min_version
57
- # @return [Array<Hash>] versions
58
- def verify(parser, min_version: Version::V4)
22
+ def verify(parser, min_version: Version::V4, sdk: nil)
59
23
  min_version = min_version.to_i if min_version.is_a?(String)
60
- if min_version && min_version > Version::V4
24
+ if min_version > Version::V4
61
25
  raise VersionError,
62
26
  "No signature found in #{min_version} scheme or newer for android file"
63
27
  end
64
-
65
28
  if min_version.zero?
66
29
  raise VersionError,
67
30
  "Unkonwn version: #{min_version}, avaiables in 1/2/3 and 4 (no implantation yet)"
68
31
  end
69
32
 
70
- # try full version signatures if min_version is nil
71
33
  versions = min_version.downto(Version::V1).each_with_object([]) do |version, signatures|
72
34
  next unless kclass = fetch(version)
73
35
 
74
36
  data = { version: version }
75
37
  begin
76
- verifier = kclass.verify(parser)
38
+ verifier = kclass.verify(parser, sdk: sdk)
77
39
  data[:verified] = verifier.verified
78
40
  data[:certificates] = verifier.certificates
79
41
  data[:verifier] = verifier
80
- rescue SecurityError, NotFoundError
81
- # not this version, try the low version
42
+ data[:verification_errors] = verifier.verification_errors
43
+ rescue SecurityError, NotFoundError => e
44
+ e
82
45
  ensure
83
46
  signatures << data
84
47
  end
85
48
  end
86
-
87
49
  versions.sort_by { |entry| entry[:version] }
88
50
  end
89
51
 
@@ -107,6 +69,63 @@ module AppInfo
107
69
  UINT32_MAX_VALUE = 2_147_483_647
108
70
  UINT32_SIZE = 4
109
71
  UINT64_SIZE = 8
72
+ CONTENT_CHUNK_SIZE = 1 << 20
73
+
74
+ class << self
75
+ def compute_content_digest(parser, digest_name)
76
+ info = Info.new(Version::V2, parser, parser.logger)
77
+ digest_class = Digest.const_get(digest_name)
78
+ chunks = content_chunks(parser, info, digest_class)
79
+ digest_class.digest("\x5a".b + [chunks.length].pack('V') + chunks.join)
80
+ end
81
+
82
+ private
83
+
84
+ def content_chunks(parser, info, digest_class)
85
+ chunks = []
86
+ pending = ''.b
87
+ add_chunk = lambda do |data|
88
+ pending << data
89
+ while pending.bytesize >= CONTENT_CHUNK_SIZE
90
+ chunks << chunk_digest(pending.slice!(0, CONTENT_CHUNK_SIZE), digest_class)
91
+ end
92
+ end
93
+ flush_chunk = lambda do
94
+ next if pending.empty?
95
+
96
+ chunks << chunk_digest(pending, digest_class)
97
+ pending = ''.b
98
+ end
99
+
100
+ ::File.open(parser.file, 'rb') do |file|
101
+ [[0, info.signing_block_offset],
102
+ [info.cdir_offset, info.eocd_offset]].each do |start, finish|
103
+ read_content(file, start, finish, add_chunk)
104
+ flush_chunk.call
105
+ end
106
+ file.seek(info.eocd_offset)
107
+ eocd = file.read(info.file_size - info.eocd_offset)
108
+ eocd[16, Signature::UINT32_SIZE] = [info.signing_block_offset].pack('V')
109
+ add_chunk.call(eocd)
110
+ flush_chunk.call
111
+ end
112
+ chunks
113
+ end
114
+
115
+ def read_content(file, start, finish, add_chunk)
116
+ file.seek(start)
117
+ remaining = finish - start
118
+ while remaining.positive?
119
+ length = [remaining, CONTENT_CHUNK_SIZE].min
120
+ add_chunk.call(file.read(length))
121
+ remaining -= length
122
+ end
123
+ end
124
+
125
+ def chunk_digest(chunk, digest_class)
126
+ digest_class.digest("\xa5".b + [chunk.bytesize].pack('V') + chunk)
127
+ end
128
+ end
110
129
  end
111
130
  end
112
131
  end
@@ -6,19 +6,26 @@ module AppInfo
6
6
  class Android < File
7
7
  module Signature
8
8
  class Base
9
- def self.verify(parser)
9
+ def self.verify(parser, **options)
10
10
  instance = new(parser)
11
- instance.verify
11
+ instance.verify(**options)
12
12
  instance
13
13
  end
14
14
 
15
15
  DESCRIPTION = 'APK Signature Scheme'
16
16
 
17
- attr_reader :verified
17
+ attr_reader :verified, :verification_errors
18
18
 
19
19
  def initialize(parser)
20
20
  @parser = parser
21
21
  @verified = false
22
+ @verification_errors = []
23
+ end
24
+
25
+ def add_verification_error(code, message, signer: nil)
26
+ error = { code: code, message: message }
27
+ error[:signer] = signer unless signer.nil?
28
+ @verification_errors << error
22
29
  end
23
30
 
24
31
  # @abstract Subclass and override {#verify} to implement
@@ -29,6 +29,18 @@ module AppInfo
29
29
  0x6b, 0x20, 0x34, 0x32
30
30
  ].freeze
31
31
 
32
+ # End of central directory record signature: PK\x05\x06
33
+ EOCD_SIGNATURE = "PK\x05\x06".b.freeze
34
+ # Zip64 end of central directory record and locator signatures.
35
+ ZIP64_END_OF_CD_SIGNATURE = "PK\x06\x06".b.freeze
36
+ ZIP64_EOCD_LOCATOR_SIGNATURE = "PK\x06\x07".b.freeze
37
+ # Static size of the end of central directory record.
38
+ STATIC_EOCD_SIZE = 22
39
+ # Maximum size of the end of central directory search window.
40
+ MAX_END_OF_CD_SIZE = 65_557
41
+ # Offset of the central directory offset field within the EOCD record.
42
+ EOCD_CDIR_OFFSET_FIELD = 16
43
+
32
44
  attr_reader :total_size, :pairs, :magic, :logger
33
45
 
34
46
  def initialize(version, parser, logger)
@@ -87,7 +99,36 @@ module AppInfo
87
99
  end
88
100
 
89
101
  def zip64?
90
- zip_io.zip64_file?(start_buffer)
102
+ return @zip64 unless @zip64.nil?
103
+
104
+ tail = ::File.open(@parser.file, 'rb') do |file|
105
+ size = file_size
106
+ file.seek(size - [size, MAX_END_OF_CD_SIZE].min)
107
+ file.read
108
+ end
109
+
110
+ @zip64 = tail.include?(ZIP64_END_OF_CD_SIGNATURE) &&
111
+ tail.include?(ZIP64_EOCD_LOCATOR_SIGNATURE)
112
+ end
113
+
114
+ def signing_block_offset
115
+ cdir_offset - signature_block.size - SIG_SIZE_OF_BLOCK_SIZE
116
+ end
117
+
118
+ def file_size
119
+ ::File.size(@parser.file)
120
+ end
121
+
122
+ def eocd_offset
123
+ tail_size = [file_size, MAX_END_OF_CD_SIZE].min
124
+ ::File.open(@parser.file, 'rb') do |file|
125
+ file.seek(file_size - tail_size)
126
+ tail = file.read(tail_size)
127
+ index = tail.rindex(EOCD_SIGNATURE)
128
+ raise NotFoundError, 'End of central directory not found' unless index
129
+
130
+ file_size - tail_size + index
131
+ end
91
132
  end
92
133
 
93
134
  def pares_signatures_pairs
@@ -135,18 +176,11 @@ module AppInfo
135
176
  end
136
177
 
137
178
  def cdir_offset
138
- @cdir_offset ||= lambda {
139
- eocd_buffer = zip_io.get_e_o_c_d(start_buffer)
140
- eocd_buffer[12..16].unpack1('V')
141
- }.call
142
- end
143
-
144
- def start_buffer
145
- @start_buffer ||= zip_io.start_buf(file_io)
146
- end
147
-
148
- def zip_io
149
- @zip_io ||= @parser.zip
179
+ @cdir_offset ||= ::File.open(@parser.file, 'rb') do |file|
180
+ file.seek(eocd_offset)
181
+ eocd = file.read(STATIC_EOCD_SIZE)
182
+ eocd[EOCD_CDIR_OFFSET_FIELD, UINT32_SIZE].unpack1('V')
183
+ end
150
184
  end
151
185
 
152
186
  def file_io
@@ -19,7 +19,7 @@ module AppInfo
19
19
  DESCRIPTION
20
20
  end
21
21
 
22
- def verify
22
+ def verify(**_options)
23
23
  @signatures = fetch_signatures
24
24
  @certificates = fetch_certificates
25
25
 
@@ -33,10 +33,19 @@ module AppInfo
33
33
 
34
34
  # Verify
35
35
  # @todo verified signatures
36
- def verify
36
+ def verify(**_options)
37
37
  signers_block = singers_block(BLOCK_ID)
38
38
  @certificates, @digests = verified_certs(signers_block, verify: true)
39
- # @verified = true
39
+ @verified = true
40
+ rescue SecurityError => e
41
+ @certificates ||= []
42
+ @digests ||= {}
43
+ code = if e.message.include?('content digest')
44
+ :content_digest_mismatch
45
+ else
46
+ :signature_invalid
47
+ end
48
+ add_verification_error(code, e.message)
40
49
  end
41
50
 
42
51
  private
@@ -92,12 +101,8 @@ module AppInfo
92
101
  'Signature algorithms don\'t match between digests and signatures records'
93
102
  end
94
103
 
95
- previous_digest = content_digests.fetch(algorithems_digest)
104
+ verify_content_digest(content_digests, algorithems_digest)
96
105
  content_digests[algorithems_digest] = content_digest
97
- if previous_digest && previous_digest[:content] != content_digest
98
- raise SecurityError,
99
- 'Signature algorithms don\'t match between digests and signatures records'
100
- end
101
106
 
102
107
  certificates = length_prefix_block(signed_data)
103
108
  certs = signed_data_certs(certificates)