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,381 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
require "io/stream"
|
|
5
|
+
require "async/semaphore"
|
|
6
|
+
|
|
7
|
+
module Crussh
|
|
8
|
+
class Channel
|
|
9
|
+
DEFAULT_WINDOW_SIZE = 2 * 1024 * 1024
|
|
10
|
+
DEFAULT_MAX_PACKET_SIZE = 32_768
|
|
11
|
+
|
|
12
|
+
Data = ::Data.define(:data) do
|
|
13
|
+
def each_key(parser: KeyParser.new, &block)
|
|
14
|
+
return enum_for(:each_key, parser: parser) unless block_given?
|
|
15
|
+
|
|
16
|
+
parser.parse(data).each(&block)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
ExtendedData = ::Data.define(:data, :type)
|
|
20
|
+
WindowChange = ::Data.define(:width, :height, :pixel_width, :pixel_height)
|
|
21
|
+
Signal = ::Data.define(:name)
|
|
22
|
+
EOF = ::Data.define
|
|
23
|
+
Closed = ::Data.define
|
|
24
|
+
|
|
25
|
+
Pty = ::Data.define(:term, :width, :height, :pixel_width, :pixel_height, :modes)
|
|
26
|
+
|
|
27
|
+
def initialize(session:, id:, remote_id:, remote_window_size:, local_window_size:, max_packet_size:, buffer_size:)
|
|
28
|
+
@session = session
|
|
29
|
+
@id = id
|
|
30
|
+
@remote_id = remote_id
|
|
31
|
+
|
|
32
|
+
@reader = Reader.new(
|
|
33
|
+
session:,
|
|
34
|
+
remote_id:,
|
|
35
|
+
window_size: local_window_size,
|
|
36
|
+
buffer_size:,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@writer = Writer.new(
|
|
40
|
+
session:,
|
|
41
|
+
remote_id:,
|
|
42
|
+
max_packet_size:,
|
|
43
|
+
window_size: remote_window_size,
|
|
44
|
+
channel: self,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@pty = nil
|
|
48
|
+
@env = {}
|
|
49
|
+
@events = Async::Queue.new
|
|
50
|
+
@read_buffer = "".b
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :session, :id, :remote_id, :pty, :env
|
|
54
|
+
attr_writer :pty
|
|
55
|
+
|
|
56
|
+
def pty? = !@pty.nil?
|
|
57
|
+
def eof? = @reader.eof?
|
|
58
|
+
def closed? = @writer.closed?
|
|
59
|
+
|
|
60
|
+
def read(...) = @reader.read(...)
|
|
61
|
+
def readpartial(...) = @reader.readpartial(...)
|
|
62
|
+
def gets(...) = @reader.gets(...)
|
|
63
|
+
def each(&) = @reader.each(&)
|
|
64
|
+
|
|
65
|
+
def write(...) = @writer.write(...)
|
|
66
|
+
def puts(...) = @writer.puts(...)
|
|
67
|
+
def print(...) = @writer.print(...)
|
|
68
|
+
def flush = @writer.flush
|
|
69
|
+
|
|
70
|
+
def <<(data)
|
|
71
|
+
write(data)
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def send_eof = @writer.send_eof
|
|
76
|
+
def close = @writer.close
|
|
77
|
+
|
|
78
|
+
def stderr
|
|
79
|
+
@stderr ||= Stderr.new(session:, remote_id:)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def exit_status(code)
|
|
83
|
+
message = Protocol::ChannelRequest.new(recipient_channel: @remote_id, request_type: "exit-status", want_reply: false, request_data: [code].pack("N"))
|
|
84
|
+
@session.write_packet(message)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def exit_signal(signal_name, core_dumped: false, error_message: "", language: "")
|
|
88
|
+
writer = Transport::Writer.new
|
|
89
|
+
writer.string(signal_name)
|
|
90
|
+
writer.boolean(core_dumped)
|
|
91
|
+
writer.string(error_message)
|
|
92
|
+
writer.string(language)
|
|
93
|
+
|
|
94
|
+
message = Protocol::ChannelRequest.new(recipient_channel: @remote_id, request_type: "exit-signal", want_reply: false, request_data: writer.to_s)
|
|
95
|
+
@session.write_packet(message)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def push_event(event) = @reader.push_event(event)
|
|
99
|
+
def adjust_remote_window(bytes) = @writer.adjust_window(bytes)
|
|
100
|
+
|
|
101
|
+
def set_env(name, value)
|
|
102
|
+
@env[name] = value
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def update_window(window_change)
|
|
106
|
+
@pty = @pty.with(width: window_change.width, height: window_change.height, pixel_width: window_change.pixel_width, pixel_height: window_change.pixel_height) if @pty
|
|
107
|
+
push_event(window_change)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class Reader
|
|
111
|
+
def initialize(session:, remote_id:, window_size:, buffer_size:)
|
|
112
|
+
@session = session
|
|
113
|
+
@remote_id = remote_id
|
|
114
|
+
@window_size = window_size
|
|
115
|
+
@window_threshold = window_size / 2
|
|
116
|
+
@bytes_consumed = 0
|
|
117
|
+
|
|
118
|
+
@events = Async::LimitedQueue.new(buffer_size)
|
|
119
|
+
@buffer = "".b
|
|
120
|
+
|
|
121
|
+
@eof = false
|
|
122
|
+
@closed = false
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def eof? = @eof
|
|
126
|
+
def closed? = @closed
|
|
127
|
+
|
|
128
|
+
def push_event(event)
|
|
129
|
+
@events.enqueue(event)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def each
|
|
133
|
+
return enum_for(:each) unless block_given?
|
|
134
|
+
|
|
135
|
+
loop do
|
|
136
|
+
event = @events.dequeue
|
|
137
|
+
yield event
|
|
138
|
+
break if event.is_a?(Closed)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def read(length = nil)
|
|
143
|
+
return drain_buffer(length) if length && @buffer.bytesize >= length
|
|
144
|
+
|
|
145
|
+
loop do
|
|
146
|
+
event = @events.dequeue
|
|
147
|
+
|
|
148
|
+
case event
|
|
149
|
+
when Data(data:)
|
|
150
|
+
consume_window(data.bytesize)
|
|
151
|
+
@buffer << data
|
|
152
|
+
return drain_buffer(length) if length && @buffer.bytesize >= length
|
|
153
|
+
when EOF
|
|
154
|
+
@eof = true
|
|
155
|
+
|
|
156
|
+
return @buffer.empty? ? nil : drain_buffer(length)
|
|
157
|
+
when Closed
|
|
158
|
+
@closed = true
|
|
159
|
+
|
|
160
|
+
return @buffer.empty? ? nil : drain_buffer(length)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def readpartial(maxlen, outbuf = nil)
|
|
166
|
+
outbuf ||= "".b
|
|
167
|
+
outbuf.clear
|
|
168
|
+
|
|
169
|
+
if @buffer.bytesize > 0
|
|
170
|
+
outbuf << drain_buffer(maxlen)
|
|
171
|
+
return outbuf
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
loop do
|
|
175
|
+
event = @events.dequeue
|
|
176
|
+
|
|
177
|
+
case event
|
|
178
|
+
when Data(data:)
|
|
179
|
+
consume_window(data.bytesize)
|
|
180
|
+
@buffer << data
|
|
181
|
+
outbuf << drain_buffer(maxlen)
|
|
182
|
+
return outbuf
|
|
183
|
+
when EOF
|
|
184
|
+
@eof = true
|
|
185
|
+
raise ::EOFError, "end of file reached"
|
|
186
|
+
when Closed
|
|
187
|
+
@closed = true
|
|
188
|
+
raise ::IOError, "channel closed"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def gets(sep = $INPUT_RECORD_SEPARATOR, limit = nil)
|
|
194
|
+
loop do
|
|
195
|
+
if (index = @buffer.index(sep))
|
|
196
|
+
return @buffer.slice!(0, index + sep.bytesize)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
return drain_buffer if limit && @buffer.bytesize >= limit
|
|
200
|
+
|
|
201
|
+
event = @events.dequeue
|
|
202
|
+
|
|
203
|
+
case event
|
|
204
|
+
when Data
|
|
205
|
+
consume_window(event.data.bytesize)
|
|
206
|
+
@buffer << event.data
|
|
207
|
+
when EOF
|
|
208
|
+
@eof = true
|
|
209
|
+
return @buffer.empty? ? nil : @buffer.slice!(0..-1)
|
|
210
|
+
when Closed
|
|
211
|
+
@closed = true
|
|
212
|
+
return @buffer.empty? ? nil : @buffer.slice!(0..-1)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
def drain_buffer(length = nil)
|
|
220
|
+
@buffer.slice!(0, length || @buffer.bytesize)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def consume_window(bytes)
|
|
224
|
+
@bytes_consumed += bytes
|
|
225
|
+
|
|
226
|
+
return if @bytes_consumed < @window_threshold
|
|
227
|
+
|
|
228
|
+
message = Protocol::ChannelWindowAdjust.new(
|
|
229
|
+
recipient_channel: @remote_id,
|
|
230
|
+
bytes_to_add: @bytes_consumed,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@session.write_packet(message)
|
|
234
|
+
@bytes_consumed = 0
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
module WriteMethods
|
|
239
|
+
def line_ending
|
|
240
|
+
"\n"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def puts(*args)
|
|
244
|
+
if args.empty?
|
|
245
|
+
write(line_ending)
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
buffer = "".b
|
|
250
|
+
args.each do |arg|
|
|
251
|
+
line = arg.to_s
|
|
252
|
+
buffer << line
|
|
253
|
+
buffer << line_ending unless line.end_with?("\n")
|
|
254
|
+
end
|
|
255
|
+
write(buffer)
|
|
256
|
+
nil
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def print(*args)
|
|
260
|
+
args.each { |arg| write(arg.to_s) }
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def <<(data)
|
|
265
|
+
write(data)
|
|
266
|
+
self
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def flush = self
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
class Writer
|
|
273
|
+
include WriteMethods
|
|
274
|
+
|
|
275
|
+
def initialize(session:, remote_id:, max_packet_size:, window_size:, channel:)
|
|
276
|
+
@session = session
|
|
277
|
+
@remote_id = remote_id
|
|
278
|
+
@max_packet_size = max_packet_size
|
|
279
|
+
@window_size = window_size
|
|
280
|
+
@channel = channel
|
|
281
|
+
|
|
282
|
+
@window_condition = Async::Condition.new
|
|
283
|
+
@semaphore = Async::Semaphore.new(1)
|
|
284
|
+
|
|
285
|
+
@eof_sent = false
|
|
286
|
+
@closed = false
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def closed? = @closed
|
|
290
|
+
|
|
291
|
+
def line_ending
|
|
292
|
+
@channel.pty? ? "\r\n" : "\n"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def adjust_window(...) = @semaphore.acquire { adjust_window_inner(...) }
|
|
296
|
+
def write(...) = @semaphore.acquire { write_inner(...) }
|
|
297
|
+
def send_eof = @semaphore.acquire { send_eof_inner }
|
|
298
|
+
def close = @semaphore.acquire { close_inner }
|
|
299
|
+
|
|
300
|
+
private
|
|
301
|
+
|
|
302
|
+
def adjust_window_inner(bytes)
|
|
303
|
+
@window_size += bytes
|
|
304
|
+
@window_condition.signal
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def close_inner
|
|
308
|
+
return if @closed
|
|
309
|
+
|
|
310
|
+
send_eof_inner unless @eof_sent
|
|
311
|
+
@closed = true
|
|
312
|
+
|
|
313
|
+
message = Protocol::ChannelClose.new(recipient_channel: @remote_id)
|
|
314
|
+
@session.write_packet(message)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def send_eof_inner
|
|
318
|
+
return if @eof_sent
|
|
319
|
+
|
|
320
|
+
@eof_sent = true
|
|
321
|
+
|
|
322
|
+
message = Protocol::ChannelEof.new(recipient_channel: @remote_id)
|
|
323
|
+
@session.write_packet(message)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def write_inner(data)
|
|
327
|
+
raise ::IOError, "channel closed" if @closed
|
|
328
|
+
raise ::IOError, "EOF already sent" if @eof_sent
|
|
329
|
+
|
|
330
|
+
data = data.to_s.b
|
|
331
|
+
bytes_written = 0
|
|
332
|
+
|
|
333
|
+
while bytes_written < data.bytesize
|
|
334
|
+
@window_condition.wait if @window_size <= 0
|
|
335
|
+
|
|
336
|
+
chunk_size = [
|
|
337
|
+
@max_packet_size,
|
|
338
|
+
@window_size,
|
|
339
|
+
data.bytesize - bytes_written,
|
|
340
|
+
].min
|
|
341
|
+
chunk = data.byteslice(bytes_written, chunk_size)
|
|
342
|
+
|
|
343
|
+
message = Protocol::ChannelData.new(recipient_channel: @remote_id, data: chunk)
|
|
344
|
+
@session.write_packet(message)
|
|
345
|
+
|
|
346
|
+
@window_size -= chunk_size
|
|
347
|
+
bytes_written += chunk_size
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
bytes_written
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
class Stderr
|
|
355
|
+
include WriteMethods
|
|
356
|
+
|
|
357
|
+
def initialize(session:, remote_id:)
|
|
358
|
+
@session = session
|
|
359
|
+
@remote_id = remote_id
|
|
360
|
+
@semaphore = Async::Semaphore.new(1)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def write(...) = @semaphore.acquire { write_inner(...) }
|
|
364
|
+
|
|
365
|
+
private
|
|
366
|
+
|
|
367
|
+
def write_inner(data)
|
|
368
|
+
data = data.to_s.b
|
|
369
|
+
|
|
370
|
+
message = Protocol::ChannelExtendedData.new(
|
|
371
|
+
recipient_channel: @remote_id,
|
|
372
|
+
data_type_code: 1,
|
|
373
|
+
data: data,
|
|
374
|
+
)
|
|
375
|
+
@session.write_packet(message)
|
|
376
|
+
|
|
377
|
+
data.bytesize
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Cipher
|
|
5
|
+
class Algorithm
|
|
6
|
+
def key_length
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def nonce_length
|
|
11
|
+
0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def block_size
|
|
15
|
+
8
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def needs_mac?
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def make_opening_key(key:, nonce:, mac_key:, mac:)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def make_sealing_key(key:, nonce:, mac_key:, mac:)
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Cipher
|
|
5
|
+
class ChaCha20Poly1305 < Algorithm
|
|
6
|
+
KEY_LENGTH = 64
|
|
7
|
+
|
|
8
|
+
def key_length = KEY_LENGTH
|
|
9
|
+
def nonce_length = 0
|
|
10
|
+
def block_size = 8
|
|
11
|
+
def tag_length = 16
|
|
12
|
+
|
|
13
|
+
def needs_mac? = false
|
|
14
|
+
|
|
15
|
+
def make_opening_key(key:, nonce: nil, mac_key: nil, mac: nil)
|
|
16
|
+
OpeningKey.new(key)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def make_sealing_key(key:, nonce: nil, mac_key: nil, mac: nil)
|
|
20
|
+
SealingKey.new(key)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Key
|
|
24
|
+
def initialize(key)
|
|
25
|
+
raise ArgumentError, "Key must be exactly #{KEY_LENGTH} bytes" unless key.bytesize == KEY_LENGTH
|
|
26
|
+
|
|
27
|
+
@main_key = key[0, 32]
|
|
28
|
+
@header_key = key[32, 32]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def build_nonce(sequence)
|
|
34
|
+
"\x00\x00\x00\x00" + [sequence].pack("Q>")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def chacha20_cipher
|
|
38
|
+
cipher = OpenSSL::Cipher.new("chacha20")
|
|
39
|
+
cipher.encrypt
|
|
40
|
+
cipher
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def chacha20_block(key, nonce, counter)
|
|
44
|
+
cipher = chacha20_cipher
|
|
45
|
+
cipher.key = key
|
|
46
|
+
cipher.iv = [counter].pack("V") + nonce
|
|
47
|
+
cipher
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def generate_poly_key(sequence)
|
|
51
|
+
nonce = build_nonce(sequence)
|
|
52
|
+
cipher = chacha20_block(@main_key, nonce, 0)
|
|
53
|
+
cipher.update("\x00" * 64)[0, 32]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class OpeningKey < Key
|
|
58
|
+
def decrypt_length(sequence, encrypted_length)
|
|
59
|
+
nonce = build_nonce(sequence)
|
|
60
|
+
cipher = chacha20_block(@header_key, nonce, 0)
|
|
61
|
+
cipher.update(encrypted_length)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def open(sequence, encrypted_length, ciphertext, tag)
|
|
65
|
+
poly_key = generate_poly_key(sequence)
|
|
66
|
+
data = encrypted_length + ciphertext
|
|
67
|
+
|
|
68
|
+
unless Crypto::Poly1305.verify(poly_key, tag, data)
|
|
69
|
+
raise DecryptionError, "Poly1305 authentication failed"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
nonce = build_nonce(sequence)
|
|
73
|
+
cipher = chacha20_block(@main_key, nonce, 1)
|
|
74
|
+
cipher.update(ciphertext)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class SealingKey < Key
|
|
79
|
+
def encrypt_length(sequence, length_bytes)
|
|
80
|
+
nonce = build_nonce(sequence)
|
|
81
|
+
cipher = chacha20_block(@header_key, nonce, 0)
|
|
82
|
+
cipher.update(length_bytes)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def seal(sequence, encrypted_length, plaintext)
|
|
86
|
+
nonce = build_nonce(sequence)
|
|
87
|
+
cipher = chacha20_block(@main_key, nonce, 1)
|
|
88
|
+
ciphertext = cipher.update(plaintext)
|
|
89
|
+
|
|
90
|
+
poly_key = generate_poly_key(sequence)
|
|
91
|
+
tag = Crypto::Poly1305.auth(poly_key, encrypted_length + ciphertext)
|
|
92
|
+
|
|
93
|
+
[ciphertext, tag]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
module Cipher
|
|
5
|
+
CHACHA20_POLY1305 = "chacha20-poly1305@openssh.com"
|
|
6
|
+
|
|
7
|
+
DEFAULT = [CHACHA20_POLY1305]
|
|
8
|
+
|
|
9
|
+
ALL = [CHACHA20_POLY1305]
|
|
10
|
+
|
|
11
|
+
REGISTRY = {
|
|
12
|
+
CHACHA20_POLY1305 => ChaCha20Poly1305,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def from_name(name)
|
|
17
|
+
algorithm_class = REGISTRY[name]
|
|
18
|
+
|
|
19
|
+
raise UnknownAlgorithm, "Unknown cipher algorithm: #{name}" if algorithm_class.nil?
|
|
20
|
+
|
|
21
|
+
algorithm_class.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Crussh
|
|
6
|
+
module Compression
|
|
7
|
+
NONE = "none"
|
|
8
|
+
ZLIB = "zlib@openssh.com"
|
|
9
|
+
|
|
10
|
+
DEFAULT = [ZLIB, NONE].freeze
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def from_name(name)
|
|
14
|
+
case name
|
|
15
|
+
when NONE
|
|
16
|
+
None.new
|
|
17
|
+
when ZLIB
|
|
18
|
+
Zlib.new
|
|
19
|
+
else
|
|
20
|
+
raise UnknownAlgorithm, "Unknown compression: #{name}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Compressor
|
|
26
|
+
def deflate(data) = data
|
|
27
|
+
def inflate(data) = data
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class None < Compressor; end
|
|
31
|
+
|
|
32
|
+
class Zlib
|
|
33
|
+
def initialize
|
|
34
|
+
@deflator = ::Zlib::Deflate.new
|
|
35
|
+
@inflator = ::Zlib::Inflate.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def deflate(data) = @deflator.deflate(data, ::Zlib::SYNC_FLUSH)
|
|
39
|
+
def inflate(data) = @inflator.inflate(data)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Crussh
|
|
4
|
+
class Gatekeeper
|
|
5
|
+
def initialize(max_connections:, max_unauthenticated:)
|
|
6
|
+
@max_connections = max_connections
|
|
7
|
+
@max_unauthenticated = max_unauthenticated
|
|
8
|
+
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@total = 0
|
|
11
|
+
@unauthenticated = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :total, :unauthenticated
|
|
15
|
+
|
|
16
|
+
def block? = !allowed?
|
|
17
|
+
|
|
18
|
+
def authenticate!
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
@unauthenticated -= 1 if @unauthenticated > 0
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def disconnect!(was_authenticated:)
|
|
25
|
+
@mutex.synchronize do
|
|
26
|
+
@total -= 1 if @total > 0
|
|
27
|
+
@unauthenticated -= 1 if !was_authenticated && @unauthenticated > 0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stats
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
{ total: @total, unauthenticated: @unauthenticated }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def allowed?
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
return false if @max_connections && @total >= @max_connections
|
|
42
|
+
return false if @max_unauthenticated && @unauthenticated >= @max_unauthenticated
|
|
43
|
+
|
|
44
|
+
@total += 1
|
|
45
|
+
@unauthenticated += 1
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|