ruby-common 1.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.
@@ -0,0 +1,203 @@
1
+ require 'base64'
2
+ require 'date'
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ require_relative '../common/PhpUnpack.rb'
7
+ require_relative '../common/Ipv6Utils.rb'
8
+ require_relative './AsymmetricOpenSSL.rb'
9
+ require_relative '../common/Utils.rb'
10
+ require_relative '../common/Exceptions'
11
+ require_relative '../common/VerifierConstants.rb'
12
+ require_relative './Signature4VerificationResult.rb'
13
+
14
+
15
+ # Helper class to represent field information
16
+ class Field
17
+ attr_reader :name, :type
18
+
19
+ def initialize(name, type)
20
+ @name = name
21
+ @type = type
22
+ end
23
+ end
24
+
25
+ # Signature4VerifierService class
26
+ class Signature4VerifierService
27
+ FIELD_IDS = {
28
+ 0x00 => Field.new('requestTime', 'ulong'),
29
+ 0x01 => Field.new('signatureTime', 'ulong'),
30
+ 0x10 => Field.new('ipv4', 'ulong'),
31
+ 0x40 => Field.new(nil, 'ushort'),
32
+ 0x80 => Field.new('masterSignType', 'uchar'),
33
+ 0x81 => Field.new('customerSignType', 'uchar'),
34
+ 0xC0 => Field.new('masterToken', 'string'),
35
+ 0xC1 => Field.new('customerToken', 'string'),
36
+ 0xC2 => Field.new('masterTokenV6', 'string'),
37
+ 0xC3 => Field.new('customerTokenV6', 'string'),
38
+ 0xc4 => Field.new('ipv6', 'string'),
39
+ 0xc5 => Field.new('masterChecksum', 'string'),
40
+ 0xd0 => Field.new('userAgent', 'string') #DEBUG FIELD
41
+ }.freeze
42
+
43
+ def self.verifySignature(signature, user_agent, key, ip_addresses, expiry, is_key_base64_encoded)
44
+ validation_result = {}
45
+
46
+ begin
47
+ data = parse4(signature)
48
+ rescue VersionError
49
+ data = parse3(signature)
50
+ end
51
+
52
+ sign_role_token = data["customerToken"]
53
+
54
+ if sign_role_token.nil? || sign_role_token.empty?
55
+ raise VerifyError, 'sign role signature mismatch'
56
+ end
57
+
58
+ sign_type = data["customerSignType"]
59
+
60
+ ip_addresses.each do |ip_address|
61
+ next if ip_address.nil? || ip_address.empty?
62
+
63
+ token = if IpV6Utils.validate(ip_address)
64
+ next unless data.key?("customerTokenV6")
65
+ IpV6Utils.abbreviate(ip_address)
66
+ data["customerTokenV6"]
67
+ else
68
+ next unless data.key?("customerToken")
69
+ data["customerToken"]
70
+ end
71
+
72
+ signature_time = data['signatureTime'].first
73
+ request_time = data['requestTime'].first
74
+
75
+ VerifierConstants::RESULTS.each do |result, verdict|
76
+ signature_base = get_base(result, request_time, signature_time, ip_address, user_agent)
77
+
78
+ case sign_type.first
79
+ when 1 # HASH_SHA256
80
+ is_hashed_data_equal_to_token = SignatureVerifierUtils.encode(
81
+ is_key_base64_encoded ? SignatureVerifierUtils.base64_decode(key) : key,
82
+ signature_base
83
+ ) == token
84
+
85
+ if is_hashed_data_equal_to_token
86
+ if is_expired(expiry, signature_time, request_time)
87
+ return Signature4VerificationResult.is_expired
88
+ end
89
+
90
+ return Signature4VerificationResult.new(
91
+ score: result.to_i,
92
+ verdict: verdict,
93
+ ip_address: ip_address,
94
+ request_time: request_time,
95
+ signature_time: signature_time
96
+ )
97
+ end
98
+ when 2 # SIGN_SHA256
99
+ if AsymmetricOpenSSL.verify_data(signature_base, token, key)
100
+ return Signature4VerificationResult.new(
101
+ score: result.to_i,
102
+ verdict: verdict,
103
+ ip_address: ip_address,
104
+ request_time: request_time,
105
+ signature_time: signature_time
106
+ )
107
+ end
108
+ else
109
+ raise VerifyError, 'unrecognized signature'
110
+ end
111
+ end
112
+ end
113
+ raise StructParseError, 'no verdict'
114
+ end
115
+
116
+ class << self
117
+ private
118
+
119
+ def parse3(signature)
120
+ sign_decoded = SignatureVerifierUtils.base64_decode(signature)
121
+ unpack_result = PhpUnpack.unpack('Cversion/NrequestTime/NsignatureTime/CmasterSignType/nmasterTokenLength', sign_buffer)
122
+
123
+ version = unpack_result['version']
124
+ raise VersionError, 'Invalid signature version' if version != 3
125
+
126
+ timestamp = unpack_result['timestamp']
127
+ raise SignatureParseError, 'invalid timestamp (future time)' if timestamp > (Time.now.to_i)
128
+
129
+ master_token_length = unpack_result['masterTokenLength']
130
+ #TODO CO TO KURWA JEST ZA METODA?
131
+ master_token = get_bytes_and_advance_position(sign_decoded, master_token_length)
132
+ unpack_result['masterToken'] = master_token
133
+
134
+ customer_data = PhpUnpack.unpack('CcustomerSignType/ncustomerTokenLength', sign_buffer)
135
+ customer_token_length = customer_data['customerTokenLength']
136
+ customer_token = get_bytes_and_advance_position(sign_decoded, customer_token_length)
137
+ customer_data['customerToken'] = customer_token
138
+
139
+ unpack_result.merge!(customer_data)
140
+ end
141
+
142
+ def parse4(signature)
143
+ sign_decoded = SignatureVerifierUtils.base64_decode(signature)
144
+ raise SignatureParseError, 'invalid base64 payload' if sign_decoded.empty?
145
+
146
+ data = PhpUnpack.unpack('Cversion/CfieldNum', sign_decoded)
147
+
148
+ version = data['version'].first
149
+ raise VersionError, 'Invalid signature version' if version != 4
150
+
151
+ field_num = data['fieldNum'].first
152
+ field_num.times do |i|
153
+ header = PhpUnpack.unpack('CfieldId', sign_decoded)
154
+
155
+ raise SignatureParseError, 'premature end of signature 0x01' if header.empty? || !header.key?('fieldId')
156
+
157
+ field = FIELD_IDS[header['fieldId'].first]
158
+ v = {}
159
+
160
+ case field&.type
161
+ when 'uchar'
162
+ v = PhpUnpack.unpack('Cv', sign_decoded)
163
+ data[field.name] = v['v'] if v.key?('v')
164
+ when 'ushort'
165
+ v = PhpUnpack.unpack('nv', sign_decoded)
166
+ data[field.name] = v['v'] if v.key?('v')
167
+ when 'ulong'
168
+ v = PhpUnpack.unpack('Nv', sign_decoded)
169
+ data[field.name] = v['v'] if v.key?('v')
170
+ when 'string'
171
+ l = PhpUnpack.unpack('nl', sign_decoded)
172
+ raise SignatureParseError, 'premature end of signature 0x05' unless l.key?('l')
173
+
174
+ l_length = l['l'].first
175
+ new_v = sign_decoded.slice!(0, l_length)
176
+ v['v'] = new_v
177
+ data[field.name] = new_v
178
+
179
+ raise SignatureParseError, 'premature end of signature 0x06' if new_v.bytesize != l_length
180
+ else
181
+ raise SignatureParseError, 'unsupported variable type'
182
+ end
183
+ end
184
+ data.delete(field_num.to_s)
185
+ data
186
+ end
187
+
188
+ def is_expired(expiry, signature_time, request_time)
189
+ return false if expiry.nil?
190
+
191
+ current_epoch_in_seconds = (Time.now.to_f * 1000).to_i / 1000
192
+
193
+ signature_time_expired = (signature_time + expiry) < current_epoch_in_seconds
194
+ request_time_expired = request_time + expiry < current_epoch_in_seconds
195
+
196
+ signature_time_expired || request_time_expired
197
+ end
198
+
199
+ def get_base(verdict, request_time, signature_time, ip_address, user_agent)
200
+ [verdict, request_time, signature_time, ip_address, user_agent].join("\n")
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,25 @@
1
+ require_relative './DecryptResult.rb'
2
+
3
+ class AbstractSymmetricCrypt
4
+ @@method_size = 2
5
+
6
+ def parse(payload, lengths)
7
+ total_length = @@method_size + lengths.values.inject(0) { |sum, value| sum + value }
8
+
9
+ raise DecryptError, "Premature data end" if payload.size < total_length
10
+
11
+ decrypt_result = DecryptResult.new
12
+
13
+ decrypt_result.method = PhpUnpack.unpack("vX", payload)['X'].first
14
+
15
+ lengths.each do |key, length|
16
+ bytes_for_key = payload.byteslice(0,length)
17
+ payload.slice!(0,length)
18
+ decrypt_result.byte_buffer_map[key] = bytes_for_key
19
+ end
20
+
21
+ decrypt_result.data = payload
22
+
23
+ decrypt_result
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ class CryptFactory
2
+ def self.create_from_payload(payload)
3
+ header = payload.byteslice(0,2)
4
+ return create_crypt(header)
5
+ end
6
+
7
+ private
8
+ def self.create_crypt(name)
9
+ case name.unpack("v").first
10
+ when MyOpenSSL::METHOD
11
+ MyOpenSSL.new
12
+ when OpenSSLAEAD::METHOD
13
+ OpenSSLAEAD.new
14
+ when Secretbox::METHOD
15
+ Secretbox.new
16
+ else
17
+ raise SignatureParseError, "Unsupported crypt class"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,10 @@
1
+ class CryptMethodConstans
2
+ CRYPT_METHODS = {
3
+ "AES-128-GCM" => 12,
4
+ "AES-192-GCM" => 12,
5
+ "AES-256-GCM" => 12,
6
+ "AES-128-CBC" => 16,
7
+ "AES-192-CBC" => 16,
8
+ "AES-256-CBC" => 16
9
+ }.freeze
10
+ end
@@ -0,0 +1,27 @@
1
+ class DecryptResult
2
+ def initialize
3
+ @method = nil
4
+ @byte_buffer_map = {}
5
+ @data = nil
6
+ end
7
+
8
+ def method
9
+ @method
10
+ end
11
+
12
+ def method=(method)
13
+ @method = method
14
+ end
15
+
16
+ def byte_buffer_map
17
+ @byte_buffer_map
18
+ end
19
+
20
+ def data
21
+ @data
22
+ end
23
+
24
+ def data=(data)
25
+ @data = data
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ require_relative "./AbstractSymmetricCrypt.rb"
2
+ require_relative "./CryptMethodConstans.rb"
3
+
4
+
5
+ class MyOpenSSL < AbstractSymmetricCrypt
6
+ METHOD = 0x0200
7
+
8
+ def initialize(crypt_method: "AES-256-CBC")
9
+ if CryptMethodConstans::CRYPT_METHODS.key?(crypt_method)
10
+ @crypt_method = crypt_method
11
+ @crypt_iv = CryptMethodConstans::CRYPT_METHODS[crypt_method]
12
+ else
13
+ raise DecryptError, "Method not supported #{crypt_method}"
14
+ end
15
+ end
16
+
17
+ def decrypt_with_key(payload, key)
18
+ lengths = {"iv" => @crypt_iv}
19
+ result = parse(payload, lengths)
20
+
21
+ raise DecryptError, 'Unrecognized payload' if result.method != METHOD;
22
+
23
+ return decode(payload, key, result.byte_buffer_map['iv'])
24
+ end
25
+
26
+ def decode(input, key, iv)
27
+ begin
28
+ cipher = OpenSSL::Cipher.new(@crypt_method)
29
+ cipher.decrypt
30
+
31
+ cipher.key = key.bytes.pack('C*')
32
+ cipher.iv = iv.bytes.pack('C*')
33
+
34
+ return cipher.update(input) + cipher.final
35
+ rescue StandardError => e
36
+ raise DecryptError, "Decryption OpenSSL failed: #{e.message}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ require_relative "./AbstractSymmetricCrypt.rb"
2
+
3
+ class OpenSSLAEAD < AbstractSymmetricCrypt
4
+ METHOD = 0x0201
5
+
6
+ def initialize(tag: 16, crypt_method: "AES-256-GCM")
7
+ @tag= tag
8
+ @crypt_method = crypt_method
9
+ @crypt_iv = CryptMethodConstans::CRYPT_METHODS[crypt_method]
10
+ end
11
+
12
+ def decrypt_with_key(payload, key)
13
+ lengths = {"iv" => @crypt_iv, "tag" => @tag}
14
+ result = parse(payload, lengths)
15
+
16
+ raise DecryptError, 'Unrecognized payload' if result.method != METHOD;
17
+
18
+ return decode(
19
+ result.data,
20
+ key,
21
+ result.byte_buffer_map['iv'],
22
+ result.byte_buffer_map['tag']
23
+ )
24
+ end
25
+
26
+ private
27
+ def decode(input, key, iv, tag)
28
+ cipher = OpenSSL::Cipher.new(@crypt_method)
29
+ cipher.decrypt
30
+
31
+ cipher.key = key.bytes.pack('C*')
32
+ cipher.iv = iv.bytes.pack('C*')
33
+ cipher.auth_tag = tag.bytes.pack('C*')
34
+
35
+ return cipher.update(input) + cipher.final
36
+ rescue OpenSSL::Cipher::CipherError => e
37
+ raise DecryptError.new("Decryption failed: #{e.message}")
38
+ end
39
+
40
+
41
+ end
@@ -0,0 +1,88 @@
1
+ class PhpUnserializer
2
+ def initialize(data)
3
+ @data = data
4
+ @index = 0
5
+ end
6
+
7
+ def unserialize
8
+ type = @data[@index]
9
+ @index += 2
10
+
11
+ case type
12
+ when 'i' then parse_int
13
+ when 'd' then parse_float
14
+ when 'b' then parse_boolean
15
+ when 's' then parse_string
16
+ when 'a' then parse_array
17
+ when 'O' then parse_object
18
+ else
19
+ raise ArgumentError, "Unsupported type: #{type}"
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def parse_int
26
+ semi_colon_index = @data.index(';', @index)
27
+ int_str = @data[@index...semi_colon_index]
28
+ @index = semi_colon_index + 1
29
+ int_str.to_i
30
+ end
31
+
32
+ def parse_float
33
+ semi_colon_index = @data.index(';', @index)
34
+ float_str = @data[@index...semi_colon_index]
35
+ @index = semi_colon_index + 1
36
+ float_str.to_f
37
+ end
38
+
39
+ def parse_boolean
40
+ bool_char = @data[@index]
41
+ @index += 2
42
+ bool_char == '1'
43
+ end
44
+
45
+ def parse_string
46
+ colon_index = @data.index(':', @index)
47
+ length = @data[@index...colon_index].to_i
48
+ @index = colon_index + 2
49
+ str = @data[@index...@index + length]
50
+ @index += length + 2
51
+ str
52
+ end
53
+
54
+ def parse_array
55
+ colon_index = @data.index(':', @index)
56
+ length = @data[@index...colon_index].to_i
57
+ @index = colon_index + 2
58
+ map = {}
59
+ length.times do
60
+ key = unserialize
61
+ value = unserialize
62
+ map[key] = value
63
+ end
64
+ @index += 1
65
+ map
66
+ end
67
+
68
+ def parse_object
69
+ colon_index = @data.index(':', @index)
70
+ class_name_length = @data[@index...colon_index].to_i
71
+ @index = colon_index + 2
72
+ class_name = @data[@index...@index + class_name_length]
73
+ @index += class_name_length + 2
74
+
75
+ colon_index = @data.index(':', @index)
76
+ length = @data[@index...colon_index].to_i
77
+ @index = colon_index + 2
78
+ fields = {}
79
+ length.times do
80
+ key = unserialize
81
+ value = unserialize
82
+ fields[key] = value
83
+ end
84
+ @index += 1
85
+
86
+ { class_name => fields }
87
+ end
88
+ end
@@ -0,0 +1,14 @@
1
+ require 'rbnacl'
2
+
3
+ class Secretbox < AbstractSymmetricCrypt
4
+ METHOD = 0x0101
5
+
6
+ def decrypt_with_key(payload, key)
7
+ nonce_bytes = 24
8
+ parse = parse(payload, { iv: nonce_bytes })
9
+ secret_box = RbNaCl::SecretBox.new(key)
10
+ return secret_box.decrypt(parse.byte_buffer_map[:iv], parse.data)
11
+ rescue RbNaCl::CryptoError
12
+ raise 'Decryption failed'
13
+ end
14
+ end
@@ -0,0 +1,154 @@
1
+ class Signature5VerificationResult
2
+ # Zone-id
3
+ attr_accessor :zone_id
4
+ # Detection result as number, one of following: 0, 3, 6, 9
5
+ attr_accessor :result
6
+ # Detection result as text, one of following: ok, junk, proxy, bot
7
+ attr_accessor :verdict
8
+ # Visitor's User Agent
9
+ attr_accessor :visitor_user_agent
10
+ # Data
11
+ attr_accessor :data
12
+ # IPv4 address
13
+ attr_accessor :ipv4_ip
14
+ # Number of bytes required for IP matching
15
+ attr_accessor :ipv4_v
16
+ # IPv6 address
17
+ attr_accessor :ipv6_ip
18
+ # Number of left-most bytes of IPv6 address needed to match
19
+ attr_accessor :ipv6_v
20
+ # Number of CPU logical cores gathered from navigator.hardwareConcurrency
21
+ attr_accessor :cpu_cores
22
+ # Amount of RAM memory in GB gathered from navigator.deviceMemory
23
+ attr_accessor :ram
24
+ # Timezone offset from GMT in minutes
25
+ attr_accessor :tz_offset
26
+ # User-Agent Client Hints Platform
27
+ attr_accessor :b_platform
28
+ # Content of Sec-CH-UA-Platform-Version request header
29
+ attr_accessor :platform_v
30
+ # GPU Model obtained from WebGL and WebGPU APIs
31
+ attr_accessor :gpu
32
+ # Detected iPhone/iPad model by Adscore AppleSense
33
+ attr_accessor :apple_sense
34
+ # Physical screen horizontal resolution
35
+ attr_accessor :horizontal_resolution
36
+ # Physical screen vertical resolution
37
+ attr_accessor :vertical_resolution
38
+ # Adscore TrueUA-enriched User-Agent
39
+ attr_accessor :true_ua
40
+ # Adscore True Location Country
41
+ attr_accessor :true_ua_location
42
+ # Adscore True Location Confidence
43
+ attr_accessor :true_ua_location_c
44
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA
45
+ attr_accessor :truech_ua
46
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Arch
47
+ attr_accessor :truech_arch
48
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Bitness
49
+ attr_accessor :truech_bitness
50
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Model
51
+ attr_accessor :truech_model
52
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Platform
53
+ attr_accessor :truech_platform
54
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Platform-Version
55
+ attr_accessor :truech_platform_v
56
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Full-Version
57
+ attr_accessor :truech_full_v
58
+ # Adscore TrueUA-enriched Client Hints header Sec-CH-UA-Mobile
59
+ attr_accessor :truech_mobile
60
+ # Indicates whether visitor is using Private Browsing (Incognito) Mode
61
+ attr_accessor :incognito
62
+ # Adscore zone subId
63
+ attr_accessor :sub_id
64
+ # Request time
65
+ attr_accessor :request_time
66
+ # Signature time
67
+ attr_accessor :signature_time
68
+ # Signature time
69
+ attr_accessor :h_signature_time
70
+ # Token
71
+ attr_accessor :token
72
+ # Other, which has not been mapped to a field, or getting error during parsing
73
+ attr_accessor :additional_data
74
+
75
+ def initialize(hash)
76
+ @zone_id = hash.delete('zone_id')&.to_i
77
+ @result = hash.delete('result')&.to_i
78
+ @verdict = hash.delete('verdict')
79
+ @visitor_user_agent = hash.delete('b.ua')
80
+ @data = hash.delete('data')
81
+ @ipv4_ip = hash.delete('ipv4.ip')
82
+ @ipv4_v = hash.delete('ipv4.v')&.to_i
83
+ @ipv6_ip = hash.delete('ipv6.ip')
84
+ @ipv6_v = hash.delete('ipv6.v')&.to_i
85
+ @cpu_cores = hash.delete('b.cpucores')&.to_i
86
+ @ram = hash.delete('b.ram')&.to_i
87
+ @tz_offset = hash.delete('b.tzoffset')&.to_i
88
+ @b_platform = hash.delete('b.platform')
89
+ @platform_v = hash.delete('b.platform.v')
90
+ @gpu = hash.delete('b.gpu')
91
+ @apple_sense = hash.delete('apple_sense')
92
+ @horizontal_resolution = hash.delete('b.sr.w')&.to_i
93
+ @vertical_resolution = hash.delete('b.sr.h')&.to_i
94
+ @true_ua = hash.delete('b.trueua')
95
+ @true_ua_location = hash.delete('b.trueloc.c')
96
+ @true_ua_location_c = hash.delete('b.truech.location.c')&.to_i
97
+ @truech_ua = hash.delete('b.truech.ua')
98
+ @truech_arch = hash.delete('b.truech.arch')
99
+ @truech_bitness = hash.delete('b.truech.bitness')&.to_i
100
+ @truech_model = hash.delete('b.truech.model')
101
+ @truech_platform = hash.delete('b.truech.platform')
102
+ @truech_platform_v = hash.delete('b.truech.platform.v')
103
+ @truech_full_v = hash.delete('b.truech.full.v')
104
+ @truech_mobile = hash.delete('b.truech.mobile')
105
+ @incognito = hash.delete('incognito')
106
+ @sub_id = hash.delete('sub_id')
107
+ @request_time = hash.delete('requestTime')&.to_i
108
+ @h_signature_time = hash.delete('HsignatureTime')
109
+ @signature_time = hash.delete('signatureTime')
110
+ @token = hash.delete('token')
111
+ @additional_data = hash
112
+ end
113
+
114
+ def to_s
115
+ <<~STRING
116
+ Zone ID: #{@zone_id}
117
+ Result: #{@result}
118
+ Verdict: #{@verdict}
119
+ Visitor User Agent: #{@visitor_user_agent}
120
+ Data: #{@data}
121
+ IPv4 IP: #{@ipv4_ip}
122
+ IPv4 Version: #{@ipv4_v}
123
+ IPv6 IP: #{@ipv6_ip}
124
+ IPv6 Version: #{@ipv6_v}
125
+ CPU Cores: #{@cpu_cores}
126
+ RAM: #{@ram}
127
+ Time Zone Offset: #{@tz_offset}
128
+ Browser Platform: #{@b_platform}
129
+ Platform Version: #{@platform_v}
130
+ GPU: #{@gpu}
131
+ Apple Sense: #{@apple_sense}
132
+ Horizontal Resolution: #{@horizontal_resolution}
133
+ Vertical Resolution: #{@vertical_resolution}
134
+ True User Agent: #{@true_ua}
135
+ True User Agent Location: #{@true_ua_location}
136
+ True User Agent Location Code: #{@true_ua_location_c}
137
+ Truech User Agent: #{@truech_ua}
138
+ Truech Architecture: #{@truech_arch}
139
+ Truech Bitness: #{@truech_bitness}
140
+ Truech Model: #{@truech_model}
141
+ Truech Platform: #{@truech_platform}
142
+ Truech Platform Version: #{@truech_platform_v}
143
+ Truech Full Version: #{@truech_full_v}
144
+ Truech Mobile: #{@truech_mobile}
145
+ Incognito: #{@incognito}
146
+ Subscriber ID: #{@sub_id}
147
+ Request Time: #{@request_time}
148
+ Signature Time: #{@signature_time}
149
+ H Signature Time: #{@h_signature_time}
150
+ Token: #{@token}
151
+ Additional Data: #{@additional_data}
152
+ STRING
153
+ end
154
+ end