tttls1.3 0.1.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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.travis.yml +8 -0
  6. data/Gemfile +13 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +52 -0
  9. data/Rakefile +133 -0
  10. data/example/helper.rb +17 -0
  11. data/example/https_client.rb +32 -0
  12. data/example/https_client_using_0rtt.rb +64 -0
  13. data/example/https_client_using_hrr.rb +35 -0
  14. data/example/https_client_using_ticket.rb +56 -0
  15. data/lib/tttls1.3/cipher_suites.rb +102 -0
  16. data/lib/tttls1.3/client.rb +745 -0
  17. data/lib/tttls1.3/connection.rb +380 -0
  18. data/lib/tttls1.3/cryptograph/aead.rb +118 -0
  19. data/lib/tttls1.3/cryptograph/passer.rb +22 -0
  20. data/lib/tttls1.3/cryptograph.rb +3 -0
  21. data/lib/tttls1.3/error.rb +22 -0
  22. data/lib/tttls1.3/key_schedule.rb +242 -0
  23. data/lib/tttls1.3/message/alert.rb +86 -0
  24. data/lib/tttls1.3/message/application_data.rb +27 -0
  25. data/lib/tttls1.3/message/certificate.rb +121 -0
  26. data/lib/tttls1.3/message/certificate_verify.rb +59 -0
  27. data/lib/tttls1.3/message/change_cipher_spec.rb +26 -0
  28. data/lib/tttls1.3/message/client_hello.rb +100 -0
  29. data/lib/tttls1.3/message/encrypted_extensions.rb +65 -0
  30. data/lib/tttls1.3/message/end_of_early_data.rb +29 -0
  31. data/lib/tttls1.3/message/extension/alpn.rb +70 -0
  32. data/lib/tttls1.3/message/extension/cookie.rb +47 -0
  33. data/lib/tttls1.3/message/extension/early_data_indication.rb +58 -0
  34. data/lib/tttls1.3/message/extension/key_share.rb +236 -0
  35. data/lib/tttls1.3/message/extension/pre_shared_key.rb +205 -0
  36. data/lib/tttls1.3/message/extension/psk_key_exchange_modes.rb +54 -0
  37. data/lib/tttls1.3/message/extension/record_size_limit.rb +46 -0
  38. data/lib/tttls1.3/message/extension/server_name.rb +91 -0
  39. data/lib/tttls1.3/message/extension/signature_algorithms.rb +69 -0
  40. data/lib/tttls1.3/message/extension/signature_algorithms_cert.rb +25 -0
  41. data/lib/tttls1.3/message/extension/status_request.rb +106 -0
  42. data/lib/tttls1.3/message/extension/supported_groups.rb +145 -0
  43. data/lib/tttls1.3/message/extension/supported_versions.rb +98 -0
  44. data/lib/tttls1.3/message/extension/unknown_extension.rb +38 -0
  45. data/lib/tttls1.3/message/extensions.rb +173 -0
  46. data/lib/tttls1.3/message/finished.rb +44 -0
  47. data/lib/tttls1.3/message/new_session_ticket.rb +89 -0
  48. data/lib/tttls1.3/message/record.rb +232 -0
  49. data/lib/tttls1.3/message/server_hello.rb +116 -0
  50. data/lib/tttls1.3/message.rb +48 -0
  51. data/lib/tttls1.3/sequence_number.rb +31 -0
  52. data/lib/tttls1.3/signature_scheme.rb +31 -0
  53. data/lib/tttls1.3/transcript.rb +69 -0
  54. data/lib/tttls1.3/utils.rb +91 -0
  55. data/lib/tttls1.3/version.rb +5 -0
  56. data/lib/tttls1.3.rb +16 -0
  57. data/spec/aead_spec.rb +95 -0
  58. data/spec/alert_spec.rb +54 -0
  59. data/spec/alpn_spec.rb +55 -0
  60. data/spec/application_data_spec.rb +26 -0
  61. data/spec/certificate_spec.rb +55 -0
  62. data/spec/certificate_verify_spec.rb +51 -0
  63. data/spec/change_cipher_spec_spec.rb +26 -0
  64. data/spec/cipher_suites_spec.rb +39 -0
  65. data/spec/client_hello_spec.rb +83 -0
  66. data/spec/client_spec.rb +319 -0
  67. data/spec/connection_spec.rb +114 -0
  68. data/spec/cookie_spec.rb +98 -0
  69. data/spec/early_data_indication_spec.rb +64 -0
  70. data/spec/encrypted_extensions_spec.rb +94 -0
  71. data/spec/error_spec.rb +18 -0
  72. data/spec/extensions_spec.rb +170 -0
  73. data/spec/finished_spec.rb +55 -0
  74. data/spec/key_schedule_spec.rb +198 -0
  75. data/spec/key_share_spec.rb +199 -0
  76. data/spec/new_session_ticket_spec.rb +80 -0
  77. data/spec/pre_shared_key_spec.rb +167 -0
  78. data/spec/psk_key_exchange_modes_spec.rb +45 -0
  79. data/spec/record_size_limit_spec.rb +61 -0
  80. data/spec/record_spec.rb +105 -0
  81. data/spec/server_hello_spec.rb +101 -0
  82. data/spec/server_name_spec.rb +110 -0
  83. data/spec/signature_algorithms_cert_spec.rb +73 -0
  84. data/spec/signature_algorithms_spec.rb +100 -0
  85. data/spec/spec_helper.rb +872 -0
  86. data/spec/status_request_spec.rb +73 -0
  87. data/spec/supported_groups_spec.rb +79 -0
  88. data/spec/supported_versions_spec.rb +136 -0
  89. data/spec/transcript_spec.rb +69 -0
  90. data/spec/unknown_extension_spec.rb +90 -0
  91. data/spec/utils_spec.rb +215 -0
  92. data/tttls1.3.gemspec +25 -0
  93. metadata +197 -0
data/spec/aead_spec.rb ADDED
@@ -0,0 +1,95 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+
6
+ RSpec.describe Aead do
7
+ context 'aead using CipherSuite::TLS_AES_128_GCM_SHA256' do
8
+ let(:cipher) do
9
+ Aead.new(cipher_suite: CipherSuite::TLS_AES_128_GCM_SHA256,
10
+ write_key: TESTBINARY_SERVER_PARAMETERS_WRITE_KEY,
11
+ write_iv: TESTBINARY_SERVER_PARAMETERS_WRITE_IV,
12
+ sequence_number: SequenceNumber.new)
13
+ end
14
+
15
+ let(:content) do
16
+ TESTBINARY_SERVER_PARAMETERS
17
+ end
18
+
19
+ let(:encrypted_record) do
20
+ TESTBINARY_SERVER_PARAMETERS_RECORD[5..]
21
+ end
22
+
23
+ let(:record_header) do
24
+ TESTBINARY_SERVER_PARAMETERS_RECORD[0...5]
25
+ end
26
+
27
+ it 'should encrypt content of server parameters' do
28
+ expect(cipher.encrypt(content, ContentType::HANDSHAKE))
29
+ .to eq encrypted_record
30
+ end
31
+
32
+ it 'should decrypt encrypted_record server parameters' do
33
+ expect(cipher.decrypt(encrypted_record, record_header))
34
+ .to eq [content, ContentType::HANDSHAKE]
35
+ end
36
+ end
37
+
38
+ context 'aead using CipherSuite::TLS_AES_128_GCM_SHA256' do
39
+ let(:cipher) do
40
+ Aead.new(cipher_suite: CipherSuite::TLS_AES_128_GCM_SHA256,
41
+ write_key: TESTBINARY_CLIENT_FINISHED_WRITE_KEY,
42
+ write_iv: TESTBINARY_CLIENT_FINISHED_WRITE_IV,
43
+ sequence_number: SequenceNumber.new)
44
+ end
45
+
46
+ let(:content) do
47
+ TESTBINARY_CLIENT_FINISHED
48
+ end
49
+
50
+ let(:encrypted_record) do
51
+ TESTBINARY_CLIENT_FINISHED_RECORD[5..]
52
+ end
53
+
54
+ let(:record_header) do
55
+ TESTBINARY_CLIENT_FINISHED_RECORD[0...5]
56
+ end
57
+
58
+ it 'should encrypt content of client finished' do
59
+ expect(cipher.encrypt(content, ContentType::HANDSHAKE))
60
+ .to eq encrypted_record
61
+ end
62
+
63
+ it 'should decrypt encrypted_record client finished' do
64
+ expect(cipher.decrypt(encrypted_record, record_header))
65
+ .to eq [content, ContentType::HANDSHAKE]
66
+ end
67
+ end
68
+
69
+ context 'aead using CipherSuite::TLS_AES_128_GCM_SHA256, ' \
70
+ 'HelloRetryRequest,' do
71
+ let(:cipher) do
72
+ Aead.new(cipher_suite: CipherSuite::TLS_AES_128_GCM_SHA256,
73
+ write_key: TESTBINARY_HRR_SERVER_PARAMETERS_WRITE_KEY,
74
+ write_iv: TESTBINARY_HRR_SERVER_PARAMETERS_WRITE_IV,
75
+ sequence_number: SequenceNumber.new)
76
+ end
77
+
78
+ let(:content) do
79
+ TESTBINARY_HRR_SERVER_PARAMETERS
80
+ end
81
+
82
+ let(:encrypted_record) do
83
+ TESTBINARY_HRR_SERVER_PARAMETERS_RECORD[5..]
84
+ end
85
+
86
+ let(:record_header) do
87
+ TESTBINARY_HRR_SERVER_PARAMETERS_RECORD[0...5]
88
+ end
89
+
90
+ it 'should decrypt encrypted_record server parameters' do
91
+ expect(cipher.decrypt(encrypted_record, record_header))
92
+ .to eq [content, ContentType::HANDSHAKE]
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,54 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+
6
+ RSpec.describe Alert do
7
+ context 'unexpected_message alert' do
8
+ let(:message) do
9
+ Alert.new(level: AlertLevel::FATAL,
10
+ description: ALERT_DESCRIPTION[:unexpected_message])
11
+ end
12
+
13
+ it 'should be serialized' do
14
+ expect(message.serialize).to eq AlertLevel::FATAL \
15
+ + ALERT_DESCRIPTION[:unexpected_message]
16
+ end
17
+
18
+ it 'should return error' do
19
+ expect(message.to_error).to be_a_kind_of(ErrorAlerts)
20
+ expect(message.to_error.message).to eq 'unexpected_message'
21
+ end
22
+ end
23
+
24
+ context 'unexpected_message alert, not given level' do
25
+ let(:message) do
26
+ Alert.new(description: ALERT_DESCRIPTION[:unexpected_message])
27
+ end
28
+
29
+ it 'should be serialized' do
30
+ expect(message.serialize).to eq AlertLevel::FATAL \
31
+ + ALERT_DESCRIPTION[:unexpected_message]
32
+ end
33
+
34
+ it 'should return error' do
35
+ expect(message.to_error).to be_a_kind_of(ErrorAlerts)
36
+ expect(message.to_error.message).to eq 'unexpected_message'
37
+ end
38
+ end
39
+
40
+ context 'valid alert binary' do
41
+ let(:message) do
42
+ Alert.deserialize(TESTBINARY_ALERT)
43
+ end
44
+
45
+ it 'should generate object' do
46
+ expect(message.level).to eq AlertLevel::WARNING
47
+ expect(message.description).to eq ALERT_DESCRIPTION[:close_notify]
48
+ end
49
+
50
+ it 'should generate serializable object' do
51
+ expect(message.serialize).to eq TESTBINARY_ALERT
52
+ end
53
+ end
54
+ end
data/spec/alpn_spec.rb ADDED
@@ -0,0 +1,55 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+ using Refinements
6
+
7
+ RSpec.describe Alpn do
8
+ context 'valid alpn' do
9
+ let(:protocol_name_list) do
10
+ ['h2', 'http/1.1', 'http/1.0']
11
+ end
12
+
13
+ let(:extension) do
14
+ Alpn.new(protocol_name_list)
15
+ end
16
+
17
+ it 'should be generated' do
18
+ expect(extension.extension_type)
19
+ .to eq ExtensionType::APPLICATION_LAYER_PROTOCOL_NEGOTIATION
20
+ expect(extension.protocol_name_list).to eq protocol_name_list
21
+ end
22
+
23
+ it 'should be serialized' do
24
+ expect(extension.serialize)
25
+ .to eq ExtensionType::APPLICATION_LAYER_PROTOCOL_NEGOTIATION \
26
+ + 23.to_uint16 \
27
+ + 21.to_uint16 \
28
+ + 'h2'.prefix_uint8_length \
29
+ + 'http/1.1'.prefix_uint8_length \
30
+ + 'http/1.0'.prefix_uint8_length
31
+ end
32
+ end
33
+
34
+ context 'invalid alpn, empty,' do
35
+ let(:extension) do
36
+ Alpn.new([])
37
+ end
38
+
39
+ it 'should not be generated' do
40
+ expect { extension }.to raise_error(ErrorAlerts)
41
+ end
42
+ end
43
+
44
+ context 'valid alpn binary' do
45
+ let(:extension) do
46
+ Alpn.deserialize(TESTBINARY_ALPN)
47
+ end
48
+
49
+ it 'should generate valid object' do
50
+ expect(extension.extension_type)
51
+ .to eq ExtensionType::APPLICATION_LAYER_PROTOCOL_NEGOTIATION
52
+ expect(extension.protocol_name_list).to eq ['h2', 'http/1.1']
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+
6
+ RSpec.describe ApplicationData do
7
+ context 'application_data' do
8
+ let(:message) do
9
+ ApplicationData.new(TESTBINARY_CLIENT_APPLICATION_DATA)
10
+ end
11
+
12
+ it 'should be serialized' do
13
+ expect(message.serialize).to eq TESTBINARY_CLIENT_APPLICATION_DATA
14
+ end
15
+ end
16
+
17
+ context 'valid application_data binary' do
18
+ let(:message) do
19
+ ApplicationData.deserialize(TESTBINARY_CLIENT_APPLICATION_DATA)
20
+ end
21
+
22
+ it 'should generate valid serializable object' do
23
+ expect(message.serialize).to eq TESTBINARY_CLIENT_APPLICATION_DATA
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+ using Refinements
6
+
7
+ RSpec.describe Certificate do
8
+ context 'valid certificate' do
9
+ let(:certificate) do
10
+ OpenSSL::X509::Certificate.new(File.read(__dir__ + '/../tmp/server.crt'))
11
+ end
12
+
13
+ let(:message) do
14
+ Certificate.new(certificate_list: [CertificateEntry.new(certificate)])
15
+ end
16
+
17
+ it 'should be generated' do
18
+ expect(message.msg_type).to eq HandshakeType::CERTIFICATE
19
+ expect(message.certificate_request_context).to be_empty
20
+
21
+ certificate_entry = message.certificate_list.first
22
+ expect(certificate_entry.cert_data.subject.to_s).to eq '/CN=localhost'
23
+ expect(certificate_entry.extensions).to be_empty
24
+ end
25
+
26
+ it 'should be serialized' do
27
+ expect(message.serialize).to eq HandshakeType::CERTIFICATE \
28
+ + 990.to_uint24 \
29
+ + 0.to_uint8 \
30
+ + 986.to_uint24 \
31
+ + 981.to_uint24 \
32
+ + certificate.to_der \
33
+ + 0.to_uint16
34
+ end
35
+ end
36
+
37
+ context 'valid certificate binary' do
38
+ let(:message) do
39
+ Certificate.deserialize(TESTBINARY_CERTIFICATE)
40
+ end
41
+
42
+ it 'should generate valid object' do
43
+ expect(message.msg_type).to eq HandshakeType::CERTIFICATE
44
+ expect(message.certificate_request_context).to be_empty
45
+
46
+ certificate_entry = message.certificate_list.first
47
+ expect(certificate_entry.cert_data.subject.to_s).to eq '/CN=rsa'
48
+ expect(certificate_entry.extensions).to be_empty
49
+ end
50
+
51
+ it 'should generate serializable object' do
52
+ expect(message.serialize).to eq TESTBINARY_CERTIFICATE
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+ using Refinements
6
+
7
+ RSpec.describe CertificateVerify do
8
+ context 'valid certificate_verify' do
9
+ let(:signature) do
10
+ OpenSSL::Random.random_bytes(128)
11
+ end
12
+
13
+ let(:message) do
14
+ CertificateVerify.new(
15
+ signature_scheme: SignatureScheme::RSA_PSS_RSAE_SHA256,
16
+ signature: signature
17
+ )
18
+ end
19
+
20
+ it 'should be generated' do
21
+ expect(message.msg_type).to eq HandshakeType::CERTIFICATE_VERIFY
22
+ expect(message.signature_scheme) \
23
+ .to eq SignatureScheme::RSA_PSS_RSAE_SHA256
24
+ expect(message.signature).to eq signature
25
+ end
26
+
27
+ it 'should be serialized' do
28
+ expect(message.serialize).to eq HandshakeType::CERTIFICATE_VERIFY \
29
+ + 132.to_uint24 \
30
+ + SignatureScheme::RSA_PSS_RSAE_SHA256 \
31
+ + signature.prefix_uint16_length
32
+ end
33
+ end
34
+
35
+ context 'valid certificate_verify binary' do
36
+ let(:message) do
37
+ CertificateVerify.deserialize(TESTBINARY_CERTIFICATE_VERIFY)
38
+ end
39
+
40
+ it 'should generate valid object' do
41
+ expect(message.msg_type).to eq HandshakeType::CERTIFICATE_VERIFY
42
+ expect(message.signature_scheme) \
43
+ .to eq SignatureScheme::RSA_PSS_RSAE_SHA256
44
+ expect(message.signature.length).to eq 128
45
+ end
46
+
47
+ it 'should generate serializable object' do
48
+ expect(message.serialize).to eq TESTBINARY_CERTIFICATE_VERIFY
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+
6
+ RSpec.describe ChangeCipherSpec do
7
+ context 'change_cipher_spec' do
8
+ let(:message) do
9
+ ChangeCipherSpec.new
10
+ end
11
+
12
+ it 'should be serialized' do
13
+ expect(message.serialize).to eq "\x01"
14
+ end
15
+ end
16
+
17
+ context 'valid change_cipher_spec binary' do
18
+ let(:message) do
19
+ ChangeCipherSpec.deserialize(TESTBINARY_CHANGE_CIPHER_SPEC)
20
+ end
21
+
22
+ it 'should generate valid serializable object' do
23
+ expect(message.serialize).to eq "\x01"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+ using Refinements
6
+
7
+ RSpec.describe CipherSuites do
8
+ context 'valid cipher suites binary' do
9
+ let(:cs) do
10
+ CipherSuites.deserialize(TESTBINARY_CIPHER_SUITES)
11
+ end
12
+
13
+ it 'should generate valid object' do
14
+ expect(cs).to eq [CipherSuite::TLS_AES_256_GCM_SHA384,
15
+ CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
16
+ CipherSuite::TLS_AES_128_GCM_SHA256]
17
+ end
18
+ end
19
+
20
+ context 'invalid cipher suites binary, too short' do
21
+ let(:cs) do
22
+ CipherSuites.deserialize(TESTBINARY_CIPHER_SUITES[0...-1])
23
+ end
24
+
25
+ it 'should not generate object' do
26
+ expect { cs }.to raise_error(ErrorAlerts)
27
+ end
28
+ end
29
+
30
+ context 'invalid cipher suites binary, binary is nil' do
31
+ let(:cs) do
32
+ CipherSuites.deserialize(nil)
33
+ end
34
+
35
+ it 'should not generate object' do
36
+ expect { cs }.to raise_error(ErrorAlerts)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,83 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'spec_helper'
5
+ using Refinements
6
+
7
+ RSpec.describe ClientHello do
8
+ context 'default client_hello' do
9
+ let(:random) do
10
+ OpenSSL::Random.random_bytes(32)
11
+ end
12
+
13
+ let(:legacy_session_id) do
14
+ Array.new(32, 0).map(&:chr).join
15
+ end
16
+
17
+ let(:cipher_suites) do
18
+ CipherSuites.new([TLS_AES_256_GCM_SHA384,
19
+ TLS_CHACHA20_POLY1305_SHA256,
20
+ TLS_AES_128_GCM_SHA256])
21
+ end
22
+
23
+ let(:message) do
24
+ ClientHello.new(random: random,
25
+ legacy_session_id: legacy_session_id,
26
+ cipher_suites: cipher_suites)
27
+ end
28
+
29
+ it 'should be generated' do
30
+ expect(message.msg_type).to eq HandshakeType::CLIENT_HELLO
31
+ expect(message.legacy_version).to eq ProtocolVersion::TLS_1_2
32
+ expect(message.random).to eq random
33
+ expect(message.legacy_session_id).to eq legacy_session_id
34
+ expect(message.cipher_suites).to eq [TLS_AES_256_GCM_SHA384,
35
+ TLS_CHACHA20_POLY1305_SHA256,
36
+ TLS_AES_128_GCM_SHA256]
37
+ expect(message.legacy_compression_methods).to eq ["\x00"]
38
+ expect(message.extensions).to be_empty
39
+ end
40
+
41
+ it 'should be serialized' do
42
+ expect(message.serialize).to eq HandshakeType::CLIENT_HELLO \
43
+ + 79.to_uint24 \
44
+ + ProtocolVersion::TLS_1_2 \
45
+ + random \
46
+ + legacy_session_id.length.to_uint8 \
47
+ + legacy_session_id \
48
+ + cipher_suites.serialize \
49
+ + "\x01\x00" \
50
+ + Extensions.new.serialize
51
+ end
52
+ end
53
+
54
+ context 'valid client_hello binary' do
55
+ let(:message) do
56
+ ClientHello.deserialize(TESTBINARY_CLIENT_HELLO)
57
+ end
58
+
59
+ it 'should generate valid object' do
60
+ expect(message.msg_type).to eq HandshakeType::CLIENT_HELLO
61
+ expect(message.legacy_version).to eq ProtocolVersion::TLS_1_2
62
+ end
63
+
64
+ it 'should generate valid serializable object' do
65
+ expect(message.serialize).to eq TESTBINARY_CLIENT_HELLO
66
+ end
67
+ end
68
+
69
+ context 'valid client_hello binary, 0-RTT,' do
70
+ let(:message) do
71
+ ClientHello.deserialize(TESTBINARY_0_RTT_CLIENT_HELLO)
72
+ end
73
+
74
+ it 'should generate valid object' do
75
+ expect(message.msg_type).to eq HandshakeType::CLIENT_HELLO
76
+ expect(message.legacy_version).to eq ProtocolVersion::TLS_1_2
77
+ end
78
+
79
+ it 'should generate valid serializable object' do
80
+ expect(message.serialize).to eq TESTBINARY_0_RTT_CLIENT_HELLO
81
+ end
82
+ end
83
+ end