crussh 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +371 -0
- data/ext/poly1305/Cargo.toml +13 -0
- data/ext/poly1305/extconf.rb +6 -0
- data/ext/poly1305/src/lib.rs +75 -0
- data/lib/crussh/auth.rb +46 -0
- data/lib/crussh/channel/key_parser.rb +125 -0
- data/lib/crussh/channel.rb +381 -0
- data/lib/crussh/cipher/algorithm.rb +31 -0
- data/lib/crussh/cipher/chacha20poly1305.rb +98 -0
- data/lib/crussh/cipher.rb +25 -0
- data/lib/crussh/compression.rb +42 -0
- data/lib/crussh/gatekeeper.rb +50 -0
- data/lib/crussh/handler/line_buffer.rb +131 -0
- data/lib/crussh/handler.rb +128 -0
- data/lib/crussh/heartbeat.rb +68 -0
- data/lib/crussh/kex/algorithm.rb +86 -0
- data/lib/crussh/kex/curve25519.rb +30 -0
- data/lib/crussh/kex/exchange.rb +234 -0
- data/lib/crussh/kex.rb +42 -0
- data/lib/crussh/keys/key_pair.rb +61 -0
- data/lib/crussh/keys/public_key.rb +35 -0
- data/lib/crussh/keys.rb +70 -0
- data/lib/crussh/limits.rb +45 -0
- data/lib/crussh/logger.rb +95 -0
- data/lib/crussh/mac/algorithm.rb +23 -0
- data/lib/crussh/mac/crypto.rb +60 -0
- data/lib/crussh/mac/none.rb +9 -0
- data/lib/crussh/mac.rb +28 -0
- data/lib/crussh/negotiator.rb +41 -0
- data/lib/crussh/preferred.rb +16 -0
- data/lib/crussh/protocol/channel_close.rb +11 -0
- data/lib/crussh/protocol/channel_data.rb +12 -0
- data/lib/crussh/protocol/channel_eof.rb +11 -0
- data/lib/crussh/protocol/channel_extended_data.rb +13 -0
- data/lib/crussh/protocol/channel_failure.rb +11 -0
- data/lib/crussh/protocol/channel_open.rb +69 -0
- data/lib/crussh/protocol/channel_open_confirmation.rb +15 -0
- data/lib/crussh/protocol/channel_open_failure.rb +14 -0
- data/lib/crussh/protocol/channel_request.rb +146 -0
- data/lib/crussh/protocol/channel_success.rb +11 -0
- data/lib/crussh/protocol/channel_window_adjust.rb +12 -0
- data/lib/crussh/protocol/debug.rb +15 -0
- data/lib/crussh/protocol/disconnect.rb +39 -0
- data/lib/crussh/protocol/ext_info.rb +48 -0
- data/lib/crussh/protocol/global_request.rb +46 -0
- data/lib/crussh/protocol/ignore.rb +11 -0
- data/lib/crussh/protocol/kex_ecdh_init.rb +11 -0
- data/lib/crussh/protocol/kex_ecdh_reply.rb +13 -0
- data/lib/crussh/protocol/kex_init.rb +38 -0
- data/lib/crussh/protocol/new_keys.rb +9 -0
- data/lib/crussh/protocol/ping.rb +11 -0
- data/lib/crussh/protocol/pong.rb +11 -0
- data/lib/crussh/protocol/request_failure.rb +9 -0
- data/lib/crussh/protocol/request_success.rb +11 -0
- data/lib/crussh/protocol/service_accept.rb +11 -0
- data/lib/crussh/protocol/service_request.rb +11 -0
- data/lib/crussh/protocol/unimplemented.rb +11 -0
- data/lib/crussh/protocol/userauth_banner.rb +12 -0
- data/lib/crussh/protocol/userauth_failure.rb +12 -0
- data/lib/crussh/protocol/userauth_pk_ok.rb +12 -0
- data/lib/crussh/protocol/userauth_request.rb +52 -0
- data/lib/crussh/protocol/userauth_success.rb +9 -0
- data/lib/crussh/protocol.rb +135 -0
- data/lib/crussh/server/auth_handler.rb +18 -0
- data/lib/crussh/server/config.rb +157 -0
- data/lib/crussh/server/layers/connection.rb +363 -0
- data/lib/crussh/server/layers/transport.rb +49 -0
- data/lib/crussh/server/layers/userauth.rb +232 -0
- data/lib/crussh/server/request_rule.rb +76 -0
- data/lib/crussh/server/session.rb +192 -0
- data/lib/crussh/server.rb +214 -0
- data/lib/crussh/ssh_id.rb +44 -0
- data/lib/crussh/transport/packet_stream.rb +245 -0
- data/lib/crussh/transport/reader.rb +98 -0
- data/lib/crussh/transport/version_exchange.rb +26 -0
- data/lib/crussh/transport/writer.rb +72 -0
- data/lib/crussh/version.rb +5 -0
- data/lib/crussh.rb +61 -0
- data/sig/crussh.rbs +4 -0
- metadata +249 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
class Server
|
|
5
|
+
class << self
|
|
6
|
+
def configure
|
|
7
|
+
yield config
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def config
|
|
11
|
+
@config ||= Config.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def banner(text = nil, &block)
|
|
15
|
+
return @banner = block if block
|
|
16
|
+
|
|
17
|
+
@banner = text
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def read_banner
|
|
21
|
+
@banner.is_a?(Proc) ? @banner.call : @banner
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def authenticate(method, &block)
|
|
25
|
+
auth_handlers[method] = AuthHandler.new(block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def auth_handlers
|
|
29
|
+
@auth_handlers ||= {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def accept(*types, only: nil, except: nil, if: nil, unless: nil, &block)
|
|
33
|
+
rule = RequestRule.accept(
|
|
34
|
+
only: only,
|
|
35
|
+
except: except,
|
|
36
|
+
if: binding.local_variable_get(:if),
|
|
37
|
+
unless: binding.local_variable_get(:unless),
|
|
38
|
+
&block
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
types.each { |type| request_rules[type] = rule }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reject(*types)
|
|
45
|
+
rule = RequestRule.reject
|
|
46
|
+
|
|
47
|
+
types.each { |type| request_rules[type] = rule }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def request_rules
|
|
51
|
+
@request_rules ||= {}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle(type, handler = nil, &block)
|
|
55
|
+
handlers[type] = handler || block
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handlers
|
|
59
|
+
@handlers ||= {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inherited(subclass)
|
|
63
|
+
subclass.instance_variable_set(:@config, config.dup)
|
|
64
|
+
subclass.instance_variable_set(:@auth_handlers, auth_handlers.dup)
|
|
65
|
+
subclass.instance_variable_set(:@banner, @banner)
|
|
66
|
+
|
|
67
|
+
super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run(**options)
|
|
71
|
+
new(**options).run
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize(**config)
|
|
76
|
+
@config = self.class.config.dup
|
|
77
|
+
|
|
78
|
+
config.each do |key, value|
|
|
79
|
+
@config.public_send(:"#{key}=", value)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@config.validate!
|
|
83
|
+
|
|
84
|
+
@gatekeeper = Gatekeeper.new(
|
|
85
|
+
max_connections: @config.max_connections,
|
|
86
|
+
max_unauthenticated: @config.max_unauthenticated,
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
attr_reader :config, :gatekeeper
|
|
91
|
+
|
|
92
|
+
def run
|
|
93
|
+
Logger.info(self, "Starting server", host: config.host, port: config.port)
|
|
94
|
+
|
|
95
|
+
endpoint = IO::Endpoint.tcp(config.host, config.port)
|
|
96
|
+
|
|
97
|
+
Async do |task|
|
|
98
|
+
endpoint.bind do |server|
|
|
99
|
+
loop do
|
|
100
|
+
socket, address = server.accept
|
|
101
|
+
|
|
102
|
+
task.async do
|
|
103
|
+
handle_connection(socket, address)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def handle_auth(method, *args)
|
|
111
|
+
handler = self.class.auth_handlers[method]
|
|
112
|
+
return Auth.reject unless handler
|
|
113
|
+
|
|
114
|
+
handler.call(*args)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def auth_methods
|
|
118
|
+
self.class.auth_handlers.keys
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def banner
|
|
122
|
+
self.class.read_banner
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def accepts_channel?(type)
|
|
126
|
+
case type
|
|
127
|
+
when :session
|
|
128
|
+
has_handler?(:shell) || has_handler?(:exec) || has_handler?(:subsystem)
|
|
129
|
+
when :direct_tcpip
|
|
130
|
+
has_handler?(:direct_tcpip)
|
|
131
|
+
when :forwarded_tcpip
|
|
132
|
+
has_handler?(:forwarded_tcpip)
|
|
133
|
+
when :x11
|
|
134
|
+
has_handler?(:x11)
|
|
135
|
+
else
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def open_channel?(type, channel, ...)
|
|
141
|
+
method_name = :"open_#{type}?"
|
|
142
|
+
|
|
143
|
+
return true unless respond_to?(method_name)
|
|
144
|
+
|
|
145
|
+
send(method_name, channel, ...)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def accepts_request?(type, channel, **params)
|
|
149
|
+
rule = self.class.request_rules[type]
|
|
150
|
+
|
|
151
|
+
return type == :pty if rule.nil?
|
|
152
|
+
|
|
153
|
+
rule.allowed?(channel, **params)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def dispatch_handler(type, channel, session, *args)
|
|
157
|
+
handler_class_or_proc = self.class.handlers[type]
|
|
158
|
+
return false unless handler_class_or_proc
|
|
159
|
+
|
|
160
|
+
case handler_class_or_proc
|
|
161
|
+
when Class
|
|
162
|
+
handler = handler_class_or_proc.new(channel, session, *args)
|
|
163
|
+
handler.call
|
|
164
|
+
when Proc, Method
|
|
165
|
+
handler_class_or_proc.call(channel, session, *args)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def has_handler?(type)
|
|
172
|
+
self.class.handlers.key?(type)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def handle_connection(socket, address)
|
|
178
|
+
peer = format_address(address)
|
|
179
|
+
|
|
180
|
+
Logger.info(self, "New connection", peer:)
|
|
181
|
+
|
|
182
|
+
if @gatekeeper.block?
|
|
183
|
+
Logger.warn(self, "Connection rejected, limit reached", peer: peer, **@gatekeeper.stats)
|
|
184
|
+
socket.close
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if config.nodelay?
|
|
189
|
+
|
|
190
|
+
session = Session.new(socket, server: self)
|
|
191
|
+
session.start
|
|
192
|
+
|
|
193
|
+
Logger.info(self, "connection closed", peer:)
|
|
194
|
+
rescue => e
|
|
195
|
+
Logger.error(self, "Connection error", error: e.message)
|
|
196
|
+
ensure
|
|
197
|
+
@gatekeeper.disconnect!(was_authenticated: !session&.user.nil?)
|
|
198
|
+
begin
|
|
199
|
+
socket.close unless socket.closed?
|
|
200
|
+
rescue
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def format_address(address)
|
|
206
|
+
case address
|
|
207
|
+
when Addrinfo
|
|
208
|
+
"#{address.ip_address}:#{address.ip_port}"
|
|
209
|
+
else
|
|
210
|
+
address.to_s
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
class SshId
|
|
5
|
+
PROTO_VERSION = "2.0"
|
|
6
|
+
|
|
7
|
+
def initialize(software_version, comments: nil)
|
|
8
|
+
@software_version = software_version
|
|
9
|
+
@comments = comments
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def parse(line)
|
|
14
|
+
line = line.chomp
|
|
15
|
+
raise ProtocolError, "Invalid SSH identification: #{line.inspect}" unless line.start_with?("SSH-")
|
|
16
|
+
|
|
17
|
+
parts = line.split("-", 3)
|
|
18
|
+
raise ProtocolError, "Invalid SSH identification" if parts.length < 3
|
|
19
|
+
|
|
20
|
+
proto_version = parts[1]
|
|
21
|
+
unless proto_version == "2.0" || proto_version.start_with?("2.")
|
|
22
|
+
raise ProtocolError, "Unsupported SSH protocol version: #{proto_version}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
software_and_comments = parts[2]
|
|
26
|
+
software_version, comments = software_and_comments.split(" ", 2)
|
|
27
|
+
|
|
28
|
+
new(software_version, comments: comments)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :software_version, :comments
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
base = "SSH-#{PROTO_VERSION}-#{@software_version}"
|
|
36
|
+
base += " #{@comments}" if @comments
|
|
37
|
+
base
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serialize
|
|
41
|
+
"#{self}\r\n"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/stream"
|
|
4
|
+
|
|
5
|
+
module Crussh
|
|
6
|
+
module Transport
|
|
7
|
+
class PacketStream
|
|
8
|
+
MIN_PADDING = 4
|
|
9
|
+
MAX_PADDING = 255
|
|
10
|
+
MIN_PACKET_SIZE = 5
|
|
11
|
+
|
|
12
|
+
BLOCK_SIZE = 8
|
|
13
|
+
|
|
14
|
+
def initialize(socket, max_packet_size:)
|
|
15
|
+
@stream = IO::Stream(socket)
|
|
16
|
+
@reader = Reader.new(@stream, max_packet_size)
|
|
17
|
+
@writer = Writer.new(@stream)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def read = @reader.read
|
|
21
|
+
def write(...) = @writer.write(...)
|
|
22
|
+
|
|
23
|
+
def enable_encryption(opening_key, sealing_key)
|
|
24
|
+
@reader.enable_encryption(opening_key)
|
|
25
|
+
@writer.enable_encryption(sealing_key)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def enable_compression(read_compressor, write_compressor)
|
|
29
|
+
@reader.enable_compression(read_compressor)
|
|
30
|
+
@writer.enable_compression(write_compressor)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def last_read_sequence = @reader.last_sequence
|
|
34
|
+
def sequence_wrapped? = @reader.sequence_wrapped?
|
|
35
|
+
|
|
36
|
+
def reset_sequence
|
|
37
|
+
@reader.reset_sequence
|
|
38
|
+
@writer.reset_sequence
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Writer
|
|
42
|
+
def initialize(stream)
|
|
43
|
+
@stream = stream
|
|
44
|
+
@sequence = 0
|
|
45
|
+
@sealing_key = nil
|
|
46
|
+
|
|
47
|
+
@compressor = Compression::None.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def encrypted?
|
|
51
|
+
!@sealing_key.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def write(data)
|
|
55
|
+
data = @compressor.deflate(data)
|
|
56
|
+
|
|
57
|
+
if encrypted?
|
|
58
|
+
write_encrypted(data)
|
|
59
|
+
else
|
|
60
|
+
write_unencrypted(data)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@stream.flush
|
|
64
|
+
increment_sequence
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def enable_encryption(sealing_key)
|
|
68
|
+
@sealing_key = sealing_key
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def enable_compression(compressor)
|
|
72
|
+
@compressor = compressor
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reset_sequence
|
|
76
|
+
@sequence = 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def increment_sequence
|
|
82
|
+
@sequence = (@sequence + 1) & 0xFFFFFFFF
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write_encrypted(data)
|
|
86
|
+
padded = pad(data)
|
|
87
|
+
|
|
88
|
+
length_bytes = [padded.bytesize].pack("N")
|
|
89
|
+
encrypted_length = @sealing_key.encrypt_length(@sequence, length_bytes)
|
|
90
|
+
|
|
91
|
+
ciphertext, tag = @sealing_key.seal(@sequence, encrypted_length, padded)
|
|
92
|
+
|
|
93
|
+
@stream.write(encrypted_length + ciphertext + tag)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def write_unencrypted(data)
|
|
97
|
+
padded = pad(data)
|
|
98
|
+
packet = [padded.bytesize].pack("N") + padded
|
|
99
|
+
|
|
100
|
+
@stream.write(packet)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def pad(data)
|
|
104
|
+
data = data.b
|
|
105
|
+
payload_length = data.bytesize
|
|
106
|
+
|
|
107
|
+
min_total = 1 + payload_length + MIN_PADDING
|
|
108
|
+
min_total += 4 unless encrypted?
|
|
109
|
+
|
|
110
|
+
extra = (BLOCK_SIZE - (min_total % BLOCK_SIZE)) % BLOCK_SIZE
|
|
111
|
+
padding_length = MIN_PADDING + extra
|
|
112
|
+
|
|
113
|
+
padding_bytes = SecureRandom.random_bytes(padding_length)
|
|
114
|
+
|
|
115
|
+
[padding_length].pack("C") + data + padding_bytes
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class Reader
|
|
120
|
+
def initialize(stream, max_packet_size)
|
|
121
|
+
@stream = stream
|
|
122
|
+
@max_packet_size = max_packet_size
|
|
123
|
+
|
|
124
|
+
@sequence = 0
|
|
125
|
+
@last_sequence = 0
|
|
126
|
+
@wrapped = false
|
|
127
|
+
|
|
128
|
+
@compressor = Compression::None.new
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
attr_reader :last_sequence
|
|
132
|
+
|
|
133
|
+
def encrypted?
|
|
134
|
+
!@opening_key.nil?
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def read
|
|
138
|
+
@last_sequence = @sequence
|
|
139
|
+
|
|
140
|
+
result = if encrypted?
|
|
141
|
+
read_encrypted
|
|
142
|
+
else
|
|
143
|
+
read_unencrypted
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
increment_sequence
|
|
147
|
+
|
|
148
|
+
@compressor.inflate(result)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def enable_encryption(opening_key)
|
|
152
|
+
@opening_key = opening_key
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def enable_compression(compressor)
|
|
156
|
+
@compressor = compressor
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def reset_sequence
|
|
160
|
+
@sequence = 0
|
|
161
|
+
@last_sequence = 0
|
|
162
|
+
@wrapped = false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def sequence_wrapped? = @wrapped
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def increment_sequence
|
|
170
|
+
@wrapped = true if @sequence == 0xFFFFFFFF
|
|
171
|
+
@sequence = (@sequence + 1) & 0xFFFFFFFF
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def read_unencrypted
|
|
175
|
+
length_bytes = @stream.read_exactly(4)
|
|
176
|
+
packet_length = length_bytes.unpack1("N")
|
|
177
|
+
|
|
178
|
+
validate_packet_length!(packet_length)
|
|
179
|
+
|
|
180
|
+
data = @stream.read_exactly(packet_length)
|
|
181
|
+
|
|
182
|
+
unwrap(data)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def read_encrypted
|
|
186
|
+
encrypted_length = @stream.read_exactly(4)
|
|
187
|
+
|
|
188
|
+
length_bytes = @opening_key.decrypt_length(@sequence, encrypted_length)
|
|
189
|
+
packet_length = length_bytes.unpack1("N")
|
|
190
|
+
|
|
191
|
+
validate_packet_length!(packet_length)
|
|
192
|
+
|
|
193
|
+
ciphertext = @stream.read_exactly(packet_length)
|
|
194
|
+
tag = @stream.read_exactly(16)
|
|
195
|
+
|
|
196
|
+
plaintext = @opening_key.open(@sequence, encrypted_length, ciphertext, tag)
|
|
197
|
+
|
|
198
|
+
unwrap(plaintext)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def unwrap(data)
|
|
202
|
+
return if data.empty?
|
|
203
|
+
|
|
204
|
+
padding_length = data.getbyte(0)
|
|
205
|
+
|
|
206
|
+
validate_padding_length!(padding_length, data.bytesize)
|
|
207
|
+
|
|
208
|
+
payload_length = data.bytesize - padding_length - 1
|
|
209
|
+
|
|
210
|
+
validate_payload_length!(payload_length)
|
|
211
|
+
|
|
212
|
+
data.byteslice(1, payload_length)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def validate_packet_length!(packet_length)
|
|
216
|
+
if packet_length < MIN_PACKET_SIZE
|
|
217
|
+
raise PacketTooSmall, "Packet length #{packet_length} below minimum #{MIN_PACKET_SIZE}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
return if packet_length <= @max_packet_size
|
|
221
|
+
|
|
222
|
+
raise PacketTooLarge, "Packet length #{packet_length} exceeds maximum #{@max_packet_size}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def validate_padding_length!(padding_length, packet_length)
|
|
226
|
+
if padding_length < MIN_PADDING
|
|
227
|
+
raise InvalidPadding, "Padding length #{padding_length} below minimum #{MIN_PADDING}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if padding_length > MAX_PADDING
|
|
231
|
+
raise InvalidPadding, "Padding length #{padding_length} exceeds maximum #{MAX_PADDING}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
return if padding_length < packet_length
|
|
235
|
+
|
|
236
|
+
raise InvalidPadding, "Padding length #{padding_length} >= packet length #{packet_length}"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def validate_payload_length!(payload_length)
|
|
240
|
+
raise PacketTooSmall, "Invalid payload length: #{payload_length}" if payload_length.negative?
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Transport
|
|
5
|
+
class Reader
|
|
6
|
+
def initialize(data)
|
|
7
|
+
@data = data.b
|
|
8
|
+
@position = 0
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def byte
|
|
12
|
+
ensure_remaining!(1)
|
|
13
|
+
|
|
14
|
+
value = @data.getbyte(@position)
|
|
15
|
+
|
|
16
|
+
@position += 1
|
|
17
|
+
|
|
18
|
+
value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def boolean
|
|
22
|
+
byte != 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def uint32
|
|
26
|
+
ensure_remaining!(4)
|
|
27
|
+
|
|
28
|
+
value = @data[@position, 4].unpack1("N")
|
|
29
|
+
|
|
30
|
+
@position += 4
|
|
31
|
+
|
|
32
|
+
value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def string(max_length: nil)
|
|
36
|
+
length = uint32
|
|
37
|
+
|
|
38
|
+
raise PacketTooLarge, "String length #{length} exceeds maximum #{max_length}" if max_length && length > max_length
|
|
39
|
+
|
|
40
|
+
ensure_remaining!(length)
|
|
41
|
+
|
|
42
|
+
value = @data[@position, length]
|
|
43
|
+
|
|
44
|
+
@position += length
|
|
45
|
+
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def read(n)
|
|
50
|
+
ensure_remaining!(n)
|
|
51
|
+
|
|
52
|
+
value = @data[@position, n]
|
|
53
|
+
|
|
54
|
+
@position += n
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mpint
|
|
60
|
+
raw = string
|
|
61
|
+
|
|
62
|
+
return OpenSSL::BN.new(0) if raw.empty?
|
|
63
|
+
|
|
64
|
+
OpenSSL::BN.new(raw, 2)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def name_list
|
|
68
|
+
string.split(",")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def remaining
|
|
72
|
+
@data.byteslice(@position..)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def remaining_bytes
|
|
76
|
+
@data.bytesize - @position
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def skip(n)
|
|
80
|
+
ensure_remaining!(n)
|
|
81
|
+
|
|
82
|
+
@position += n
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def eof?
|
|
86
|
+
@position >= @data.bytesize
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def ensure_remaining!(n)
|
|
92
|
+
return if @position + n <= @data.bytesize
|
|
93
|
+
|
|
94
|
+
raise IncompletePacket, "Need #{n} bytes but only #{remaining_bytes} available"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Transport
|
|
5
|
+
class VersionExchange
|
|
6
|
+
MAX_VERSION_LENGTH = 255
|
|
7
|
+
|
|
8
|
+
def initialize(socket, server_id:)
|
|
9
|
+
@socket = socket
|
|
10
|
+
@server_id = server_id
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def exchange
|
|
14
|
+
@socket.write(@server_id.serialize)
|
|
15
|
+
|
|
16
|
+
loop do
|
|
17
|
+
line = @socket.readline(MAX_VERSION_LENGTH, chomp: true)
|
|
18
|
+
|
|
19
|
+
next unless line.start_with?("SSH-")
|
|
20
|
+
|
|
21
|
+
return SshId.parse(line)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Transport
|
|
5
|
+
class Writer
|
|
6
|
+
def initialize
|
|
7
|
+
@buffer = String.new(encoding: Encoding::BINARY, capacity: 256)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def byte(value)
|
|
11
|
+
@buffer << [value].pack("C")
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def boolean(value)
|
|
16
|
+
byte(value ? 1 : 0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def uint32(value)
|
|
20
|
+
@buffer << [value].pack("N")
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def string(value)
|
|
25
|
+
value = value.b if value.is_a?(String)
|
|
26
|
+
uint32(value.bytesize)
|
|
27
|
+
@buffer << value
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def name_list(names)
|
|
32
|
+
string(names.join(","))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def raw(bytes)
|
|
36
|
+
@buffer << bytes
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remaining(value)
|
|
41
|
+
raw(value.b)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mpint(bn)
|
|
46
|
+
bn = OpenSSL::BN.new(bn) if bn.is_a?(Integer)
|
|
47
|
+
|
|
48
|
+
return uint32(0) if bn.zero?
|
|
49
|
+
|
|
50
|
+
raw_bytes = bn.to_s(2)
|
|
51
|
+
|
|
52
|
+
# two's compliment moment
|
|
53
|
+
raw_bytes = "\u0000#{raw_bytes}" if raw_bytes.getbyte(0) & 0x80 != 0
|
|
54
|
+
|
|
55
|
+
string(raw_bytes)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_s
|
|
59
|
+
@buffer.dup
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def length
|
|
63
|
+
@buffer.bytesize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reset
|
|
67
|
+
@buffer.clear
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|