app-info 2.8.2 → 3.0.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/dependabot.yml +12 -7
- data/.github/workflows/ci.yml +7 -5
- data/.github/workflows/create_release.yml +15 -0
- data/.rubocop.yml +33 -11
- data/CHANGELOG.md +107 -1
- data/Gemfile +10 -5
- data/README.md +82 -15
- data/Rakefile +11 -0
- data/app_info.gemspec +14 -5
- data/lib/app_info/aab.rb +76 -110
- data/lib/app_info/android/signature.rb +114 -0
- data/lib/app_info/android/signatures/base.rb +53 -0
- data/lib/app_info/android/signatures/info.rb +158 -0
- data/lib/app_info/android/signatures/v1.rb +63 -0
- data/lib/app_info/android/signatures/v2.rb +121 -0
- data/lib/app_info/android/signatures/v3.rb +131 -0
- data/lib/app_info/android/signatures/v4.rb +18 -0
- data/lib/app_info/android.rb +181 -0
- data/lib/app_info/apk.rb +77 -112
- data/lib/app_info/apple.rb +192 -0
- data/lib/app_info/certificate.rb +176 -0
- data/lib/app_info/const.rb +76 -0
- data/lib/app_info/core_ext/object/try.rb +3 -1
- data/lib/app_info/core_ext/string/inflector.rb +2 -0
- data/lib/app_info/dsym/debug_info.rb +81 -0
- data/lib/app_info/dsym/macho.rb +62 -0
- data/lib/app_info/dsym.rb +35 -135
- data/lib/app_info/error.rb +3 -1
- data/lib/app_info/file.rb +49 -0
- data/lib/app_info/helper/archive.rb +37 -0
- data/lib/app_info/helper/file_size.rb +25 -0
- data/lib/app_info/helper/generate_class.rb +29 -0
- data/lib/app_info/helper/protobuf.rb +12 -0
- data/lib/app_info/helper/signatures.rb +229 -0
- data/lib/app_info/helper.rb +5 -128
- data/lib/app_info/info_plist.rb +66 -29
- data/lib/app_info/ipa/framework.rb +4 -4
- data/lib/app_info/ipa.rb +61 -135
- data/lib/app_info/macos.rb +54 -102
- data/lib/app_info/mobile_provision.rb +66 -48
- data/lib/app_info/pe.rb +322 -0
- data/lib/app_info/png_uncrush.rb +25 -5
- data/lib/app_info/proguard.rb +39 -22
- data/lib/app_info/protobuf/manifest.rb +22 -11
- data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
- data/lib/app_info/protobuf/models/README.md +8 -1
- data/lib/app_info/protobuf/models/Resources.proto +51 -0
- data/lib/app_info/protobuf/models/Resources_pb.rb +42 -0
- data/lib/app_info/protobuf/resources.rb +5 -5
- data/lib/app_info/version.rb +1 -1
- data/lib/app_info.rb +93 -43
- metadata +57 -37
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo
|
4
|
+
class Android < File
|
5
|
+
module Signature
|
6
|
+
# Android v2 Signature
|
7
|
+
#
|
8
|
+
# FULL FORMAT:
|
9
|
+
# OFFSET DATA TYPE DESCRIPTION
|
10
|
+
# * @+0 bytes uint32: signer size in bytes
|
11
|
+
# * @+4 bytes payload signer block
|
12
|
+
# * @+0 bytes unit32: signed data size in bytes
|
13
|
+
# * @+4 bytes payload signed data block
|
14
|
+
# * @+0 bytes unit32: digests with size in bytes
|
15
|
+
# * @+0 bytes unit32: digests with size in bytes
|
16
|
+
# * @+X bytes unit32: signatures with size in bytes
|
17
|
+
# * @+X+4 bytes payload signed data block
|
18
|
+
# * @+Y bytes unit32: public key with size in bytes
|
19
|
+
# * @+Y+4 bytes payload signed data block
|
20
|
+
class V2 < Base
|
21
|
+
include AppInfo::Helper::IOBlock
|
22
|
+
include AppInfo::Helper::Signatures
|
23
|
+
include AppInfo::Helper::Algorithm
|
24
|
+
|
25
|
+
# V2 Signature ID 0x7109871a
|
26
|
+
BLOCK_ID = [0x1a, 0x87, 0x09, 0x71].freeze
|
27
|
+
|
28
|
+
attr_reader :certificates, :digests
|
29
|
+
|
30
|
+
def version
|
31
|
+
Version::V2
|
32
|
+
end
|
33
|
+
|
34
|
+
# Verify
|
35
|
+
# @todo verified signatures
|
36
|
+
def verify
|
37
|
+
signers_block = singers_block(BLOCK_ID)
|
38
|
+
@certificates, @digests = verified_certs(signers_block, verify: true)
|
39
|
+
# @verified = true
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def verified_certs(signers_block, verify:)
|
45
|
+
unless (signers = length_prefix_block(signers_block))
|
46
|
+
raise SecurityError, 'Not found signers'
|
47
|
+
end
|
48
|
+
|
49
|
+
certificates = []
|
50
|
+
content_digests = {}
|
51
|
+
loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
|
52
|
+
signer_certs, signer_digests = extract_signer_data(signer, verify: verify)
|
53
|
+
certificates.concat(signer_certs)
|
54
|
+
content_digests.merge!(signer_digests)
|
55
|
+
end
|
56
|
+
raise SecurityError, 'No signers found' if certificates.empty?
|
57
|
+
|
58
|
+
[certificates, content_digests]
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_signer_data(signer, verify:)
|
62
|
+
# raw data
|
63
|
+
signed_data = length_prefix_block(signer)
|
64
|
+
signatures = length_prefix_block(signer)
|
65
|
+
public_key = length_prefix_block(signer, raw: true)
|
66
|
+
|
67
|
+
# FIXME: extract code below and re-organize
|
68
|
+
|
69
|
+
algorithems = signature_algorithms(signatures)
|
70
|
+
raise SecurityError, 'No signatures found' if verify && algorithems.empty?
|
71
|
+
|
72
|
+
# find best algorithem to verify signed data with public key and signature
|
73
|
+
unless best_algorithem = best_algorithem(algorithems)
|
74
|
+
raise SecurityError, 'No supported signatures found'
|
75
|
+
end
|
76
|
+
|
77
|
+
algorithems_digest = best_algorithem[:digest]
|
78
|
+
signature = best_algorithem[:signature]
|
79
|
+
|
80
|
+
pkey = OpenSSL::PKey.read(public_key)
|
81
|
+
digest = OpenSSL::Digest.new(algorithems_digest)
|
82
|
+
verified = pkey.verify(digest, signature, signed_data.string)
|
83
|
+
raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
|
84
|
+
|
85
|
+
# verify algorithm ID full equal (and sort) between digests and signature
|
86
|
+
digests = length_prefix_block(signed_data)
|
87
|
+
content_digests = signed_data_digests(digests)
|
88
|
+
content_digest = content_digests[algorithems_digest]&.fetch(:content)
|
89
|
+
|
90
|
+
unless content_digest
|
91
|
+
raise SecurityError,
|
92
|
+
'Signature algorithms don\'t match between digests and signatures records'
|
93
|
+
end
|
94
|
+
|
95
|
+
previous_digest = content_digests.fetch(algorithems_digest)
|
96
|
+
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
|
+
|
102
|
+
certificates = length_prefix_block(signed_data)
|
103
|
+
certs = signed_data_certs(certificates)
|
104
|
+
raise SecurityError, 'No certificates listed' if certs.empty?
|
105
|
+
|
106
|
+
main_cert = certs[0]
|
107
|
+
if main_cert.public_key.to_der != pkey.to_der
|
108
|
+
raise SecurityError, 'Public key mismatch between certificate and signature record'
|
109
|
+
end
|
110
|
+
|
111
|
+
additional_attrs = length_prefix_block(signed_data)
|
112
|
+
verify_additional_attrs(additional_attrs, certs)
|
113
|
+
|
114
|
+
[certs, content_digests]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
register(Version::V2, V2)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo
|
4
|
+
class Android < File
|
5
|
+
module Signature
|
6
|
+
# Android v3 Signature
|
7
|
+
#
|
8
|
+
# FULL FORMAT:
|
9
|
+
# OFFSET DATA TYPE DESCRIPTION
|
10
|
+
# * @+0 bytes uint32: signer size in bytes
|
11
|
+
# * @+4 bytes payload signer block
|
12
|
+
# * @+0 bytes unit32: signed data size in bytes
|
13
|
+
# * @+4 bytes payload signed data block
|
14
|
+
# * @+0 bytes unit32: digests with size in bytes
|
15
|
+
# * @+0 bytes unit32: digests with size in bytes
|
16
|
+
# * @+W bytes unit32: minSDK
|
17
|
+
# * @+X+4 bytes unit32: maxSDK
|
18
|
+
# * @+Y+4 bytes unit32: signatures with size in bytes
|
19
|
+
# * @+Y+4 bytes payload signed data block
|
20
|
+
# * @+Z bytes unit32: public key with size in bytes
|
21
|
+
# * @+Z+4 bytes payload signed data block
|
22
|
+
class V3 < Base
|
23
|
+
include AppInfo::Helper::IOBlock
|
24
|
+
include AppInfo::Helper::Signatures
|
25
|
+
include AppInfo::Helper::Algorithm
|
26
|
+
|
27
|
+
# V3 Signature ID 0xf05368c0
|
28
|
+
V3_BLOCK_ID = [0xc0, 0x68, 0x53, 0xf0].freeze
|
29
|
+
|
30
|
+
# V3.1 Signature ID 0x1b93ad61
|
31
|
+
V3_1_BLOCK_ID = [0x61, 0xad, 0x93, 0x1b].freeze
|
32
|
+
|
33
|
+
attr_reader :certificates, :digests
|
34
|
+
|
35
|
+
def version
|
36
|
+
Version::V3
|
37
|
+
end
|
38
|
+
|
39
|
+
def verify
|
40
|
+
begin
|
41
|
+
signers_block = singers_block(V3_1_BLOCK_ID)
|
42
|
+
rescue NotFoundError
|
43
|
+
signers_block = singers_block(V3_BLOCK_ID)
|
44
|
+
end
|
45
|
+
|
46
|
+
@certificates, @digests = verified_certs(signers_block)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def verified_certs(signers_block)
|
52
|
+
unless (signers = length_prefix_block(signers_block))
|
53
|
+
raise SecurityError, 'Not found signers'
|
54
|
+
end
|
55
|
+
|
56
|
+
certificates = []
|
57
|
+
content_digests = {}
|
58
|
+
loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
|
59
|
+
signer_certs, signer_digests = extract_signer_data(signer)
|
60
|
+
certificates.concat(signer_certs)
|
61
|
+
content_digests.merge!(signer_digests)
|
62
|
+
end
|
63
|
+
raise SecurityError, 'No signers found' if certificates.empty?
|
64
|
+
|
65
|
+
[certificates, content_digests]
|
66
|
+
end
|
67
|
+
|
68
|
+
def extract_signer_data(signer)
|
69
|
+
# raw data
|
70
|
+
signed_data = length_prefix_block(signer)
|
71
|
+
|
72
|
+
# TODO: verify min_sdk and max_sdk
|
73
|
+
min_sdk = signer.read(UINT32_SIZE)
|
74
|
+
max_sdk = signer.read(UINT32_SIZE)
|
75
|
+
|
76
|
+
signatures = length_prefix_block(signer)
|
77
|
+
public_key = length_prefix_block(signer, raw: true)
|
78
|
+
|
79
|
+
algorithems = signature_algorithms(signatures)
|
80
|
+
raise SecurityError, 'No signatures found' if algorithems.empty?
|
81
|
+
|
82
|
+
# find best algorithem to verify signed data with public key and signature
|
83
|
+
unless best_algorithem = best_algorithem(algorithems)
|
84
|
+
raise SecurityError, 'No supported signatures found'
|
85
|
+
end
|
86
|
+
|
87
|
+
algorithems_digest = best_algorithem[:digest]
|
88
|
+
signature = best_algorithem[:signature]
|
89
|
+
|
90
|
+
pkey = OpenSSL::PKey.read(public_key)
|
91
|
+
digest = OpenSSL::Digest.new(algorithems_digest)
|
92
|
+
verified = pkey.verify(digest, signature, signed_data.string)
|
93
|
+
raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
|
94
|
+
|
95
|
+
# verify algorithm ID full equal (and sort) between digests and signature
|
96
|
+
digests = length_prefix_block(signed_data)
|
97
|
+
content_digests = signed_data_digests(digests)
|
98
|
+
content_digest = content_digests[algorithems_digest]&.fetch(:content)
|
99
|
+
|
100
|
+
unless content_digest
|
101
|
+
raise SecurityError,
|
102
|
+
'Signature algorithms don\'t match between digests and signatures records'
|
103
|
+
end
|
104
|
+
|
105
|
+
previous_digest = content_digests.fetch(algorithems_digest)
|
106
|
+
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
|
+
|
112
|
+
certificates = length_prefix_block(signed_data)
|
113
|
+
certs = signed_data_certs(certificates)
|
114
|
+
raise SecurityError, 'No certificates listed' if certs.empty?
|
115
|
+
|
116
|
+
main_cert = certs[0]
|
117
|
+
if main_cert.public_key.to_der != pkey.to_der
|
118
|
+
raise SecurityError, 'Public key mismatch between certificate and signature record'
|
119
|
+
end
|
120
|
+
|
121
|
+
additional_attrs = length_prefix_block(signed_data)
|
122
|
+
verify_additional_attrs(additional_attrs, certs)
|
123
|
+
|
124
|
+
[certs, content_digests]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
register(Version::V3, V3)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo
|
4
|
+
class Android < File
|
5
|
+
module Signature
|
6
|
+
# Android v4 Signature
|
7
|
+
#
|
8
|
+
# TODO: ApkSignatureSchemeV4Verifier.java
|
9
|
+
class V4 < Base
|
10
|
+
def version
|
11
|
+
Version::V4
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# register(Version::V4, V4)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_info/android/signature'
|
4
|
+
require 'image_size'
|
5
|
+
require 'forwardable'
|
6
|
+
|
7
|
+
module AppInfo
|
8
|
+
# Android base parser for apk and aab file
|
9
|
+
class Android < File
|
10
|
+
extend Forwardable
|
11
|
+
include Helper::HumanFileSize
|
12
|
+
|
13
|
+
# return file size
|
14
|
+
# @example Read file size in integer
|
15
|
+
# aab.size # => 3618865
|
16
|
+
#
|
17
|
+
# @example Read file size in human readabale
|
18
|
+
# aab.size(human_size: true) # => '3.45 MB'
|
19
|
+
#
|
20
|
+
# @param [Boolean] human_size Convert integer value to human readable.
|
21
|
+
# @return [Integer, String]
|
22
|
+
def size(human_size: false)
|
23
|
+
file_to_human_size(@file, human_size: human_size)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Symbol] {Manufacturer}
|
27
|
+
def manufacturer
|
28
|
+
Manufacturer::GOOGLE
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Symbol] {Platform}
|
32
|
+
def platform
|
33
|
+
Platform::ANDROID
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Symbol] {Device}
|
37
|
+
def device
|
38
|
+
if watch?
|
39
|
+
Device::WATCH
|
40
|
+
elsif television?
|
41
|
+
Device::TELEVISION
|
42
|
+
elsif automotive?
|
43
|
+
Device::AUTOMOTIVE
|
44
|
+
elsif tablet?
|
45
|
+
Device::TABLET
|
46
|
+
else
|
47
|
+
Device::PHONE
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# @abstract Subclass and override {#name} to implement.
|
52
|
+
def name
|
53
|
+
not_implemented_error!(__method__)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @todo find a way to detect, no way!
|
57
|
+
# @see https://stackoverflow.com/questions/9279111/determine-if-the-device-is-a-smartphone-or-tablet
|
58
|
+
# @return [Boolean] false always false
|
59
|
+
def tablet?
|
60
|
+
# Not works!
|
61
|
+
# resource.first_package
|
62
|
+
# .entries('bool')
|
63
|
+
# .select{|e| e.name == 'isTablet' }
|
64
|
+
# .size >= 1
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Boolean]
|
69
|
+
def watch?
|
70
|
+
!!use_features&.include?('android.hardware.type.watch')
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Boolean]
|
74
|
+
def television?
|
75
|
+
!!use_features&.include?('android.software.leanback')
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Boolean]
|
79
|
+
def automotive?
|
80
|
+
!!use_features&.include?('android.hardware.type.automotive')
|
81
|
+
end
|
82
|
+
|
83
|
+
# @abstract Subclass and override {#use_features} to implement.
|
84
|
+
def use_features
|
85
|
+
not_implemented_error!(__method__)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @abstract Subclass and override {#use_permissions} to implement.
|
89
|
+
def use_permissions
|
90
|
+
not_implemented_error!(__method__)
|
91
|
+
end
|
92
|
+
|
93
|
+
# @abstract Subclass and override {#use_permissions} to implement.
|
94
|
+
def activities
|
95
|
+
not_implemented_error!(__method__)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @abstract Subclass and override {#use_permissions} to implement.
|
99
|
+
def services
|
100
|
+
not_implemented_error!(__method__)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @abstract Subclass and override {#use_permissions} to implement.
|
104
|
+
def components
|
105
|
+
not_implemented_error!(__method__)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Return multi version certifiates of signatures
|
109
|
+
# @return [Array<Hash>] signatures
|
110
|
+
# @see AppInfo::Android::Signature.verify
|
111
|
+
def signatures
|
112
|
+
@signatures ||= Android::Signature.verify(self)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Legacy v1 scheme signatures, it will remove soon.
|
116
|
+
# @deprecated Use {#signatures}
|
117
|
+
# @return [Array<OpenSSL::PKCS7, nil>] signatures
|
118
|
+
def signs
|
119
|
+
@signs ||= v1sign&.signatures || []
|
120
|
+
end
|
121
|
+
|
122
|
+
# Legacy v1 scheme certificates, it will remove soon.
|
123
|
+
# @deprecated Use {#signatures}
|
124
|
+
# @return [Array<OpenSSL::PKCS7, nil>] certificates
|
125
|
+
def certificates
|
126
|
+
@certificates ||= v1sign&.certificates || []
|
127
|
+
end
|
128
|
+
|
129
|
+
# @abstract Subclass and override {#manifest} to implement.
|
130
|
+
def manifest
|
131
|
+
not_implemented_error!(__method__)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @abstract Subclass and override {#resource} to implement.
|
135
|
+
def resource
|
136
|
+
not_implemented_error!(__method__)
|
137
|
+
end
|
138
|
+
|
139
|
+
# @abstract Subclass and override {#zip} to implement.
|
140
|
+
def zip
|
141
|
+
not_implemented_error!(__method__)
|
142
|
+
end
|
143
|
+
|
144
|
+
# @abstract Subclass and override {#clear!} to implement.
|
145
|
+
def clear!
|
146
|
+
not_implemented_error!(__method__)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [String] contents path of contents
|
150
|
+
def contents
|
151
|
+
@contents ||= ::File.join(Dir.mktmpdir, "AppInfo-android-#{SecureRandom.hex}")
|
152
|
+
end
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
def extract_icon(icons, exclude: nil)
|
157
|
+
excludes = exclude_icon_exts(exclude: exclude)
|
158
|
+
icons.reject { |icon| icon_ext_match?(icon[:name], excludes) }
|
159
|
+
end
|
160
|
+
|
161
|
+
def exclude_icon_exts(exclude:)
|
162
|
+
case exclude
|
163
|
+
when String then [exclude]
|
164
|
+
when Array then exclude.map(&:to_s)
|
165
|
+
when Symbol then [exclude.to_s]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def icon_ext_match?(file, excludes)
|
170
|
+
return false if file.nil? || excludes.nil?
|
171
|
+
|
172
|
+
excludes.include?(::File.extname(file)[1..-1])
|
173
|
+
end
|
174
|
+
|
175
|
+
def v1sign
|
176
|
+
@v1sign ||= Android::Signature::V1.verify(self)
|
177
|
+
rescue Android::Signature::NotFoundError
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/lib/app_info/apk.rb
CHANGED
@@ -1,41 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'ruby_apk'
|
4
|
-
require 'image_size'
|
5
|
-
require 'forwardable'
|
6
4
|
|
7
5
|
module AppInfo
|
8
|
-
# Parse APK file
|
9
|
-
class APK
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
#
|
16
|
-
module Device
|
17
|
-
PHONE = 'Phone'
|
18
|
-
TABLET = 'Tablet'
|
19
|
-
WATCH = 'Watch'
|
20
|
-
TV = 'Television'
|
21
|
-
AUTOMOTIVE = 'Automotive'
|
22
|
-
end
|
23
|
-
|
24
|
-
def initialize(file)
|
25
|
-
@file = file
|
26
|
-
end
|
27
|
-
|
28
|
-
def size(human_size: false)
|
29
|
-
file_to_human_size(@file, human_size: human_size)
|
30
|
-
end
|
31
|
-
|
32
|
-
def os
|
33
|
-
Platform::ANDROID
|
34
|
-
end
|
35
|
-
alias file_type os
|
36
|
-
|
6
|
+
# Parse APK file parser, wrapper for {https://github.com/icyleaf/android_parser android_parser}.
|
7
|
+
class APK < Android
|
8
|
+
# @!method manifest
|
9
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Apk#manifest-instance_method ::Android::Apk#manifest
|
10
|
+
# @!method resource
|
11
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Apk#resource-instance_method ::Android::Apk#resource
|
12
|
+
# @!method dex
|
13
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Apk#dex-instance_method ::Android::Apk#dex
|
37
14
|
def_delegators :apk, :manifest, :resource, :dex
|
38
15
|
|
16
|
+
# @!method version_name
|
17
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#version_name-instance_method ::Android::Manifest#version_name
|
18
|
+
# @!method package_name
|
19
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#package_name-instance_method ::Android::Manifest#package_name
|
20
|
+
# @!method target_sdk_versionx
|
21
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#target_sdk_versionx-instance_method ::Android::Manifest#target_sdk_version
|
22
|
+
# @!method components
|
23
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#components-instance_method ::Android::Manifest#components
|
24
|
+
# @!method services
|
25
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#services-instance_method ::Android::Manifest#services
|
26
|
+
# @!method use_permissions
|
27
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#use_permissions-instance_method ::Android::Manifest#use_permissions
|
28
|
+
# @!method use_features
|
29
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#use_features-instance_method ::Android::Manifest#use_features
|
30
|
+
# @!method deep_links
|
31
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#deep_links-instance_method ::Android::Manifest#deep_links
|
32
|
+
# @!method schemes
|
33
|
+
# @see https://rubydoc.info/gems/android_parser/Android/Manifest#schemes-instance_method ::Android::Manifest#schemes
|
39
34
|
def_delegators :manifest, :version_name, :package_name, :target_sdk_version,
|
40
35
|
:components, :services, :use_permissions, :use_features,
|
41
36
|
:deep_links, :schemes
|
@@ -53,78 +48,70 @@ module AppInfo
|
|
53
48
|
manifest.label || resource.find('@string/app_name')
|
54
49
|
end
|
55
50
|
|
56
|
-
|
57
|
-
if wear?
|
58
|
-
Device::WATCH
|
59
|
-
elsif tv?
|
60
|
-
Device::TV
|
61
|
-
elsif automotive?
|
62
|
-
Device::AUTOMOTIVE
|
63
|
-
else
|
64
|
-
Device::PHONE
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# TODO: find a way to detect, no way!
|
69
|
-
# def tablet?
|
70
|
-
# end
|
71
|
-
|
72
|
-
def wear?
|
73
|
-
use_features.include?('android.hardware.type.watch')
|
74
|
-
end
|
75
|
-
|
76
|
-
def tv?
|
77
|
-
use_features.include?('android.software.leanback')
|
78
|
-
end
|
79
|
-
|
80
|
-
def automotive?
|
81
|
-
use_features.include?('android.hardware.type.automotive')
|
82
|
-
end
|
83
|
-
|
51
|
+
# @return [String]
|
84
52
|
def min_sdk_version
|
85
53
|
manifest.min_sdk_ver
|
86
54
|
end
|
87
55
|
alias min_os_version min_sdk_version
|
88
56
|
|
89
|
-
|
90
|
-
return 'v1' unless signs.empty?
|
91
|
-
|
92
|
-
# when ?
|
93
|
-
# https://source.android.com/security/apksigning/v2?hl=zh-cn
|
94
|
-
# 'v2'
|
95
|
-
# when ?
|
96
|
-
# https://source.android.com/security/apksigning/v3?hl=zh-cn
|
97
|
-
# 'v3'
|
98
|
-
'unknown'
|
99
|
-
end
|
100
|
-
|
101
|
-
def signs
|
102
|
-
apk.signs.each_with_object([]) do |(path, sign), obj|
|
103
|
-
obj << Sign.new(path, sign)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def certificates
|
108
|
-
apk.certificates.each_with_object([]) do |(path, certificate), obj|
|
109
|
-
obj << Certificate.new(path, certificate)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
57
|
+
# @return [String]
|
113
58
|
def activities
|
114
59
|
components.select { |c| c.type == 'activity' }
|
115
60
|
end
|
116
61
|
|
62
|
+
# @return [::Android::Apk]
|
117
63
|
def apk
|
118
64
|
@apk ||= ::Android::Apk.new(@file)
|
119
65
|
end
|
120
66
|
|
121
|
-
|
67
|
+
# @return [Zip::File]
|
68
|
+
def zip
|
69
|
+
@zip ||= apk.instance_variable_get(:@zip)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Full icons metadata
|
73
|
+
# @example full icons
|
74
|
+
# apk.icons
|
75
|
+
# # => [
|
76
|
+
# # {
|
77
|
+
# # name: 'ic_launcher.png',
|
78
|
+
# # file: '/path/to/ic_launcher.png',
|
79
|
+
# # dimensions: [29, 29]
|
80
|
+
# # },
|
81
|
+
# # {
|
82
|
+
# # name: 'ic_launcher.png',
|
83
|
+
# # file: '/path/to/ic_launcher.png',
|
84
|
+
# # dimensions: [120, 120]
|
85
|
+
# # },
|
86
|
+
# # {
|
87
|
+
# # name: 'ic_launcher.xml',
|
88
|
+
# # file: '/path/to/ic_launcher.xml',
|
89
|
+
# # dimensions: [nil, nil]
|
90
|
+
# # },
|
91
|
+
# # ]
|
92
|
+
# @example exclude xml icons
|
93
|
+
# apk.icons(exclude: :xml)
|
94
|
+
# # => [
|
95
|
+
# # {
|
96
|
+
# # name: 'ic_launcher.png',
|
97
|
+
# # file: '/path/to/ic_launcher.png',
|
98
|
+
# # dimensions: [29, 29]
|
99
|
+
# # },
|
100
|
+
# # {
|
101
|
+
# # name: 'ic_launcher.png',
|
102
|
+
# # file: '/path/to/ic_launcher.png',
|
103
|
+
# # dimensions: [120, 120]
|
104
|
+
# # }
|
105
|
+
# # ]
|
106
|
+
# @param [Boolean] xml return xml icons
|
107
|
+
# @return [Array<Hash{Symbol => String, Array<Integer>}>] icons paths of icons
|
108
|
+
def icons(exclude: nil)
|
122
109
|
@icons ||= apk.icon.each_with_object([]) do |(path, data), obj|
|
123
|
-
icon_name = File.basename(path)
|
124
|
-
icon_path = File.join(contents, File.dirname(path))
|
125
|
-
icon_file = File.join(icon_path, icon_name)
|
110
|
+
icon_name = ::File.basename(path)
|
111
|
+
icon_path = ::File.join(contents, ::File.dirname(path))
|
112
|
+
icon_file = ::File.join(icon_path, icon_name)
|
126
113
|
FileUtils.mkdir_p icon_path
|
127
|
-
File.write(icon_file, data, encoding: Encoding::BINARY)
|
114
|
+
::File.write(icon_file, data, encoding: Encoding::BINARY)
|
128
115
|
|
129
116
|
obj << {
|
130
117
|
name: icon_name,
|
@@ -132,6 +119,8 @@ module AppInfo
|
|
132
119
|
dimensions: ImageSize.path(icon_file).size
|
133
120
|
}
|
134
121
|
end
|
122
|
+
|
123
|
+
extract_icon(@icons, exclude: exclude)
|
135
124
|
end
|
136
125
|
|
137
126
|
def clear!
|
@@ -145,29 +134,5 @@ module AppInfo
|
|
145
134
|
@app_path = nil
|
146
135
|
@info = nil
|
147
136
|
end
|
148
|
-
|
149
|
-
def contents
|
150
|
-
@contents ||= File.join(Dir.mktmpdir, "AppInfo-android-#{SecureRandom.hex}")
|
151
|
-
end
|
152
|
-
|
153
|
-
# Android Certificate
|
154
|
-
class Certificate
|
155
|
-
attr_reader :path, :certificate
|
156
|
-
|
157
|
-
def initialize(path, certificate)
|
158
|
-
@path = path
|
159
|
-
@certificate = certificate
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# Android Sign
|
164
|
-
class Sign
|
165
|
-
attr_reader :path, :sign
|
166
|
-
|
167
|
-
def initialize(path, sign)
|
168
|
-
@path = path
|
169
|
-
@sign = sign
|
170
|
-
end
|
171
|
-
end
|
172
137
|
end
|
173
138
|
end
|