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
data/lib/app_info/aab.rb
CHANGED
@@ -1,156 +1,85 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'app_info/protobuf/manifest'
|
4
|
-
require 'image_size'
|
5
|
-
require 'forwardable'
|
6
4
|
|
7
5
|
module AppInfo
|
8
|
-
# Parse APK file
|
9
|
-
class AAB
|
10
|
-
include Helper::HumanFileSize
|
11
|
-
extend Forwardable
|
12
|
-
|
13
|
-
attr_reader :file
|
14
|
-
|
15
|
-
# APK Devices
|
16
|
-
module Device
|
17
|
-
PHONE = 'Phone'
|
18
|
-
TABLET = 'Tablet'
|
19
|
-
WATCH = 'Watch'
|
20
|
-
TV = 'Television'
|
21
|
-
AUTOMOTIVE = 'Automotive'
|
22
|
-
end
|
23
|
-
|
6
|
+
# Parse APK file parser
|
7
|
+
class AAB < Android
|
24
8
|
BASE_PATH = 'base'
|
25
9
|
BASE_MANIFEST = "#{BASE_PATH}/manifest/AndroidManifest.xml"
|
26
10
|
BASE_RESOURCES = "#{BASE_PATH}/resources.pb"
|
27
11
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
Platform::ANDROID
|
38
|
-
end
|
39
|
-
alias file_type os
|
40
|
-
|
12
|
+
# @!method version_name
|
13
|
+
# @see Protobuf::Manifest#version_name
|
14
|
+
# @return [String]
|
15
|
+
# @!method deep_links
|
16
|
+
# @see Protobuf::Manifest#deep_links
|
17
|
+
# @return [String]
|
18
|
+
# @!method schemes
|
19
|
+
# @see Protobuf::Manifest#schemes
|
20
|
+
# @return [String]
|
41
21
|
def_delegators :manifest, :version_name, :deep_links, :schemes
|
42
22
|
|
43
23
|
alias release_version version_name
|
44
24
|
|
25
|
+
# @return [String]
|
45
26
|
def package_name
|
46
27
|
manifest.package
|
47
28
|
end
|
48
29
|
alias identifier package_name
|
49
30
|
alias bundle_id package_name
|
50
31
|
|
32
|
+
# @return [String]
|
51
33
|
def version_code
|
52
34
|
manifest.version_code.to_s
|
53
35
|
end
|
54
36
|
alias build_version version_code
|
55
37
|
|
38
|
+
# @return [String]
|
56
39
|
def name
|
57
40
|
manifest.label
|
58
41
|
end
|
59
42
|
|
60
|
-
|
61
|
-
if wear?
|
62
|
-
Device::WATCH
|
63
|
-
elsif tv?
|
64
|
-
Device::TV
|
65
|
-
elsif automotive?
|
66
|
-
Device::AUTOMOTIVE
|
67
|
-
else
|
68
|
-
Device::PHONE
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# TODO: find a way to detect
|
73
|
-
# Found answer but not works: https://stackoverflow.com/questions/9279111/determine-if-the-device-is-a-smartphone-or-tablet
|
74
|
-
# def tablet?
|
75
|
-
# resource.first_package
|
76
|
-
# .entries('bool')
|
77
|
-
# .select{|e| e.name == 'isTablet' }
|
78
|
-
# .size >= 1
|
79
|
-
# end
|
80
|
-
|
81
|
-
def wear?
|
82
|
-
use_features.include?('android.hardware.type.watch')
|
83
|
-
end
|
84
|
-
|
85
|
-
def tv?
|
86
|
-
use_features.include?('android.software.leanback')
|
87
|
-
end
|
88
|
-
|
89
|
-
def automotive?
|
90
|
-
use_features.include?('android.hardware.type.automotive')
|
91
|
-
end
|
92
|
-
|
43
|
+
# @return [String]
|
93
44
|
def min_sdk_version
|
94
45
|
manifest.uses_sdk.min_sdk_version
|
95
46
|
end
|
96
47
|
alias min_os_version min_sdk_version
|
97
48
|
|
49
|
+
# @return [String]
|
98
50
|
def target_sdk_version
|
99
51
|
manifest.uses_sdk.target_sdk_version
|
100
52
|
end
|
101
53
|
|
54
|
+
# @return [Array<String>]
|
102
55
|
def use_features
|
56
|
+
return [] unless manifest.respond_to?(:uses_feature)
|
57
|
+
|
103
58
|
@use_features ||= manifest&.uses_feature&.map(&:name)
|
104
59
|
end
|
105
60
|
|
61
|
+
# @return [Array<String>]
|
106
62
|
def use_permissions
|
63
|
+
return [] unless manifest.respond_to?(:uses_permission)
|
64
|
+
|
107
65
|
@use_permissions ||= manifest&.uses_permission&.map(&:name)
|
108
66
|
end
|
109
67
|
|
68
|
+
# @return [Protobuf::Node]
|
110
69
|
def activities
|
111
70
|
@activities ||= manifest.activities
|
112
71
|
end
|
113
72
|
|
73
|
+
# @return [Protobuf::Node]
|
114
74
|
def services
|
115
75
|
@services ||= manifest.services
|
116
76
|
end
|
117
77
|
|
78
|
+
# @return [Protobuf::Node]
|
118
79
|
def components
|
119
80
|
@components ||= manifest.components.transform_values
|
120
81
|
end
|
121
82
|
|
122
|
-
def sign_version
|
123
|
-
return 'v1' unless signs.empty?
|
124
|
-
|
125
|
-
# when ?
|
126
|
-
# https://source.android.com/security/apksigning/v2?hl=zh-cn
|
127
|
-
# 'v2'
|
128
|
-
# when ?
|
129
|
-
# https://source.android.com/security/apksigning/v3?hl=zh-cn
|
130
|
-
# 'v3'
|
131
|
-
'unknown'
|
132
|
-
end
|
133
|
-
|
134
|
-
def signs
|
135
|
-
return @signs if @signs
|
136
|
-
|
137
|
-
@signs = []
|
138
|
-
each_file do |path, data|
|
139
|
-
# find META-INF/xxx.{RSA|DSA}
|
140
|
-
next unless path =~ %r{^META-INF/} && data.unpack('CC') == [0x30, 0x82]
|
141
|
-
|
142
|
-
@signs << APK::Sign.new(path, OpenSSL::PKCS7.new(data))
|
143
|
-
end
|
144
|
-
|
145
|
-
@signs
|
146
|
-
end
|
147
|
-
|
148
|
-
def certificates
|
149
|
-
@certificates ||= signs.each_with_object([]) do |sign, obj|
|
150
|
-
obj << APK::Certificate.new(sign.path, sign.sign.certificates[0])
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
83
|
def each_file
|
155
84
|
zip.each do |entry|
|
156
85
|
next unless entry.file?
|
@@ -167,36 +96,70 @@ module AppInfo
|
|
167
96
|
end
|
168
97
|
|
169
98
|
def entry(name, base_path: BASE_PATH)
|
170
|
-
entry = @zip.find_entry(File.join(base_path, name))
|
99
|
+
entry = @zip.find_entry(::File.join(base_path, name))
|
171
100
|
raise NotFoundError, "'#{name}'" if entry.nil?
|
172
101
|
|
173
102
|
entry
|
174
103
|
end
|
175
104
|
|
105
|
+
# @return [Protobuf::Manifest]
|
176
106
|
def manifest
|
177
107
|
io = zip.read(zip.find_entry(BASE_MANIFEST))
|
178
108
|
@manifest ||= Protobuf::Manifest.parse(io, resource)
|
179
109
|
end
|
180
110
|
|
111
|
+
# @return [Protobuf::Resources]
|
181
112
|
def resource
|
182
113
|
io = zip.read(zip.find_entry(BASE_RESOURCES))
|
183
114
|
@resource ||= Protobuf::Resources.parse(io)
|
184
115
|
end
|
185
116
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
117
|
+
# Full icons metadata
|
118
|
+
# @example full icons
|
119
|
+
# aab.icons
|
120
|
+
# # => [
|
121
|
+
# # {
|
122
|
+
# # name: 'ic_launcher.png',
|
123
|
+
# # file: '/path/to/ic_launcher.webp',
|
124
|
+
# # dimensions: [29, 29]
|
125
|
+
# # },
|
126
|
+
# # {
|
127
|
+
# # name: 'ic_launcher.png',
|
128
|
+
# # file: '/path/to/ic_launcher.png',
|
129
|
+
# # dimensions: [120, 120]
|
130
|
+
# # },
|
131
|
+
# # {
|
132
|
+
# # name: 'ic_launcher.xml',
|
133
|
+
# # file: '/path/to/ic_launcher.xml',
|
134
|
+
# # dimensions: [nil, nil]
|
135
|
+
# # },
|
136
|
+
# # ]
|
137
|
+
# @example exclude xml icons
|
138
|
+
# aab.icons(filter: :xml)
|
139
|
+
# # => [
|
140
|
+
# # {
|
141
|
+
# # name: 'ic_launcher.png',
|
142
|
+
# # file: '/path/to/ic_launcher.webp',
|
143
|
+
# # dimensions: [29, 29]
|
144
|
+
# # },
|
145
|
+
# # {
|
146
|
+
# # name: 'ic_launcher.png',
|
147
|
+
# # file: '/path/to/ic_launcher.png',
|
148
|
+
# # dimensions: [120, 120]
|
149
|
+
# # }
|
150
|
+
# # ]
|
151
|
+
# @param [String, Symbol, Array<Symbol, Array>] filter filter file extension name
|
152
|
+
# @return [Array<Hash{Symbol => String, Array<Integer>}>] icons paths of icons
|
153
|
+
def icons(exclude: nil)
|
191
154
|
@icons ||= manifest.icons.each_with_object([]) do |res, obj|
|
192
155
|
path = res.value
|
193
|
-
filename = File.basename(path)
|
194
|
-
filepath = File.join(contents, File.dirname(path))
|
195
|
-
file = File.join(filepath, filename)
|
196
|
-
FileUtils.mkdir_p
|
156
|
+
filename = ::File.basename(path)
|
157
|
+
filepath = ::File.join(contents, ::File.dirname(path))
|
158
|
+
file = ::File.join(filepath, filename)
|
159
|
+
FileUtils.mkdir_p(filepath)
|
197
160
|
|
198
161
|
binary_data = read_file(path)
|
199
|
-
File.write(file, binary_data, encoding: Encoding::BINARY)
|
162
|
+
::File.write(file, binary_data, encoding: Encoding::BINARY)
|
200
163
|
|
201
164
|
obj << {
|
202
165
|
name: filename,
|
@@ -204,6 +167,8 @@ module AppInfo
|
|
204
167
|
dimensions: ImageSize.path(file).size
|
205
168
|
}
|
206
169
|
end
|
170
|
+
|
171
|
+
extract_icon(@icons, exclude: exclude)
|
207
172
|
end
|
208
173
|
|
209
174
|
def clear!
|
@@ -218,14 +183,15 @@ module AppInfo
|
|
218
183
|
@info = nil
|
219
184
|
end
|
220
185
|
|
221
|
-
|
222
|
-
|
186
|
+
# @return [Zip::File]
|
187
|
+
def zip
|
188
|
+
@zip ||= Zip::File.open(@file)
|
223
189
|
end
|
224
190
|
|
225
191
|
private
|
226
192
|
|
227
193
|
def xml_file?(file)
|
228
|
-
File.extname(file) == '.xml'
|
194
|
+
::File.extname(file) == '.xml'
|
229
195
|
end
|
230
196
|
|
231
197
|
# TODO: how to convert xml content after decode protoubufed content
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo
|
4
|
+
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
|
+
module Signature
|
13
|
+
class VersionError < Error; end
|
14
|
+
class SecurityError < Error; end
|
15
|
+
class NotFoundError < NotFoundError; end
|
16
|
+
|
17
|
+
module Version
|
18
|
+
V1 = 1
|
19
|
+
V2 = 2
|
20
|
+
V3 = 3
|
21
|
+
V4 = 4
|
22
|
+
end
|
23
|
+
|
24
|
+
# All registerd verions to verify
|
25
|
+
#
|
26
|
+
# key is the version
|
27
|
+
# value is the class
|
28
|
+
@versions = {}
|
29
|
+
|
30
|
+
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)
|
59
|
+
min_version = min_version.to_i if min_version.is_a?(String)
|
60
|
+
if min_version && min_version > Version::V4
|
61
|
+
raise VersionError,
|
62
|
+
"No signature found in #{min_version} scheme or newer for android file"
|
63
|
+
end
|
64
|
+
|
65
|
+
if min_version.zero?
|
66
|
+
raise VersionError,
|
67
|
+
"Unkonwn version: #{min_version}, avaiables in 1/2/3 and 4 (no implantation yet)"
|
68
|
+
end
|
69
|
+
|
70
|
+
# try full version signatures if min_version is nil
|
71
|
+
versions = min_version.downto(Version::V1).each_with_object([]) do |version, signatures|
|
72
|
+
next unless kclass = fetch(version)
|
73
|
+
|
74
|
+
data = { version: version }
|
75
|
+
begin
|
76
|
+
verifier = kclass.verify(parser)
|
77
|
+
data[:verified] = verifier.verified
|
78
|
+
data[:certificates] = verifier.certificates
|
79
|
+
data[:verifier] = verifier
|
80
|
+
rescue SecurityError, NotFoundError
|
81
|
+
# not this version, try the low version
|
82
|
+
ensure
|
83
|
+
signatures << data
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
versions.sort_by { |entry| entry[:version] }
|
88
|
+
end
|
89
|
+
|
90
|
+
def registered
|
91
|
+
@versions.keys
|
92
|
+
end
|
93
|
+
|
94
|
+
def register(version, verifier)
|
95
|
+
@versions[version] = verifier
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch(version)
|
99
|
+
@versions[version]
|
100
|
+
end
|
101
|
+
|
102
|
+
def exist?(version)
|
103
|
+
@versions.key?(version)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
UINT32_MAX_VALUE = 2_147_483_647
|
108
|
+
UINT32_SIZE = 4
|
109
|
+
UINT64_SIZE = 8
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
require 'app_info/android/signatures/base'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'app_info/android/signatures/info'
|
4
|
+
|
5
|
+
module AppInfo
|
6
|
+
class Android < File
|
7
|
+
module Signature
|
8
|
+
class Base
|
9
|
+
def self.verify(parser)
|
10
|
+
instance = new(parser)
|
11
|
+
instance.verify
|
12
|
+
instance
|
13
|
+
end
|
14
|
+
|
15
|
+
DESCRIPTION = 'APK Signature Scheme'
|
16
|
+
|
17
|
+
attr_reader :verified
|
18
|
+
|
19
|
+
def initialize(parser)
|
20
|
+
@parser = parser
|
21
|
+
@verified = false
|
22
|
+
end
|
23
|
+
|
24
|
+
# @abstract Subclass and override {#verify} to implement
|
25
|
+
def verify
|
26
|
+
raise NotImplementedError, ".#{__method__} method implantation required in #{self.class}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @abstract Subclass and override {#certificates} to implement
|
30
|
+
def certificates
|
31
|
+
raise NotImplementedError, ".#{__method__} method implantation required in #{self.class}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def scheme
|
35
|
+
"v#{version}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def description
|
39
|
+
"#{DESCRIPTION} #{scheme}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def logger
|
43
|
+
@parser.logger
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
require 'app_info/android/signatures/v1'
|
51
|
+
require 'app_info/android/signatures/v2'
|
52
|
+
require 'app_info/android/signatures/v3'
|
53
|
+
require 'app_info/android/signatures/v4'
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module AppInfo
|
7
|
+
class Android < File
|
8
|
+
module Signature
|
9
|
+
# APK signature scheme signurate info
|
10
|
+
#
|
11
|
+
# FORMAT:
|
12
|
+
# OFFSET DATA TYPE DESCRIPTION
|
13
|
+
# * @+0 bytes uint64: size in bytes (excluding this field)
|
14
|
+
# * @+8 bytes payload
|
15
|
+
# * @-24 bytes uint64: size in bytes (same as the one above)
|
16
|
+
# * @-16 bytes uint128: magic value
|
17
|
+
class Info
|
18
|
+
include AppInfo::Helper::IOBlock
|
19
|
+
|
20
|
+
# Signature block information
|
21
|
+
SIG_SIZE_OF_BLOCK_SIZE = 8
|
22
|
+
SIG_MAGIC_BLOCK_SIZE = 16
|
23
|
+
SIG_BLOCK_MIN_SIZE = 32
|
24
|
+
|
25
|
+
# Magic value: APK Sig Block 42
|
26
|
+
SIG_MAGIC = [
|
27
|
+
0x41, 0x50, 0x4b, 0x20, 0x53, 0x69,
|
28
|
+
0x67, 0x20, 0x42, 0x6c, 0x6f, 0x63,
|
29
|
+
0x6b, 0x20, 0x34, 0x32
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
attr_reader :total_size, :pairs, :magic, :logger
|
33
|
+
|
34
|
+
def initialize(version, parser, logger)
|
35
|
+
@version = version
|
36
|
+
@parser = parser
|
37
|
+
@logger = logger
|
38
|
+
|
39
|
+
pares_signatures_pairs
|
40
|
+
end
|
41
|
+
|
42
|
+
# Find singers
|
43
|
+
#
|
44
|
+
# FORMAT:
|
45
|
+
# OFFSET DATA TYPE DESCRIPTION
|
46
|
+
# * @+0 bytes uint64: size in bytes
|
47
|
+
# * @+8 bytes payload block
|
48
|
+
# * @+0 bytes uint32: id
|
49
|
+
# * @+4 bytes payload: value
|
50
|
+
def signers(block_id)
|
51
|
+
count = 0
|
52
|
+
until @pairs.eof?
|
53
|
+
left_bytes = left_bytes_check(
|
54
|
+
@pairs, UINT64_SIZE, NotFoundError,
|
55
|
+
"Insufficient data to read size of APK Signing Block ##{count}"
|
56
|
+
)
|
57
|
+
|
58
|
+
pair_buf = @pairs.read(UINT64_SIZE)
|
59
|
+
pair_size = pair_buf.unpack1('Q')
|
60
|
+
if pair_size < UINT32_SIZE || pair_size > UINT32_MAX_VALUE
|
61
|
+
raise NotFoundError,
|
62
|
+
"APK Signing Block ##{count} size out of range: #{pair_size} > #{UINT32_MAX_VALUE}"
|
63
|
+
end
|
64
|
+
|
65
|
+
if pair_size > left_bytes
|
66
|
+
raise NotFoundError,
|
67
|
+
"APK Signing Block ##{count} size out of range: #{pair_size} > #{left_bytes}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# fetch next signer block position
|
71
|
+
next_pos = @pairs.pos + pair_size.to_i
|
72
|
+
|
73
|
+
id_block = @pairs.read(UINT32_SIZE)
|
74
|
+
id_bytes = id_block.unpack('C*')
|
75
|
+
if id_bytes == block_id
|
76
|
+
logger.debug "Signature block id v#{@version} scheme (0x#{id_block.unpack1('H*')}) found"
|
77
|
+
value = @pairs.read(pair_size - UINT32_SIZE)
|
78
|
+
return StringIO.new(value)
|
79
|
+
end
|
80
|
+
|
81
|
+
@pairs.seek(next_pos)
|
82
|
+
count += 1
|
83
|
+
end
|
84
|
+
|
85
|
+
block_id_hex = block_id.reverse.pack('C*').unpack1('H*')
|
86
|
+
raise NotFoundError, "Not found block id 0x#{block_id_hex} in APK Signing Block."
|
87
|
+
end
|
88
|
+
|
89
|
+
def zip64?
|
90
|
+
zip_io.zip64_file?(start_buffer)
|
91
|
+
end
|
92
|
+
|
93
|
+
def pares_signatures_pairs
|
94
|
+
block = signature_block
|
95
|
+
block.rewind
|
96
|
+
# get pairs size
|
97
|
+
@total_size = block.size - (SIG_SIZE_OF_BLOCK_SIZE + SIG_MAGIC_BLOCK_SIZE)
|
98
|
+
|
99
|
+
# get pairs block
|
100
|
+
@pairs = StringIO.new(block.read(@total_size))
|
101
|
+
|
102
|
+
# get magic value
|
103
|
+
block.seek(block.pos + SIG_SIZE_OF_BLOCK_SIZE)
|
104
|
+
@magic = block.read(SIG_MAGIC_BLOCK_SIZE)
|
105
|
+
end
|
106
|
+
|
107
|
+
def signature_block
|
108
|
+
@signature_block ||= lambda {
|
109
|
+
logger.debug "cdir_offset: #{cdir_offset}"
|
110
|
+
|
111
|
+
file_io.seek(cdir_offset - (Info::SIG_MAGIC_BLOCK_SIZE + Info::SIG_SIZE_OF_BLOCK_SIZE))
|
112
|
+
footer_block = file_io.read(Info::SIG_SIZE_OF_BLOCK_SIZE)
|
113
|
+
if footer_block.size < Info::SIG_SIZE_OF_BLOCK_SIZE
|
114
|
+
raise NotFoundError, "APK Signing Block size out of range: #{footer_block.size}"
|
115
|
+
end
|
116
|
+
|
117
|
+
footer = footer_block.unpack1('Q')
|
118
|
+
total_size = footer
|
119
|
+
offset = cdir_offset - total_size - Info::SIG_SIZE_OF_BLOCK_SIZE
|
120
|
+
if offset.negative?
|
121
|
+
raise NotFoundError, "APK Signing Block offset out of range: #{offset}"
|
122
|
+
end
|
123
|
+
|
124
|
+
file_io.seek(offset)
|
125
|
+
header = file_io.read(Info::SIG_SIZE_OF_BLOCK_SIZE).unpack1('Q')
|
126
|
+
|
127
|
+
if header != footer
|
128
|
+
raise NotFoundError,
|
129
|
+
"APK Signing Block header and footer mismatch: #{header} != #{footer}"
|
130
|
+
end
|
131
|
+
|
132
|
+
io = file_io.read(total_size)
|
133
|
+
StringIO.new(io)
|
134
|
+
}.call
|
135
|
+
end
|
136
|
+
|
137
|
+
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
|
150
|
+
end
|
151
|
+
|
152
|
+
def file_io
|
153
|
+
@file_io ||= ::File.open(@parser.file, 'rb')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AppInfo
|
4
|
+
class Android < File
|
5
|
+
module Signature
|
6
|
+
# Android v1 Signature
|
7
|
+
class V1 < Base
|
8
|
+
DESCRIPTION = 'JAR signing'
|
9
|
+
|
10
|
+
PKCS7_HEADER = [0x30, 0x82].freeze
|
11
|
+
|
12
|
+
attr_reader :certificates, :signatures
|
13
|
+
|
14
|
+
def version
|
15
|
+
Version::V1
|
16
|
+
end
|
17
|
+
|
18
|
+
def description
|
19
|
+
DESCRIPTION
|
20
|
+
end
|
21
|
+
|
22
|
+
def verify
|
23
|
+
@signatures = fetch_signatures
|
24
|
+
@certificates = fetch_certificates
|
25
|
+
|
26
|
+
raise NotFoundError, 'Not found certificates' if @certificates.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def fetch_signatures
|
32
|
+
case @parser
|
33
|
+
when AppInfo::APK
|
34
|
+
signatures_from(@parser.apk)
|
35
|
+
when AppInfo::AAB
|
36
|
+
signatures_from(@parser)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_certificates
|
41
|
+
@signatures.each_with_object([]) do |(_, sign), obj|
|
42
|
+
next if sign.certificates.empty?
|
43
|
+
|
44
|
+
obj << AppInfo::Certificate.new(sign.certificates[0])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def signatures_from(parser)
|
49
|
+
signs = {}
|
50
|
+
parser.each_file do |path, data|
|
51
|
+
# find META-INF/xxx.{RSA|DSA|EC}
|
52
|
+
next unless path =~ %r{^META-INF/} && data.unpack('CC') == PKCS7_HEADER
|
53
|
+
|
54
|
+
signs[path] = OpenSSL::PKCS7.new(data)
|
55
|
+
end
|
56
|
+
signs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
register(Version::V1, V1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|