app-info 2.8.5 → 3.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +12 -7
  3. data/.github/workflows/ci.yml +3 -1
  4. data/.rubocop.yml +33 -11
  5. data/CHANGELOG.md +34 -1
  6. data/Gemfile +7 -1
  7. data/README.md +68 -13
  8. data/Rakefile +11 -0
  9. data/app_info.gemspec +12 -3
  10. data/lib/app_info/aab.rb +48 -108
  11. data/lib/app_info/android/signature.rb +114 -0
  12. data/lib/app_info/android/signatures/base.rb +53 -0
  13. data/lib/app_info/android/signatures/info.rb +158 -0
  14. data/lib/app_info/android/signatures/v1.rb +63 -0
  15. data/lib/app_info/android/signatures/v2.rb +121 -0
  16. data/lib/app_info/android/signatures/v3.rb +131 -0
  17. data/lib/app_info/android/signatures/v4.rb +18 -0
  18. data/lib/app_info/android.rb +162 -0
  19. data/lib/app_info/apk.rb +54 -111
  20. data/lib/app_info/apple.rb +192 -0
  21. data/lib/app_info/certificate.rb +175 -0
  22. data/lib/app_info/const.rb +75 -0
  23. data/lib/app_info/core_ext/object/try.rb +3 -1
  24. data/lib/app_info/core_ext/string/inflector.rb +2 -0
  25. data/lib/app_info/dsym/debug_info.rb +72 -0
  26. data/lib/app_info/dsym/macho.rb +55 -0
  27. data/lib/app_info/dsym.rb +31 -135
  28. data/lib/app_info/error.rb +2 -2
  29. data/lib/app_info/file.rb +49 -0
  30. data/lib/app_info/helper/archive.rb +37 -0
  31. data/lib/app_info/helper/file_size.rb +25 -0
  32. data/lib/app_info/helper/generate_class.rb +29 -0
  33. data/lib/app_info/helper/protobuf.rb +12 -0
  34. data/lib/app_info/helper/signatures.rb +229 -0
  35. data/lib/app_info/helper.rb +5 -126
  36. data/lib/app_info/info_plist.rb +66 -29
  37. data/lib/app_info/ipa/framework.rb +4 -4
  38. data/lib/app_info/ipa.rb +61 -135
  39. data/lib/app_info/macos.rb +54 -102
  40. data/lib/app_info/mobile_provision.rb +67 -49
  41. data/lib/app_info/pe.rb +260 -0
  42. data/lib/app_info/png_uncrush.rb +24 -4
  43. data/lib/app_info/proguard.rb +29 -16
  44. data/lib/app_info/protobuf/manifest.rb +6 -3
  45. data/lib/app_info/protobuf/models/Configuration_pb.rb +1 -0
  46. data/lib/app_info/protobuf/models/README.md +7 -0
  47. data/lib/app_info/protobuf/models/Resources_pb.rb +2 -0
  48. data/lib/app_info/protobuf/resources.rb +5 -5
  49. data/lib/app_info/version.rb +1 -1
  50. data/lib/app_info.rb +90 -46
  51. metadata +48 -35
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ # Certificate wrapper for
5
+ # {https://docs.ruby-lang.org/en/3.0/OpenSSL/X509/Certificate.html OpenSSL::X509::Certifiate}.
6
+ class Certificate
7
+ # Parse Raw data into X509 cerificate wrapper
8
+ # @param [String] certificate raw data
9
+ def self.parse(data)
10
+ cert = OpenSSL::X509::Certificate.new(data)
11
+ new(cert)
12
+ end
13
+
14
+ # @param [OpenSSL::X509::Certificate] certificate
15
+ def initialize(cert)
16
+ @cert = cert
17
+ end
18
+
19
+ # return version of certificate
20
+ # @param [String] prefix
21
+ # @param [Integer] base
22
+ # @return [String] version
23
+ def version(prefix: 'v', base: 1)
24
+ "#{prefix}#{raw.version + base}"
25
+ end
26
+
27
+ # return serial of certificate
28
+ # @param [Integer] base
29
+ # @param [Symbol] transform avaiables in :lower, :upper
30
+ # @param [String] prefix
31
+ # @return [String] serial
32
+ def serial(base = 10, transform: :lower, prefix: nil)
33
+ serial = raw.serial.to_s(base)
34
+ serial = transform == :lower ? serial.downcase : serial.upcase
35
+ return serial unless prefix
36
+
37
+ "#{prefix}#{serial}"
38
+ end
39
+
40
+ # return issuer from DN, similar to {#subject}.
41
+ #
42
+ # Example:
43
+ #
44
+ # @param [Symbol] format avaiables in `:to_a`, `:to_s` and `:raw`
45
+ # @return [Array, String, OpenSSL::X509::Name] the object converted into the expected format.
46
+ def issuer(format: :raw)
47
+ convert_cert_name(raw.issuer, format: format)
48
+ end
49
+
50
+ # return subject from DN, similar to {#issuer}.
51
+ # @param [Symbol] format avaiables in `:to_a`, `:to_s` and `:raw`
52
+ # @return [Array, String, OpenSSL::X509::Name] the object converted into the expected format.
53
+ def subject(format: :raw)
54
+ convert_cert_name(raw.subject, format: format)
55
+ end
56
+
57
+ # @return [Time]
58
+ def created_at
59
+ raw.not_before
60
+ end
61
+
62
+ # @return [Time]
63
+ def expired_at
64
+ raw.not_after
65
+ end
66
+
67
+ # @return [Boolean]
68
+ def expired?
69
+ expired_at < Time.now.utc
70
+ end
71
+
72
+ # @return [String] format always be :x509.
73
+ def format
74
+ :x509
75
+ end
76
+
77
+ # return algorithm digest
78
+ #
79
+ # OpenSSL supported digests:
80
+ #
81
+ # -blake2b512 -blake2s256 -md4
82
+ # -md5 -md5-sha1 -mdc2
83
+ # -ripemd -ripemd160 -rmd160
84
+ # -sha1 -sha224 -sha256
85
+ # -sha3-224 -sha3-256 -sha3-384
86
+ # -sha3-512 -sha384 -sha512
87
+ # -sha512-224 -sha512-256 -shake128
88
+ # -shake256 -sm3 -ssl3-md5
89
+ # -ssl3-sha1 -whirlpool
90
+ def digest
91
+ signature_algorithm = raw.signature_algorithm
92
+
93
+ case signature_algorithm
94
+ when /md5/
95
+ :md5
96
+ when /sha1/
97
+ :sha1
98
+ when /sha224/
99
+ :sha224
100
+ when /sha256/
101
+ :sha256
102
+ when /sha512/
103
+ :sha512
104
+ else
105
+ # Android signature no need the others
106
+ signature_algorithm.to_sym
107
+ end
108
+ end
109
+
110
+ # return algorithm name of public key
111
+ def algorithm
112
+ case public_key
113
+ when OpenSSL::PKey::RSA then :rsa
114
+ when OpenSSL::PKey::DSA then :dsa
115
+ when OpenSSL::PKey::DH then :dh
116
+ when OpenSSL::PKey::EC then :ec
117
+ end
118
+ end
119
+
120
+ # return size of public key
121
+ # @return [Integer]
122
+ def size
123
+ case public_key
124
+ when OpenSSL::PKey::RSA
125
+ public_key.n.num_bits
126
+ when OpenSSL::PKey::DSA, OpenSSL::PKey::DH
127
+ public_key.p.num_bits
128
+ when OpenSSL::PKey::EC
129
+ raise NotImplementedError, "key size for #{public_key.inspect} not implemented"
130
+ end
131
+ end
132
+
133
+ # return fingerprint of certificate
134
+ # @return [String]
135
+ def fingerprint(name = :sha256, transform: :lower, delimiter: nil)
136
+ digest = OpenSSL::Digest.new(name.to_s.upcase)
137
+ digest.update(raw.to_der)
138
+ fingerprint = digest.to_s
139
+ fingerprint = fingerprint.upcase if transform.to_sym == :upper
140
+ return fingerprint unless delimiter
141
+
142
+ fingerprint.scan(/../).join(delimiter)
143
+ end
144
+
145
+ # Orginal OpenSSL X509 certificate
146
+ # @return [OpenSSL::X509::Certificate]
147
+ def raw
148
+ @cert
149
+ end
150
+
151
+ private
152
+
153
+ def convert_cert_name(name, format:)
154
+ data = name.to_a
155
+ case format
156
+ when :to_a
157
+ data.map { |k, v, _| [k, v] }
158
+ when :to_s
159
+ data.map { |k, v, _| "#{k}=#{v}" }.join(' ')
160
+ else
161
+ name
162
+ end
163
+ end
164
+
165
+ def method_missing(method, *args, &block)
166
+ @cert.send(method.to_sym, *args, &block) || super
167
+ end
168
+
169
+ def respond_to_missing?(method_name, *args)
170
+ @cert.key?(method_name.to_sym) ||
171
+ @cert.respond_to?(method_name) ||
172
+ super
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppInfo
4
+ # Full Format
5
+ module Format
6
+ # Apple
7
+
8
+ INFOPLIST = :infoplist
9
+ MOBILEPROVISION = :mobileprovision
10
+ DSYM = :dsym
11
+
12
+ # macOS
13
+
14
+ MACOS = :macos
15
+
16
+ # iOS
17
+
18
+ IPA = :ipa
19
+
20
+ # Android
21
+
22
+ APK = :apk
23
+ AAB = :aab
24
+ PROGUARD = :proguard
25
+
26
+ # Windows
27
+
28
+ PE = :pe
29
+
30
+ UNKNOWN = :unknown
31
+ end
32
+
33
+ # Platform
34
+ module Platform
35
+ APPLE = :apple
36
+ GOOGLE = :google
37
+ WINDOWS = :windows
38
+ end
39
+
40
+ module OperaSystem
41
+ MACOS = :macos
42
+ IOS = :ios
43
+ ANDROID = :android
44
+ WINDOWS = :windows
45
+ end
46
+
47
+ # Apple Device Type
48
+ module Device
49
+ # macOS
50
+ MACOS = :macos
51
+
52
+ # Apple iPhone
53
+ IPHONE = :iphone
54
+ # Apple iPad
55
+ IPAD = :ipad
56
+ # Apple Watch
57
+ IWATCH = :iwatch # not implemented yet
58
+ # Apple Universal (iPhone and iPad)
59
+ UNIVERSAL = :universal
60
+
61
+ # Android Phone
62
+ PHONE = :phone
63
+ # Android Tablet (not implemented yet)
64
+ TABLET = :tablet
65
+ # Android Watch
66
+ WATCH = :watch
67
+ # Android TV
68
+ TELEVISION = :television
69
+ # Android Car Automotive
70
+ AUTOMOTIVE = :automotive
71
+
72
+ # Windows
73
+ WINDOWS = :windows
74
+ end
75
+ end
@@ -4,7 +4,8 @@
4
4
  # Copy from https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
5
5
 
6
6
  module AppInfo
7
- module Tryable # :nodoc:
7
+ # @!visibility private
8
+ module Tryable
8
9
  ##
9
10
  # :method: try
10
11
  #
@@ -107,6 +108,7 @@ module AppInfo
107
108
  end
108
109
  end
109
110
 
111
+ # @!visibility private
110
112
  class Object
111
113
  include AppInfo::Tryable
112
114
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppInfo
4
+ # @!visibility private
4
5
  module Inflector
5
6
  def ai_snakecase
6
7
  gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -30,6 +31,7 @@ module AppInfo
30
31
  end
31
32
  end
32
33
 
34
+ # @!visibility private
33
35
  class String
34
36
  include AppInfo::Inflector
35
37
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'app_info/dsym/macho'
4
+
5
+ module AppInfo
6
+ class DSYM < File
7
+ # DSYM Debug Information Format Struct
8
+ class DebugInfo
9
+ attr_reader :path
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ end
14
+
15
+ def object
16
+ @object ||= ::File.basename(bin_path)
17
+ end
18
+
19
+ def macho_type
20
+ @macho_type ||= ::MachO.open(bin_path)
21
+ end
22
+
23
+ def machos
24
+ @machos ||= case macho_type
25
+ when ::MachO::MachOFile
26
+ [MachO.new(macho_type, ::File.size(bin_path))]
27
+ else
28
+ size = macho_type.fat_archs.each_with_object([]) do |arch, obj|
29
+ obj << arch.size
30
+ end
31
+
32
+ machos = []
33
+ macho_type.machos.each_with_index do |file, i|
34
+ machos << MachO.new(file, size[i])
35
+ end
36
+ machos
37
+ end
38
+ end
39
+
40
+ def release_version
41
+ info.try(:[], 'CFBundleShortVersionString')
42
+ end
43
+
44
+ def build_version
45
+ info.try(:[], 'CFBundleVersion')
46
+ end
47
+
48
+ def identifier
49
+ info.try(:[], 'CFBundleIdentifier').sub('com.apple.xcode.dsym.', '')
50
+ end
51
+ alias bundle_id identifier
52
+
53
+ def info
54
+ return nil unless ::File.exist?(info_path)
55
+
56
+ @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
57
+ end
58
+
59
+ def info_path
60
+ @info_path ||= ::File.join(path, 'Contents', 'Info.plist')
61
+ end
62
+
63
+ def bin_path
64
+ @bin_path ||= lambda {
65
+ dwarf_path = ::File.join(path, 'Contents', 'Resources', 'DWARF')
66
+ name = Dir.children(dwarf_path)[0]
67
+ ::File.join(dwarf_path, name)
68
+ }.call
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'macho'
4
+
5
+ module AppInfo
6
+ class DSYM < File
7
+ # Mach-O Struct
8
+ class MachO
9
+ include Helper::HumanFileSize
10
+
11
+ def initialize(file, size = 0)
12
+ @file = file
13
+ @size = size
14
+ end
15
+
16
+ def cpu_name
17
+ @file.cpusubtype
18
+ end
19
+
20
+ def cpu_type
21
+ @file.cputype
22
+ end
23
+
24
+ def type
25
+ @file.filetype
26
+ end
27
+
28
+ def size(human_size: false)
29
+ return number_to_human_size(@size) if human_size
30
+
31
+ @size
32
+ end
33
+
34
+ def uuid
35
+ @file[:LC_UUID][0].uuid_string
36
+ end
37
+ alias debug_id uuid
38
+
39
+ def header
40
+ @header ||= @file.header
41
+ end
42
+
43
+ def to_h
44
+ {
45
+ uuid: uuid,
46
+ type: type,
47
+ cpu_name: cpu_name,
48
+ cpu_type: cpu_type,
49
+ size: size,
50
+ human_size: size(human_size: true)
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/app_info/dsym.rb CHANGED
@@ -1,78 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'macho'
3
+ require 'app_info/dsym/debug_info'
4
4
 
5
5
  module AppInfo
6
6
  # DSYM parser
7
- class DSYM
7
+ class DSYM < File
8
8
  include Helper::Archive
9
9
 
10
- attr_reader :file
11
-
12
- def initialize(file)
13
- @file = file
14
- end
15
-
16
- def file_type
17
- Platform::DSYM
18
- end
19
-
20
- def object
21
- @object ||= File.basename(app_path)
22
- end
23
-
24
- def macho_type
25
- @macho_type ||= ::MachO.open(app_path)
26
- end
27
-
28
- def machos
29
- @machos ||= case macho_type
30
- when ::MachO::MachOFile
31
- [MachO.new(macho_type, File.size(app_path))]
32
- else
33
- size = macho_type.fat_archs.each_with_object([]) do |arch, obj|
34
- obj << arch.size
35
- end
36
-
37
- machos = []
38
- macho_type.machos.each_with_index do |file, i|
39
- machos << MachO.new(file, size[i])
40
- end
41
- machos
42
- end
43
- end
44
-
45
- def release_version
46
- info.try(:[], 'CFBundleShortVersionString')
10
+ # @return [Symbol] {Platform}
11
+ def platform
12
+ Platform::APPLE
47
13
  end
48
14
 
49
- def build_version
50
- info.try(:[], 'CFBundleVersion')
15
+ def each_file(&block)
16
+ files.each { |file| block.call(file) }
51
17
  end
52
18
 
53
- def identifier
54
- info.try(:[], 'CFBundleIdentifier').sub('com.apple.xcode.dsym.', '')
55
- end
56
- alias bundle_id identifier
57
-
58
- def info
59
- return nil unless File.exist?(info_path)
60
-
61
- @info ||= CFPropertyList.native_types(CFPropertyList::List.new(file: info_path).value)
62
- end
63
-
64
- def info_path
65
- @info_path ||= File.join(contents, 'Contents', 'Info.plist')
66
- end
67
-
68
- def app_path
69
- unless @app_path
70
- path = File.join(contents, 'Contents', 'Resources', 'DWARF')
71
- name = Dir.entries(path).reject { |f| ['.', '..'].include?(f) }.first
72
- @app_path = File.join(path, name)
19
+ # @return [Array<DebugInfo>] dsym_files files by alphabetical order
20
+ def files
21
+ @files ||= Dir.children(contents).sort.each_with_object([]) do |file, obj|
22
+ obj << DebugInfo.new(::File.join(contents, file))
73
23
  end
74
-
75
- @app_path
76
24
  end
77
25
 
78
26
  def clear!
@@ -81,85 +29,33 @@ module AppInfo
81
29
  FileUtils.rm_rf(@contents)
82
30
 
83
31
  @contents = nil
84
- @app_path = nil
85
- @info = nil
86
- @object = nil
87
- @macho_type = nil
32
+ @files = nil
88
33
  end
89
34
 
35
+ # @return [String] contents path of dsym
90
36
  def contents
91
- unless @contents
92
- if File.directory?(@file)
93
- @contents = @file
94
- else
95
- dsym_dir = nil
96
- @contents = unarchive(@file, path: 'dsym') do |path, zip_file|
97
- zip_file.each do |f|
98
- unless dsym_dir
99
- dsym_dir = f.name
100
- # fix filename is xxx.app.dSYM/Contents
101
- dsym_dir = dsym_dir.split('/')[0] if dsym_dir.include?('/')
102
- end
37
+ @contents ||= lambda {
38
+ return @file if ::File.directory?(@file)
103
39
 
104
- f_path = File.join(path, f.name)
105
- FileUtils.mkdir_p(File.dirname(f_path))
106
- f.extract(f_path) unless File.exist?(f_path)
107
- end
108
- end
40
+ dsym_filenames = []
41
+ unarchive(@file, prefix: 'dsym') do |base_path, zip_file|
42
+ zip_file.each do |entry|
43
+ file_path = entry.name
44
+ next unless file_path.downcase.include?('.dsym/contents/')
45
+ next if ::File.basename(file_path).start_with?('.')
109
46
 
110
- @contents = File.join(@contents, dsym_dir)
111
- end
112
- end
113
-
114
- @contents
115
- end
116
-
117
- # DSYM Mach-O
118
- class MachO
119
- include Helper::HumanFileSize
120
-
121
- def initialize(file, size = 0)
122
- @file = file
123
- @size = size
124
- end
125
-
126
- def cpu_name
127
- @file.cpusubtype
128
- end
129
-
130
- def cpu_type
131
- @file.cputype
132
- end
133
-
134
- def type
135
- @file.filetype
136
- end
137
-
138
- def size(human_size: false)
139
- return number_to_human_size(@size) if human_size
140
-
141
- @size
142
- end
143
-
144
- def uuid
145
- @file[:LC_UUID][0].uuid_string
146
- end
147
- alias debug_id uuid
47
+ dsym_filename = file_path.split('/').select { |f| f.downcase.end_with?('.dsym') }.last
48
+ dsym_filenames << dsym_filename unless dsym_filenames.include?(dsym_filename)
148
49
 
149
- def header
150
- @header ||= @file.header
151
- end
50
+ unless file_path.start_with?(dsym_filename)
51
+ file_path = file_path.split('/')[1..-1].join('/')
52
+ end
152
53
 
153
- def to_h
154
- {
155
- uuid: uuid,
156
- type: type,
157
- cpu_name: cpu_name,
158
- cpu_type: cpu_type,
159
- size: size,
160
- human_size: size(human_size: true)
161
- }
162
- end
54
+ dest_path = ::File.join(base_path, file_path)
55
+ entry.extract(dest_path) unless ::File.exist?(dest_path)
56
+ end
57
+ end
58
+ }.call
163
59
  end
164
60
  end
165
61
  end
@@ -9,7 +9,7 @@ module AppInfo
9
9
 
10
10
  class NotFoundError < Error; end
11
11
 
12
- class UnkownFileTypeError < Error; end
13
-
14
12
  class ProtobufParseError < Error; end
13
+
14
+ class UnknownFormatError < Error; end
15
15
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AppInfo base file
4
+ module AppInfo
5
+ class File
6
+ attr_reader :file, :logger
7
+
8
+ def initialize(file, logger: AppInfo.logger)
9
+ @file = file
10
+ @logger = logger
11
+ end
12
+
13
+ # @return [Symbol] {Format}
14
+ def format
15
+ @format ||= lambda {
16
+ if instance_of?(AppInfo::File) || instance_of?(AppInfo::Apple) ||
17
+ instance_of?(AppInfo::Android)
18
+ not_implemented_error!(__method__)
19
+ end
20
+
21
+ self.class.name.split('::')[-1].downcase.to_sym
22
+ }.call
23
+ end
24
+
25
+ # @abstract Subclass and override {#opera_system} to implement.
26
+ def opera_system
27
+ not_implemented_error!(__method__)
28
+ end
29
+
30
+ # @abstract Subclass and override {#platform} to implement.
31
+ def platform
32
+ not_implemented_error!(__method__)
33
+ end
34
+
35
+ # @abstract Subclass and override {#device} to implement.
36
+ def device
37
+ not_implemented_error!(__method__)
38
+ end
39
+
40
+ # @abstract Subclass and override {#size} to implement
41
+ def size(human_size: false)
42
+ not_implemented_error!(__method__)
43
+ end
44
+
45
+ def not_implemented_error!(method)
46
+ raise NotImplementedError, ".#{method} method implantation required in #{self.class}"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zip'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+ require 'securerandom'
7
+
8
+ module AppInfo::Helper
9
+ module Archive
10
+ # Unarchive zip file
11
+ #
12
+ # source: https://github.com/soffes/lagunitas/blob/master/lib/lagunitas/ipa.rb
13
+ def unarchive(file, prefix:, dest_path: '/tmp')
14
+ base_path = Dir.mktmpdir("appinfo-#{prefix}", dest_path)
15
+ Zip::File.open(file) do |zip_file|
16
+ if block_given?
17
+ yield base_path, zip_file
18
+ else
19
+ zip_file.each do |f|
20
+ f_path = ::File.join(base_path, f.name)
21
+ FileUtils.mkdir_p(::File.dirname(f_path))
22
+ zip_file.extract(f, f_path) unless ::File.exist?(f_path)
23
+ end
24
+ end
25
+ end
26
+
27
+ base_path
28
+ end
29
+
30
+ def tempdir(file, prefix:, system: false)
31
+ base_path = system ? '/tmp' : ::File.dirname(file)
32
+ full_prefix = "appinfo-#{prefix}-#{::File.basename(file, '.*')}"
33
+ dest_path = Dir.mktmpdir(full_prefix, base_path)
34
+ ::File.join(dest_path, ::File.basename(file))
35
+ end
36
+ end
37
+ end