net-ssh 7.1.0 → 7.3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/workflows/ci.yml +10 -3
  5. data/.gitignore +2 -0
  6. data/.rubocop_todo.yml +2 -2
  7. data/CHANGES.txt +24 -0
  8. data/DEVELOPMENT.md +23 -0
  9. data/Dockerfile +4 -2
  10. data/Gemfile.norbnacl +12 -0
  11. data/README.md +24 -18
  12. data/Rakefile +51 -18
  13. data/docker-compose.yml +2 -0
  14. data/lib/net/ssh/authentication/ed25519.rb +2 -4
  15. data/lib/net/ssh/authentication/key_manager.rb +19 -2
  16. data/lib/net/ssh/authentication/methods/publickey.rb +1 -1
  17. data/lib/net/ssh/authentication/pub_key_fingerprint.rb +1 -1
  18. data/lib/net/ssh/authentication/session.rb +7 -0
  19. data/lib/net/ssh/buffered_io.rb +1 -1
  20. data/lib/net/ssh/known_hosts.rb +2 -3
  21. data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
  22. data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
  23. data/lib/net/ssh/transport/algorithms.rb +35 -6
  24. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +117 -0
  25. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +17 -0
  26. data/lib/net/ssh/transport/cipher_factory.rb +28 -1
  27. data/lib/net/ssh/transport/gcm_cipher.rb +207 -0
  28. data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
  29. data/lib/net/ssh/transport/identity_cipher.rb +8 -0
  30. data/lib/net/ssh/transport/openssl_cipher_extensions.rb +8 -0
  31. data/lib/net/ssh/transport/packet_stream.rb +44 -23
  32. data/lib/net/ssh/transport/state.rb +1 -1
  33. data/lib/net/ssh/version.rb +1 -1
  34. data/lib/net/ssh.rb +5 -2
  35. data/net-ssh-public_cert.pem +19 -18
  36. data/net-ssh.gemspec +5 -2
  37. data.tar.gz.sig +0 -0
  38. metadata +62 -24
  39. metadata.gz.sig +0 -0
@@ -44,13 +44,22 @@ module Net
44
44
  diffie-hellman-group14-sha256
45
45
  diffie-hellman-group14-sha1],
46
46
 
47
- encryption: %w[aes256-ctr aes192-ctr aes128-ctr],
47
+ encryption: %w[aes256-ctr
48
+ aes192-ctr
49
+ aes128-ctr
50
+ aes256-gcm@openssh.com
51
+ aes128-gcm@openssh.com],
48
52
 
49
53
  hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
50
54
  hmac-sha2-512 hmac-sha2-256
51
55
  hmac-sha1]
52
56
  }.freeze
53
57
 
58
+ if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
59
+ DEFAULT_ALGORITHMS[:encryption].unshift(
60
+ 'chacha20-poly1305@openssh.com'
61
+ )
62
+ end
54
63
  if Net::SSH::Authentication::ED25519Loader::LOADED
55
64
  DEFAULT_ALGORITHMS[:host_key].unshift(
56
65
  'ssh-ed25519-cert-v01@openssh.com',
@@ -437,12 +446,13 @@ module Net
437
446
  def exchange_keys
438
447
  debug { "exchanging keys" }
439
448
 
449
+ need_bytes = kex_byte_requirement
440
450
  algorithm = Kex::MAP[kex].new(self, session,
441
451
  client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION,
442
452
  server_version_string: session.server_version.version,
443
453
  server_algorithm_packet: @server_packet,
444
454
  client_algorithm_packet: @client_packet,
445
- need_bytes: kex_byte_requirement,
455
+ need_bytes: need_bytes,
446
456
  minimum_dh_bits: options[:minimum_dh_bits],
447
457
  logger: logger)
448
458
  result = algorithm.exchange_keys
@@ -464,11 +474,30 @@ module Net
464
474
 
465
475
  parameters = { shared: secret, hash: hash, digester: digester }
466
476
 
467
- cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true))
468
- cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true))
477
+ cipher_client = CipherFactory.get(
478
+ encryption_client,
479
+ parameters.merge(iv: iv_client, key: key_client, encrypt: true)
480
+ )
481
+ cipher_server = CipherFactory.get(
482
+ encryption_server,
483
+ parameters.merge(iv: iv_server, key: key_server, decrypt: true)
484
+ )
485
+
486
+ mac_client =
487
+ if cipher_client.implicit_mac?
488
+ cipher_client.implicit_mac
489
+ else
490
+ HMAC.get(hmac_client, mac_key_client, parameters)
491
+ end
492
+ mac_server =
493
+ if cipher_server.implicit_mac?
494
+ cipher_server.implicit_mac
495
+ else
496
+ HMAC.get(hmac_server, mac_key_server, parameters)
497
+ end
469
498
 
470
- mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
471
- mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
499
+ cipher_client.nonce = iv_client if mac_client.respond_to?(:aead) && mac_client.aead
500
+ cipher_server.nonce = iv_server if mac_server.respond_to?(:aead) && mac_client.aead
472
501
 
473
502
  session.configure_client cipher: cipher_client, hmac: mac_client,
474
503
  compression: normalize_compression_name(compression_client),
@@ -0,0 +1,117 @@
1
+ require 'rbnacl'
2
+ require 'net/ssh/loggable'
3
+
4
+ module Net
5
+ module SSH
6
+ module Transport
7
+ ## Implements the chacha20-poly1305@openssh cipher
8
+ class ChaCha20Poly1305Cipher
9
+ include Net::SSH::Loggable
10
+
11
+ # Implicit HMAC, no need to do anything
12
+ class ImplicitHMac
13
+ def etm
14
+ # TODO: ideally this shouln't be called
15
+ true
16
+ end
17
+
18
+ def key_length
19
+ 64
20
+ end
21
+ end
22
+
23
+ def initialize(encrypt:, key:)
24
+ @chacha_hdr = OpenSSL::Cipher.new("chacha20")
25
+ key_len = @chacha_hdr.key_len
26
+ @chacha_main = OpenSSL::Cipher.new("chacha20")
27
+ @poly = RbNaCl::OneTimeAuths::Poly1305
28
+ if key.size < key_len * 2
29
+ error { "chacha20_poly1305: keylength doesn't match" }
30
+ raise "chacha20_poly1305: keylength doesn't match"
31
+ end
32
+ if encrypt
33
+ @chacha_hdr.encrypt
34
+ @chacha_main.encrypt
35
+ else
36
+ @chacha_hdr.decrypt
37
+ @chacha_main.decrypt
38
+ end
39
+ main_key = key[0...key_len]
40
+ @chacha_main.key = main_key
41
+ hdr_key = key[key_len...(2 * key_len)]
42
+ @chacha_hdr.key = hdr_key
43
+ end
44
+
45
+ def update_cipher_mac(payload, sequence_number)
46
+ iv_data = [0, 0, 0, sequence_number].pack("NNNN")
47
+ @chacha_main.iv = iv_data
48
+ poly_key = @chacha_main.update(([0] * 32).pack('C32'))
49
+
50
+ packet_length = payload.size
51
+ length_data = [packet_length].pack("N")
52
+ @chacha_hdr.iv = iv_data
53
+ packet = @chacha_hdr.update(length_data)
54
+
55
+ iv_data[0] = 1.chr
56
+ @chacha_main.iv = iv_data
57
+ unencrypted_data = payload
58
+ packet += @chacha_main.update(unencrypted_data)
59
+
60
+ packet += @poly.auth(poly_key, packet)
61
+ return packet
62
+ end
63
+
64
+ def read_length(data, sequence_number)
65
+ iv_data = [0, 0, 0, sequence_number].pack("NNNN")
66
+ @chacha_hdr.iv = iv_data
67
+ @chacha_hdr.update(data).unpack1("N")
68
+ end
69
+
70
+ def read_and_mac(data, mac, sequence_number)
71
+ iv_data = [0, 0, 0, sequence_number].pack("NNNN")
72
+ @chacha_main.iv = iv_data
73
+ poly_key = @chacha_main.update(([0] * 32).pack('C32'))
74
+
75
+ iv_data[0] = 1.chr
76
+ @chacha_main.iv = iv_data
77
+ unencrypted_data = @chacha_main.update(data[4..])
78
+ begin
79
+ ok = @poly.verify(poly_key, mac, data[0..])
80
+ raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless ok
81
+ rescue RbNaCl::BadAuthenticatorError
82
+ raise Net::SSH::Exception, "corrupted hmac detected #{name}"
83
+ end
84
+ return unencrypted_data
85
+ end
86
+
87
+ def mac_length
88
+ 16
89
+ end
90
+
91
+ def block_size
92
+ 8
93
+ end
94
+
95
+ def name
96
+ "chacha20-poly1305@openssh.com"
97
+ end
98
+
99
+ def implicit_mac?
100
+ true
101
+ end
102
+
103
+ def implicit_mac
104
+ return ImplicitHMac.new
105
+ end
106
+
107
+ def self.block_size
108
+ 8
109
+ end
110
+
111
+ def self.key_length
112
+ 64
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,17 @@
1
+ module Net
2
+ module SSH
3
+ module Transport
4
+ # Loads chacha20 poly1305 support which requires optinal dependency rbnacl
5
+ module ChaCha20Poly1305CipherLoader
6
+ begin
7
+ require 'net/ssh/transport/chacha20_poly1305_cipher'
8
+ LOADED = true
9
+ ERROR = nil
10
+ rescue LoadError => e
11
+ ERROR = e
12
+ LOADED = false
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,7 +1,11 @@
1
1
  require 'openssl'
2
2
  require 'net/ssh/transport/ctr.rb'
3
+ require 'net/ssh/transport/aes128_gcm'
4
+ require 'net/ssh/transport/aes256_gcm'
3
5
  require 'net/ssh/transport/key_expander'
4
6
  require 'net/ssh/transport/identity_cipher'
7
+ require 'net/ssh/transport/chacha20_poly1305_cipher_loader'
8
+ require 'net/ssh/transport/openssl_cipher_extensions'
5
9
 
6
10
  module Net
7
11
  module SSH
@@ -29,13 +33,25 @@ module Net
29
33
  'none' => 'none'
30
34
  }
31
35
 
36
+ SSH_TO_CLASS = {
37
+ 'aes256-gcm@openssh.com' => Net::SSH::Transport::AES256_GCM,
38
+ 'aes128-gcm@openssh.com' => Net::SSH::Transport::AES128_GCM
39
+ }.tap do |hash|
40
+ if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
41
+ hash['chacha20-poly1305@openssh.com'] =
42
+ Net::SSH::Transport::ChaCha20Poly1305Cipher
43
+ end
44
+ end
45
+
32
46
  # Returns true if the underlying OpenSSL library supports the given cipher,
33
47
  # and false otherwise.
34
48
  def self.supported?(name)
49
+ return true if SSH_TO_CLASS.key?(name)
50
+
35
51
  ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'"
36
52
  return true if ossl_name == "none"
37
53
 
38
- return OpenSSL::Cipher.ciphers.include?(ossl_name)
54
+ return SSH_TO_CLASS.key?(name) || OpenSSL::Cipher.ciphers.include?(ossl_name)
39
55
  end
40
56
 
41
57
  # Retrieves a new instance of the named algorithm. The new instance
@@ -44,6 +60,13 @@ module Net
44
60
  # cipher will be put into encryption or decryption mode, based on the
45
61
  # value of the +encrypt+ parameter.
46
62
  def self.get(name, options = {})
63
+ klass = SSH_TO_CLASS[name]
64
+ unless klass.nil?
65
+ key_len = klass.key_length
66
+ key = Net::SSH::Transport::KeyExpander.expand_key(key_len, options[:key], options)
67
+ return klass.new(encrypt: options[:encrypt], key: key)
68
+ end
69
+
47
70
  ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'"
48
71
  return IdentityCipher if ossl_name == "none"
49
72
 
@@ -53,6 +76,7 @@ module Net
53
76
 
54
77
  cipher.padding = 0
55
78
 
79
+ cipher.extend(Net::SSH::Transport::OpenSSLCipherExtensions)
56
80
  if name =~ /-ctr(@openssh.org)?$/
57
81
  if ossl_name !~ /-ctr/
58
82
  cipher.extend(Net::SSH::Transport::CTR)
@@ -75,6 +99,9 @@ module Net
75
99
  # of the tuple.
76
100
  # if :iv_len option is supplied the third return value will be ivlen
77
101
  def self.get_lengths(name, options = {})
102
+ klass = SSH_TO_CLASS[name]
103
+ return [klass.key_length, klass.block_size] unless klass.nil?
104
+
78
105
  ossl_name = SSH_TO_OSSL[name]
79
106
  if ossl_name.nil? || ossl_name == "none"
80
107
  result = [0, 0]
@@ -0,0 +1,207 @@
1
+ require 'net/ssh/loggable'
2
+
3
+ module Net
4
+ module SSH
5
+ module Transport
6
+ ## Extension module for aes(128|256)gcm ciphers
7
+ module GCMCipher
8
+ # rubocop:disable Metrics/AbcSize
9
+ def self.extended(orig)
10
+ # rubocop:disable Metrics/BlockLength
11
+ orig.class_eval do
12
+ include Net::SSH::Loggable
13
+
14
+ attr_reader :cipher
15
+ attr_reader :key
16
+ attr_accessor :nonce
17
+
18
+ #
19
+ # Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
20
+ # as it is not randomly generated due to being supplied from a counter.
21
+ # The RFC's use IV and nonce interchangeably.
22
+ #
23
+ def initialize(encrypt:, key:)
24
+ @cipher = OpenSSL::Cipher.new(algo_name)
25
+ @key = key
26
+ key_len = @cipher.key_len
27
+ if key.size != key_len
28
+ error_message = "#{cipher_name}: keylength does not match"
29
+ error { error_message }
30
+ raise error_message
31
+ end
32
+ encrypt ? @cipher.encrypt : @cipher.decrypt
33
+ @cipher.key = key
34
+
35
+ @nonce = {
36
+ fixed: nil,
37
+ invocation_counter: 0
38
+ }
39
+ end
40
+
41
+ def update_cipher_mac(payload, _sequence_number)
42
+ #
43
+ # --- RFC 5647 7.3 ---
44
+ # When using AES-GCM with secure shell, the packet_length field is to
45
+ # be treated as additional authenticated data, not as plaintext.
46
+ #
47
+ length_data = [payload.bytesize].pack('N')
48
+
49
+ cipher.auth_data = length_data
50
+
51
+ encrypted_data = cipher.update(payload) << cipher.final
52
+
53
+ mac = cipher.auth_tag
54
+
55
+ incr_nonce
56
+ length_data + encrypted_data + mac
57
+ end
58
+
59
+ #
60
+ # --- RFC 5647 ---
61
+ # uint32 packet_length; // 0 <= packet_length < 2^32
62
+ #
63
+ def read_length(data, _sequence_number)
64
+ data.unpack1('N')
65
+ end
66
+
67
+ #
68
+ # --- RFC 5647 ---
69
+ # In AES-GCM secure shell, the inputs to the authenticated encryption
70
+ # are:
71
+ # PT (Plain Text)
72
+ # byte padding_length; // 4 <= padding_length < 256
73
+ # byte[n1] payload; // n1 = packet_length-padding_length-1
74
+ # byte[n2] random_padding; // n2 = padding_length
75
+ # AAD (Additional Authenticated Data)
76
+ # uint32 packet_length; // 0 <= packet_length < 2^32
77
+ # IV (Initialization Vector)
78
+ # As described in section 7.1.
79
+ # BK (Block Cipher Key)
80
+ # The appropriate Encryption Key formed during the Key Exchange.
81
+ #
82
+ def read_and_mac(data, mac, _sequence_number)
83
+ # The authentication tag will be placed in the MAC field at the end of the packet
84
+
85
+ # OpenSSL does not verify auth tag length
86
+ # GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single
87
+ # byte allows authentication to pass. If single byte auth tags are possible
88
+ # an attacker would require no more than 256 attempts to forge a valid tag.
89
+ #
90
+ raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length
91
+
92
+ packet_length = data.unpack1('N')
93
+
94
+ cipher.auth_tag = mac.to_s
95
+ cipher.auth_data = [packet_length].pack('N')
96
+
97
+ result = cipher.update(data[4...]) << cipher.final
98
+ incr_nonce
99
+ result
100
+ end
101
+
102
+ def mac_length
103
+ 16
104
+ end
105
+
106
+ def block_size
107
+ 16
108
+ end
109
+
110
+ def self.block_size
111
+ 16
112
+ end
113
+
114
+ #
115
+ # --- RFC 5647 ---
116
+ # N_MIN minimum nonce (IV) length 12 octets
117
+ # N_MAX maximum nonce (IV) length 12 octets
118
+ #
119
+ def iv_len
120
+ 12
121
+ end
122
+
123
+ #
124
+ # --- RFC 5288 ---
125
+ # Each value of the nonce_explicit MUST be distinct for each distinct
126
+ # invocation of the GCM encrypt function for any fixed key. Failure to
127
+ # meet this uniqueness requirement can significantly degrade security.
128
+ # The nonce_explicit MAY be the 64-bit sequence number.
129
+ #
130
+ # --- RFC 5116 ---
131
+ # (2.1) Applications that can generate distinct nonces SHOULD use the nonce
132
+ # formation method defined in Section 3.2, and MAY use any
133
+ # other method that meets the uniqueness requirement.
134
+ #
135
+ # (3.2) The following method to construct nonces is RECOMMENDED.
136
+ #
137
+ # <- variable -> <- variable ->
138
+ # - - - - - - - - - - - - - -
139
+ # | fixed | counter |
140
+ #
141
+ # Initial octets consist of a fixed field and final octets consist of a
142
+ # Counter field. Implementations SHOULD support 12-octet nonces in which
143
+ # the Counter field is four octets long.
144
+ # The Counter fields of successive nonces form a monotonically increasing
145
+ # sequence, when those fields are regarded as unsignd integers in network
146
+ # byte order.
147
+ # The Counter part SHOULD be equal to zero for the first nonce and increment
148
+ # by one for each successive nonce that is generated.
149
+ # The Fixed field MUST remain constant for all nonces that are generated for
150
+ # a given encryption device.
151
+ #
152
+ # --- RFC 5647 ---
153
+ # The invocation field is treated as a 64-bit integer and is increment after
154
+ # each invocation of AES-GCM to process a binary packet.
155
+ # AES-GCM produces a keystream in blocks of 16-octets that is used to
156
+ # encrypt the plaintext. This keystream is produced by encrypting the
157
+ # following 16-octet data structure:
158
+ #
159
+ # uint32 fixed; // 4 octets
160
+ # uint64 invocation_counter; // 8 octets
161
+ # unit32 block_counter; // 4 octets
162
+ #
163
+ # The block_counter is initially set to one (1) and increment as each block
164
+ # of key is produced.
165
+ #
166
+ # The reader is reminded that SSH requires that the data to be encrypted
167
+ # MUST be padded out to a multiple of the block size (16-octets for AES-GCM).
168
+ #
169
+ def incr_nonce
170
+ return if nonce[:fixed].nil?
171
+
172
+ nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*')
173
+
174
+ apply_nonce
175
+ end
176
+
177
+ def nonce=(iv_s)
178
+ return if nonce[:fixed]
179
+
180
+ nonce[:fixed] = iv_s[0...4]
181
+ nonce[:invocation_counter] = iv_s[4...12]
182
+
183
+ apply_nonce
184
+ end
185
+
186
+ def apply_nonce
187
+ cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}"
188
+ end
189
+
190
+ #
191
+ # --- RFC 5647 ---
192
+ # If AES-GCM is selected as the encryption algorithm for a given
193
+ # tunnel, AES-GCM MUST also be selected as the Message Authentication
194
+ # Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC
195
+ # algorithm, it MUST also be selected as the encryption algorithm.
196
+ #
197
+ def implicit_mac?
198
+ true
199
+ end
200
+ end
201
+ end
202
+ # rubocop:enable Metrics/BlockLength
203
+ end
204
+ # rubocop:enable Metrics/AbcSize
205
+ end
206
+ end
207
+ end
@@ -8,6 +8,18 @@ module Net
8
8
  # The base class of all OpenSSL-based HMAC algorithm wrappers.
9
9
  class Abstract
10
10
  class << self
11
+ def aead(*v)
12
+ @aead = false if !defined?(@aead)
13
+ if v.empty?
14
+ @aead = superclass.aead if @aead.nil? && superclass.respond_to?(:aead)
15
+ return @aead
16
+ elsif v.length == 1
17
+ @aead = v.first
18
+ else
19
+ raise ArgumentError, "wrong number of arguments (#{v.length} for 1)"
20
+ end
21
+ end
22
+
11
23
  def etm(*v)
12
24
  @etm = false if !defined?(@etm)
13
25
  if v.empty?
@@ -57,6 +69,10 @@ module Net
57
69
  end
58
70
  end
59
71
 
72
+ def aead
73
+ self.class.aead
74
+ end
75
+
60
76
  def etm
61
77
  self.class.etm
62
78
  end
@@ -11,6 +11,10 @@ module Net
11
11
  8
12
12
  end
13
13
 
14
+ def key_length
15
+ 0
16
+ end
17
+
14
18
  # Returns an arbitrary integer.
15
19
  def iv_len
16
20
  4
@@ -50,6 +54,10 @@ module Net
50
54
  def reset
51
55
  self
52
56
  end
57
+
58
+ def implicit_mac?
59
+ false
60
+ end
53
61
  end
54
62
  end
55
63
  end
@@ -0,0 +1,8 @@
1
+ module Net::SSH::Transport
2
+ # we add those mehtods to OpenSSL::Chipher instances
3
+ module OpenSSLCipherExtensions
4
+ def implicit_mac?
5
+ false
6
+ end
7
+ end
8
+ end
@@ -12,7 +12,7 @@ module Net
12
12
  # module. It adds SSH encryption, compression, and packet validation, as
13
13
  # per the SSH2 protocol. It also adds an abstraction for polling packets,
14
14
  # to allow for both blocking and non-blocking reads.
15
- module PacketStream
15
+ module PacketStream # rubocop:disable Metrics/ModuleLength
16
16
  PROXY_COMMAND_HOST_IP = '<no hostip for proxy command>'.freeze
17
17
 
18
18
  include BufferedIo
@@ -123,12 +123,12 @@ module Net
123
123
  # Enqueues a packet to be sent, but does not immediately send the packet.
124
124
  # The given payload is pre-processed according to the algorithms specified
125
125
  # in the client state (compression, cipher, and hmac).
126
- def enqueue_packet(payload)
126
+ def enqueue_packet(payload) # rubocop:disable Metrics/AbcSize
127
127
  # try to compress the packet
128
128
  payload = client.compress(payload)
129
129
 
130
130
  # the length of the packet, minus the padding
131
- actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1
131
+ actual_length = (client.hmac.etm || client.hmac.aead ? 0 : 4) + payload.bytesize + 1
132
132
 
133
133
  # compute the padding length
134
134
  padding_length = client.block_size - (actual_length % client.block_size)
@@ -144,11 +144,14 @@ module Net
144
144
 
145
145
  padding = Array.new(padding_length) { rand(256) }.pack("C*")
146
146
 
147
- if client.hmac.etm
147
+ if client.cipher.implicit_mac?
148
+ unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
149
+ message = client.cipher.update_cipher_mac(unencrypted_data, client.sequence_number)
150
+ elsif client.hmac.etm
148
151
  debug { "using encrypt-then-mac" }
149
152
 
150
153
  # Encrypt padding_length, payload, and padding. Take MAC
151
- # from the unencrypted packet_lenght and the encrypted
154
+ # from the unencrypted packet_length and the encrypted
152
155
  # data.
153
156
  length_data = [packet_length].pack("N")
154
157
 
@@ -216,7 +219,7 @@ module Net
216
219
  # new Packet object.
217
220
  # rubocop:disable Metrics/AbcSize
218
221
  def poll_next_packet
219
- aad_length = server.hmac.etm ? 4 : 0
222
+ aad_length = server.hmac.etm || server.hmac.aead ? 4 : 0
220
223
 
221
224
  if @packet.nil?
222
225
  minimum = server.block_size < 4 ? 4 : server.block_size
@@ -225,7 +228,11 @@ module Net
225
228
  data = read_available(minimum + aad_length)
226
229
 
227
230
  # decipher it
228
- if server.hmac.etm
231
+ if server.cipher.implicit_mac?
232
+ @packet_length = server.cipher.read_length(data[0...4], server.sequence_number)
233
+ @packet = Net::SSH::Buffer.new
234
+ @mac_data = data
235
+ elsif server.hmac.etm
229
236
  @packet_length = data.unpack("N").first
230
237
  @mac_data = data
231
238
  @packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1]))
@@ -238,31 +245,45 @@ module Net
238
245
  need = @packet_length + 4 - aad_length - server.block_size
239
246
  raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0
240
247
 
241
- return nil if available < need + server.hmac.mac_length
248
+ if server.cipher.implicit_mac?
249
+ return nil if available < need + server.cipher.mac_length
250
+ else
251
+ return nil if available < need + server.hmac.mac_length # rubocop:disable Style/IfInsideElse
252
+ end
242
253
 
243
254
  if need > 0
244
255
  # read the remainder of the packet and decrypt it.
245
256
  data = read_available(need)
246
- @mac_data += data if server.hmac.etm
247
- @packet.append(server.update_cipher(data))
257
+ @mac_data += data if server.hmac.etm || server.cipher.implicit_mac?
258
+ unless server.cipher.implicit_mac?
259
+ @packet.append(
260
+ server.update_cipher(data)
261
+ )
262
+ end
248
263
  end
249
264
 
250
- # get the hmac from the tail of the packet (if one exists), and
251
- # then validate it.
252
- real_hmac = read_available(server.hmac.mac_length) || ""
253
-
254
- @packet.append(server.final_cipher)
255
- padding_length = @packet.read_byte
265
+ if server.cipher.implicit_mac?
266
+ real_hmac = read_available(server.cipher.mac_length) || ""
267
+ @packet = Net::SSH::Buffer.new(server.cipher.read_and_mac(@mac_data, real_hmac, server.sequence_number))
268
+ padding_length = @packet.read_byte
269
+ payload = @packet.read(@packet_length - padding_length - 1)
270
+ else
271
+ # get the hmac from the tail of the packet (if one exists), and
272
+ # then validate it.
273
+ real_hmac = read_available(server.hmac.mac_length) || ""
256
274
 
257
- payload = @packet.read(@packet_length - padding_length - 1)
275
+ @packet.append(server.final_cipher)
276
+ padding_length = @packet.read_byte
258
277
 
259
- my_computed_hmac = if server.hmac.etm
260
- server.hmac.digest([server.sequence_number, @mac_data].pack("NA*"))
261
- else
262
- server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
263
- end
264
- raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
278
+ payload = @packet.read(@packet_length - padding_length - 1)
265
279
 
280
+ my_computed_hmac = if server.hmac.etm
281
+ server.hmac.digest([server.sequence_number, @mac_data].pack("NA*"))
282
+ else
283
+ server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
284
+ end
285
+ raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
286
+ end
266
287
  # try to decompress the payload, in case compression is active
267
288
  payload = server.decompress(payload)
268
289
 
@@ -125,7 +125,7 @@ module Net
125
125
  compressor.deflate(data, Zlib::SYNC_FLUSH)
126
126
  end
127
127
 
128
- # Deompresses the data. If no compression is in effect, this will just return
128
+ # Decompresses the data. If no compression is in effect, this will just return
129
129
  # the data unmodified, otherwise it uses #decompressor to decompress the data.
130
130
  def decompress(data)
131
131
  data = data.to_s