app-info 2.8.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|