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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +26 -3
- data/README.md +124 -19
- data/app_info.gemspec +2 -2
- data/lib/app_info/android/signature.rb +65 -46
- data/lib/app_info/android/signatures/base.rb +10 -3
- data/lib/app_info/android/signatures/info.rb +47 -13
- data/lib/app_info/android/signatures/v1.rb +1 -1
- data/lib/app_info/android/signatures/v2.rb +12 -7
- data/lib/app_info/android/signatures/v3.rb +156 -15
- data/lib/app_info/dsym.rb +3 -1
- data/lib/app_info/hap.rb +80 -4
- data/lib/app_info/happ.rb +16 -1
- data/lib/app_info/harmonyos.rb +123 -0
- data/lib/app_info/helper/archive.rb +3 -1
- data/lib/app_info/helper/signatures.rb +16 -4
- data/lib/app_info/pe.rb +11 -6
- data/lib/app_info/png_uncrush.rb +2 -2
- data/lib/app_info/protobuf/resources.rb +3 -3
- data/lib/app_info/version.rb +1 -1
- metadata +7 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8240bf7236451c5971d0ee28d067689605f6470b0f64cd3ff21d54f3dac415c7
|
|
4
|
+
data.tar.gz: bb88f65d0f66be67b0f94ddfa4d1061346b974b1974eb36b7be7c2fbe8be3c73
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9bea1da034bd88f84768c0d899cf5b03d4bc746e90fc1252d55e01d9006095287c83b23f3e5e865fb97386eb49d659f4ac475c8812c04bad30b39ee4b7a78f5a
|
|
7
|
+
data.tar.gz: 7ffeba0577eefc612f8963fd85005ecc763df7bf03f368807276a5537d00902e2ed2ce48da4297c331cc2b688c5e56024716c8a8ecf9fdbdc08f7167b1f809b2
|
data/.github/workflows/ci.yml
CHANGED
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`
|
|
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.
|
|
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
|
|
10
|
+
Analysis metadata like version, name, icon etc.
|
|
11
11
|
|
|
12
12
|
## Support
|
|
13
13
|
|
|
14
14
|
- Android file
|
|
15
15
|
- `.apk`
|
|
16
|
-
- `.aab` (
|
|
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
|
|
21
|
+
- HarmonyOS file
|
|
22
22
|
- `.hap`
|
|
23
23
|
- `.app`
|
|
24
|
-
- macOS App file (archived by
|
|
24
|
+
- macOS App file (archived by standard pkzip format)
|
|
25
25
|
- `.app.zip`
|
|
26
|
-
- dSYMs file (archived by
|
|
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
|
|
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
|
|
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
|
-
|
|
262
|
-
# =>
|
|
265
|
+
hap.size
|
|
266
|
+
# => 142283
|
|
263
267
|
|
|
264
|
-
# get app file size in human
|
|
265
|
-
|
|
266
|
-
# =>
|
|
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
|
-
|
|
270
|
-
# => 1.0
|
|
273
|
+
hap.release_version
|
|
274
|
+
# => "1.0.0"
|
|
271
275
|
|
|
272
|
-
# get app
|
|
273
|
-
|
|
274
|
-
# =>
|
|
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 (
|
|
525
|
+
app-info (3.4.0)> p = AppInfo.parse('/path/to/app')
|
|
421
526
|
=> #<AppInfo::APK::......>
|
|
422
|
-
app-info (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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 ||=
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|