app-info 3.0.0.beta1 → 3.0.0.beta3

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.
@@ -1,127 +1,131 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AppInfo::Android::Signature
4
- # Android v3 Signature
5
- #
6
- # FULL FORMAT:
7
- # OFFSET DATA TYPE DESCRIPTION
8
- # * @+0 bytes uint32: signer size in bytes
9
- # * @+4 bytes payload signer block
10
- # * @+0 bytes unit32: signed data size in bytes
11
- # * @+4 bytes payload signed data block
12
- # * @+0 bytes unit32: digests with size in bytes
13
- # * @+0 bytes unit32: digests with size in bytes
14
- # * @+W bytes unit32: minSDK
15
- # * @+X+4 bytes unit32: maxSDK
16
- # * @+Y+4 bytes unit32: signatures with size in bytes
17
- # * @+Y+4 bytes payload signed data block
18
- # * @+Z bytes unit32: public key with size in bytes
19
- # * @+Z+4 bytes payload signed data block
20
- class V3 < Base
21
- include AppInfo::Helper::IOBlock
22
- include AppInfo::Helper::Signatures
23
- include AppInfo::Helper::Algorithm
24
-
25
- # V3 Signature ID 0xf05368c0
26
- V3_BLOCK_ID = [0xc0, 0x68, 0x53, 0xf0].freeze
27
-
28
- # V3.1 Signature ID 0x1b93ad61
29
- V3_1_BLOCK_ID = [0x61, 0xad, 0x93, 0x1b].freeze
30
-
31
- attr_reader :certificates, :digests
32
-
33
- def version
34
- Version::V3
35
- end
36
-
37
- def verify
38
- begin
39
- signers_block = singers_block(V3_1_BLOCK_ID)
40
- rescue NotFoundError
41
- signers_block = singers_block(V3_BLOCK_ID)
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
42
126
  end
43
127
 
44
- @certificates, @digests = verified_certs(signers_block)
45
- end
46
-
47
- private
48
-
49
- def verified_certs(signers_block)
50
- unless (signers = length_prefix_block(signers_block))
51
- raise SecurityError, 'Not found signers'
52
- end
53
-
54
- certificates = []
55
- content_digests = {}
56
- loop_length_prefix_io(signers, name: 'Singer', logger: logger) do |signer|
57
- signer_certs, signer_digests = extract_signer_data(signer)
58
- certificates.concat(signer_certs)
59
- content_digests.merge!(signer_digests)
60
- end
61
- raise SecurityError, 'No signers found' if certificates.empty?
62
-
63
- [certificates, content_digests]
64
- end
65
-
66
- def extract_signer_data(signer)
67
- # raw data
68
- signed_data = length_prefix_block(signer)
69
-
70
- # TODO: verify min_sdk and max_sdk
71
- min_sdk = signer.read(UINT32_SIZE)
72
- max_sdk = signer.read(UINT32_SIZE)
73
-
74
- signatures = length_prefix_block(signer)
75
- public_key = length_prefix_block(signer, raw: true)
76
-
77
- algorithems = signature_algorithms(signatures)
78
- raise SecurityError, 'No signatures found' if algorithems.empty?
79
-
80
- # find best algorithem to verify signed data with public key and signature
81
- unless best_algorithem = best_algorithem(algorithems)
82
- raise SecurityError, 'No supported signatures found'
83
- end
84
-
85
- algorithems_digest = best_algorithem[:digest]
86
- signature = best_algorithem[:signature]
87
-
88
- pkey = OpenSSL::PKey.read(public_key)
89
- digest = OpenSSL::Digest.new(algorithems_digest)
90
- verified = pkey.verify(digest, signature, signed_data.string)
91
- raise SecurityError, "#{algorithems_digest} signature did not verify" unless verified
92
-
93
- # verify algorithm ID full equal (and sort) between digests and signature
94
- digests = length_prefix_block(signed_data)
95
- content_digests = signed_data_digests(digests)
96
- content_digest = content_digests[algorithems_digest]&.fetch(:content)
97
-
98
- unless content_digest
99
- raise SecurityError,
100
- 'Signature algorithms don\'t match between digests and signatures records'
101
- end
102
-
103
- previous_digest = content_digests.fetch(algorithems_digest)
104
- content_digests[algorithems_digest] = content_digest
105
- if previous_digest && previous_digest[:content] != content_digest
106
- raise SecurityError,
107
- 'Signature algorithms don\'t match between digests and signatures records'
108
- end
109
-
110
- certificates = length_prefix_block(signed_data)
111
- certs = signed_data_certs(certificates)
112
- raise SecurityError, 'No certificates listed' if certs.empty?
113
-
114
- main_cert = certs[0]
115
- if main_cert.public_key.to_der != pkey.to_der
116
- raise SecurityError, 'Public key mismatch between certificate and signature record'
117
- end
118
-
119
- additional_attrs = length_prefix_block(signed_data)
120
- verify_additional_attrs(additional_attrs, certs)
121
-
122
- [certs, content_digests]
128
+ register(Version::V3, V3)
123
129
  end
124
130
  end
125
-
126
- register(Version::V3, V3)
127
131
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module AppInfo::Android::Signature
4
- # Android v4 Signature
5
- #
6
- # TODO: ApkSignatureSchemeV4Verifier.java
7
- class V4 < Base
8
- def version
9
- Version::V4
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)
10
16
  end
11
17
  end
12
-
13
- # register(Version::V4, V4)
14
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,49 +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
6
  # Parse APK file parser, wrapper for {https://github.com/icyleaf/android_parser android_parser}.
9
- class APK < File
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
-
24
- # return file size
25
- # @example Read file size in integer
26
- # aab.size # => 3618865
27
- #
28
- # @example Read file size in human readabale
29
- # aab.size(human_size: true) # => '3.45 MB'
30
- #
31
- # @param [Boolean] human_size Convert integer value to human readable.
32
- # @return [Integer, String]
33
- def size(human_size: false)
34
- file_to_human_size(@file, human_size: human_size)
35
- end
36
-
37
- def file_type
38
- Format::APK
39
- end
40
-
41
- def platform
42
- Platform::ANDROID
43
- end
44
-
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
45
14
  def_delegators :apk, :manifest, :resource, :dex
46
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
47
34
  def_delegators :manifest, :version_name, :package_name, :target_sdk_version,
48
35
  :components, :services, :use_permissions, :use_features,
49
36
  :deep_links, :schemes
@@ -61,73 +48,64 @@ module AppInfo
61
48
  manifest.label || resource.find('@string/app_name')
62
49
  end
63
50
 
64
- def device_type
65
- if wear?
66
- Device::WATCH
67
- elsif tv?
68
- Device::TV
69
- elsif automotive?
70
- Device::AUTOMOTIVE
71
- else
72
- Device::PHONE
73
- end
74
- end
75
-
76
- # TODO: find a way to detect, no way!
77
- # def tablet?
78
- # end
79
-
80
- def wear?
81
- use_features.include?('android.hardware.type.watch')
82
- end
83
-
84
- def tv?
85
- use_features.include?('android.software.leanback')
86
- end
87
-
88
- def automotive?
89
- use_features.include?('android.hardware.type.automotive')
90
- end
91
-
51
+ # @return [String]
92
52
  def min_sdk_version
93
53
  manifest.min_sdk_ver
94
54
  end
95
55
  alias min_os_version min_sdk_version
96
56
 
97
- # Return multi version certifiates of signatures
98
- # @return [Array<Hash>]
99
- # @see AppInfo::Android::Signature.verify
100
- def signatures
101
- @signatures ||= Android::Signature.verify(self)
102
- end
103
-
104
- # Legacy v1 scheme signatures, it will remove soon.
105
- # @deprecated Use {#signatures}
106
- # @return [Array<OpenSSL::PKCS7, nil>]
107
- def signs
108
- @signs ||= v1sign&.signatures || []
109
- end
110
-
111
- # Legacy v1 scheme certificates, it will remove soon.
112
- # @deprecated Use {#signatures}
113
- # @return [Array<OpenSSL::PKCS7, nil>]
114
- def certificates
115
- @certificates ||= v1sign&.certificates || []
116
- end
117
-
57
+ # @return [String]
118
58
  def activities
119
59
  components.select { |c| c.type == 'activity' }
120
60
  end
121
61
 
62
+ # @return [::Android::Apk]
122
63
  def apk
123
64
  @apk ||= ::Android::Apk.new(@file)
124
65
  end
125
66
 
67
+ # @return [Zip::File]
126
68
  def zip
127
69
  @zip ||= apk.instance_variable_get(:@zip)
128
70
  end
129
71
 
130
- def icons
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)
131
109
  @icons ||= apk.icon.each_with_object([]) do |(path, data), obj|
132
110
  icon_name = ::File.basename(path)
133
111
  icon_path = ::File.join(contents, ::File.dirname(path))
@@ -141,6 +119,8 @@ module AppInfo
141
119
  dimensions: ImageSize.path(icon_file).size
142
120
  }
143
121
  end
122
+
123
+ extract_icon(@icons, exclude: exclude)
144
124
  end
145
125
 
146
126
  def clear!
@@ -154,17 +134,5 @@ module AppInfo
154
134
  @app_path = nil
155
135
  @info = nil
156
136
  end
157
-
158
- def contents
159
- @contents ||= ::File.join(Dir.mktmpdir, "AppInfo-android-#{SecureRandom.hex}")
160
- end
161
-
162
- private
163
-
164
- def v1sign
165
- @v1sign ||= Android::Signature::V1.verify(self)
166
- rescue Android::Signature::NotFoundError
167
- nil
168
- end
169
137
  end
170
138
  end