summaryrefslogtreecommitdiff
path: root/lib/net/ssh/transport/state.rb
blob: b472191006d87369986e994aceee87a7ac85307c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
require 'zlib'
require 'net/ssh/transport/cipher_factory'
require 'net/ssh/transport/hmac'

module Net
  module SSH
    module Transport
      # Encapsulates state information about one end of an SSH connection. Such
      # state includes the packet sequence number, the algorithms in use, how
      # many packets and blocks have been processed since the last reset, and so
      # forth. This class will never be instantiated directly, but is used as
      # part of the internal state of the PacketStream module.
      class State
        # The socket object that owns this state object.
        attr_reader :socket

        # The next packet sequence number for this socket endpoint.
        attr_reader :sequence_number

        # The hmac algorithm in use for this endpoint.
        attr_reader :hmac

        # The compression algorithm in use for this endpoint.
        attr_reader :compression

        # The compression level to use when compressing data (or nil, for the default).
        attr_reader :compression_level

        # The number of packets processed since the last call to #reset!
        attr_reader :packets

        # The number of data blocks processed since the last call to #reset!
        attr_reader :blocks

        # The cipher algorithm in use for this socket endpoint.
        attr_reader :cipher

        # The block size for the cipher
        attr_reader :block_size

        # The role that this state plays (either :client or :server)
        attr_reader :role

        # The maximum number of packets that this endpoint wants to process before
        # needing a rekey.
        attr_accessor :max_packets

        # The maximum number of blocks that this endpoint wants to process before
        # needing a rekey.
        attr_accessor :max_blocks

        # The user-specified maximum number of bytes that this endpoint ought to
        # process before needing a rekey.
        attr_accessor :rekey_limit

        # Creates a new state object, belonging to the given socket. Initializes
        # the algorithms to "none".
        def initialize(socket, role)
          @socket = socket
          @role = role
          @sequence_number = @packets = @blocks = 0
          @cipher = CipherFactory.get("none")
          @block_size = 8
          @hmac = HMAC.get("none")
          @compression = nil
          @compressor = @decompressor = nil
          @next_iv = String.new
        end

        # A convenience method for quickly setting multiple values in a single
        # command.
        def set(values)
          values.each do |key, value|
            instance_variable_set("@#{key}", value)
          end
          reset!
        end

        def update_cipher(data)
          result = cipher.update(data)
          update_next_iv(role == :client ? result : data)
          return result
        end

        def final_cipher
          result = cipher.final
          update_next_iv(role == :client ? result : "", true)
          return result
        end

        # Increments the counters. The sequence number is incremented (and remapped
        # so it always fits in a 32-bit integer). The number of packets and blocks
        # are also incremented.
        def increment(packet_length)
          @sequence_number = (@sequence_number + 1) & 0xFFFFFFFF
          @packets += 1
          @blocks += (packet_length + 4) / @block_size
        end

        # The compressor object to use when compressing data. This takes into account
        # the desired compression level.
        def compressor
          @compressor ||= Zlib::Deflate.new(compression_level || Zlib::DEFAULT_COMPRESSION)
        end

        # The decompressor object to use when decompressing data.
        def decompressor
          @decompressor ||= Zlib::Inflate.new(nil)
        end

        # Returns true if data compression/decompression is enabled. This will
        # return true if :standard compression is selected, or if :delayed
        # compression is selected and the :authenticated hint has been received
        # by the socket.
        def compression?
          compression == :standard || (compression == :delayed && socket.hints[:authenticated])
        end

        # Compresses the data. If no compression is in effect, this will just return
        # the data unmodified, otherwise it uses #compressor to compress the data.
        def compress(data)
          data = data.to_s
          return data unless compression?

          compressor.deflate(data, Zlib::SYNC_FLUSH)
        end

        # Deompresses the data. If no compression is in effect, this will just return
        # the data unmodified, otherwise it uses #decompressor to decompress the data.
        def decompress(data)
          data = data.to_s
          return data unless compression?

          decompressor.inflate(data)
        end

        # Resets the counters on the state object, but leaves the sequence_number
        # unchanged. It also sets defaults for and recomputes the max_packets and
        # max_blocks values.
        def reset!
          @packets = @blocks = 0

          @max_packets ||= 1 << 31

          @block_size = cipher.block_size

          if max_blocks.nil?
            # cargo-culted from openssh. the idea is that "the 2^(blocksize*2)
            # limit is too expensive for 3DES, blowfish, etc., so enforce a 1GB
            # limit for small blocksizes."
            if @block_size >= 16
              @max_blocks = 1 << (@block_size * 2)
            else
              @max_blocks = (1 << 30) / @block_size
            end

            # if a limit on the # of bytes has been given, convert that into a
            # minimum number of blocks processed.

            @max_blocks = [@max_blocks, rekey_limit / @block_size].min if rekey_limit
          end

          cleanup
        end

        # Closes any the compressor and/or decompressor objects that have been
        # instantiated.
        def cleanup
          if @compressor
            @compressor.finish if !@compressor.finished?
            @compressor.close
          end

          if @decompressor
            # we call reset here so that we don't get warnings when we try to
            # close the decompressor
            @decompressor.reset
            @decompressor.close
          end

          @compressor = @decompressor = nil
        end

        # Returns true if the number of packets processed exceeds the maximum
        # number of packets, or if the number of blocks processed exceeds the
        # maximum number of blocks.
        def needs_rekey?
          max_packets && packets > max_packets ||
          max_blocks && blocks > max_blocks
        end

        private

        def update_next_iv(data, reset=false)
          @next_iv << data
          @next_iv = @next_iv[@next_iv.size - cipher.iv_len..-1]

          if reset
            cipher.reset
            cipher.iv = @next_iv
          end

          return data
        end
      end
    end
  end
end