noise-ruby 0.14.0 → 0.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6817736b9a72ca54f30ceb3238c9962fbd1f4a09092e9e0f783fec5a1035e9f
4
- data.tar.gz: de1d7510423907f0b73fa4f4115916bb2044195dc4dbbf8acb1f83cb484f2e76
3
+ metadata.gz: 92da6ad3c1e14197a093a54fd4e70f26cfa6cb15aec9d7fe6686b664d79235b2
4
+ data.tar.gz: 2837519b1708e55d0a0a052c11c9fd05da2fef1c1a4d881624602070fd07a579
5
5
  SHA512:
6
- metadata.gz: 3235949265d26680b60447ad79e7c2ef02824233343a948b3b6805cd519c5e3f1ff5b573abb2ab1feec11eb7e51523448e47ce787ed81bc0399e8ab86e2b8368
7
- data.tar.gz: 29d18a35a9d052d117e2ce0422f2a165eedf85d8f53af2d4b77263af500a257d194237cfa274b6ee40d432fbb8366490ed30fffafdaffa7713d70593c4104559
6
+ metadata.gz: a36f16cb90bcb3ac4979031744dbd0d2de4e324e14209bc8ae4b073e92b5ee789c0b898ec641a6a92c59c7e07e4f7e4169140b0499e23695e11a31ea51ed5874
7
+ data.tar.gz: c12c1216154ce5bd8f37e7e6bb56a62cca0a5da7cfacc7bf2b8d2ed1014bd25a487450e62850092a1bee0f1847f2554f1f3bac5c8b216220eeea3c593cdc37c7
data/.dockerignore CHANGED
@@ -4,5 +4,6 @@
4
4
  .rspec_status
5
5
  .ruby-lsp/
6
6
  coverage/
7
+ interop/target/
7
8
  pkg/
8
9
  tmp/
@@ -53,3 +53,42 @@ jobs:
53
53
  - name: Run tests
54
54
  run: bundle exec rspec spec
55
55
 
56
+ interop:
57
+ runs-on: ubuntu-latest
58
+ # The suite finishes in seconds. Anything longer means two implementations are waiting on each
59
+ # other, and a job that hangs until the six hour default helps nobody.
60
+ timeout-minutes: 15
61
+ steps:
62
+ - name: Checkout code
63
+ uses: actions/checkout@v4
64
+
65
+ - name: Set up Ruby
66
+ uses: ruby/setup-ruby@v1
67
+ with:
68
+ ruby-version: '4.0'
69
+ bundler-cache: true
70
+
71
+ - name: Set up Rust
72
+ id: rust
73
+ uses: dtolnay/rust-toolchain@stable
74
+
75
+ # interop/target holds compiler fingerprints, so the toolchain belongs in the key: a new
76
+ # stable rustc has to start from a cache of its own rather than rebuild under an old one.
77
+ - name: Cache the interoperability harness build
78
+ uses: actions/cache@v4
79
+ with:
80
+ path: |
81
+ ~/.cargo/registry
82
+ ~/.cargo/git
83
+ interop/target
84
+ key: interop-${{ runner.os }}-${{ steps.rust.outputs.cachekey }}-${{ hashFiles('interop/Cargo.lock') }}
85
+ restore-keys: interop-${{ runner.os }}-${{ steps.rust.outputs.cachekey }}-
86
+
87
+ - name: Build the interoperability harness
88
+ run: cargo build --release --locked
89
+ working-directory: interop
90
+
91
+ # A real handshake against snow, the Rust implementation. See interop/README.md.
92
+ - name: Run the interoperability suite
93
+ run: bundle exec rake interop
94
+
data/README.md CHANGED
@@ -96,7 +96,7 @@ it.
96
96
  ```
97
97
  initiator = Noise::Connection::Initiator.new("Noise_NN_25519_ChaChaPoly_BLAKE2b")
98
98
  initiator.prologue = "test" # => "test"
99
- initiator.start_handshake # => true
99
+ initiator.start_handshake
100
100
  cipher = initiator.write_message("") # => "\xB6\xF7gmxi\xAB\xBCY|t\xF0\x9D\x01A\ad\x92\xBBvp\x80ZNU\f=\x83\x81^\xFD\x15"
101
101
  ```
102
102
 
@@ -110,7 +110,7 @@ The responder responds messages to the initiator.
110
110
  ```
111
111
  responder = Noise::Connection::Responder.new("Noise_NN_25519_ChaChaPoly_BLAKE2b")
112
112
  responder.prologue = "test" # => "test"
113
- responder.start_handshake # => true
113
+ responder.start_handshake
114
114
  plain = responder.read_message(cipher) # => ""
115
115
  cipher = responder.write_message("") # => "\v\xD9\x97'\xC0\xB1\xC9\xFFD\x8C\x7F\x18L\xB0\xF2\x14\xB0\x11\xC0\x90\xAAZ\xE1\x03\x17z)\xB81/5L\x16\xE3\xD1\xBE<{\xB8\xBB\xD6\xF1\x00\x10]\x99=\xD7"
116
116
  ```
@@ -161,6 +161,154 @@ initiator.rekey_encryption
161
161
  responder.rekey_decryption
162
162
  ```
163
163
 
164
+ ### Thread safety
165
+
166
+ One connection belongs to one thread. A `Noise::Connection` holds its handshake state and its
167
+ transport nonces in plain instance variables and never locks, so two threads that share one
168
+ connection can encrypt two different plaintexts under the same nonce. That is not a small loss for
169
+ either cipher this gem implements: reusing a nonce breaks the confidentiality of both messages and
170
+ lets an attacker forge further ones.
171
+
172
+ If an application has to reach one connection from more than one thread, it is responsible for
173
+ serialising every call to it.
174
+
175
+ This is deliberate rather than an omission. A lock inside the connection would make each call
176
+ atomic without making concurrent use correct, because the transport nonce numbers the messages of
177
+ a direction: two threads that both encrypt still produce a stream in an order the receiver cannot
178
+ reconstruct. Only the application knows which message comes first, and deciding that order is
179
+ itself the serialisation the lock cannot supply.
180
+
181
+ Receiving is no easier. Decrypting a message that arrived out of order is a `decryption_nonce=`
182
+ call followed by a `decrypt` call, and no lock taken one call at a time holds those two together.
183
+
184
+ ### Half-duplex mode
185
+
186
+ Section 11.5 of the Noise specification describes protocols in which the two parties strictly take
187
+ turns. Such a protocol may keep a single `CipherState` for both directions instead of one per
188
+ direction, which halves what each party has to store after the handshake. Pass `half_duplex: true`
189
+ to both parties to run that way.
190
+
191
+ ```
192
+ initiator = Noise::Connection::Initiator.new("Noise_NN_25519_ChaChaPoly_SHA256", half_duplex: true)
193
+ responder = Noise::Connection::Responder.new("Noise_NN_25519_ChaChaPoly_SHA256", half_duplex: true)
194
+ ```
195
+
196
+ Because there is one `CipherState`, the methods that name a direction all reach it:
197
+ `encryption_nonce` and `decryption_nonce` report the same count, setting either sets both, and
198
+ `rekey_encryption` and `rekey_decryption` replace the same key.
199
+
200
+ **The two parties must strictly alternate their transport messages.** They share one nonce, so a
201
+ message in either direction advances the count both of them keep. If both parties encrypt before
202
+ either decrypts, the two messages go out under the same nonce, which breaks the confidentiality of
203
+ both and lets an attacker forge further ones. Nothing in this gem can detect that: it is the
204
+ application protocol that has to guarantee the turn taking.
205
+
206
+ Use it only when the protocol you are implementing calls for it. It is off by default, and a
207
+ one-way pattern refuses it, because such a pattern has no messages to alternate.
208
+
209
+ ### Framing a connection for a socket
210
+
211
+ `encrypt` and `decrypt` work on whole messages, which is the shape the Noise specification
212
+ describes but not the shape a socket has. `Noise::Transport::Framed` puts a finished connection on
213
+ top of an `IO` by writing each message preceded by its length, so that the reader knows how many
214
+ bytes to take.
215
+
216
+ ```
217
+ transport = Noise::Transport::Framed.new(connection, socket)
218
+
219
+ transport.write("a message")
220
+ message = transport.read
221
+ ```
222
+
223
+ `read` returns one message, waiting until all of it has arrived however many pieces it comes in.
224
+ It answers `nil` when the stream ends between messages, which is how the other party closes
225
+ without cutting one in half, and raises `TruncatedMessageError` when the stream ends part way
226
+ through one. `write` returns once the whole frame has gone out.
227
+
228
+ Every failure ends the transport, so build a new connection rather than reading again. A
229
+ `DecryptError` means the frame was not written by the party this connection shares a key with, or
230
+ not in the order it claims. A `TruncatedMessageError` or a `ReadTimeoutError` leaves the bytes
231
+ already taken out of the stream and nowhere to put them, so the next read would take the middle of
232
+ a message for a length. Errors the socket itself raises, such as `Errno::ECONNRESET`, come through
233
+ as they are.
234
+
235
+ Pass `read_timeout:` in seconds to give up on a message that stops arriving:
236
+
237
+ ```
238
+ transport = Noise::Transport::Framed.new(connection, socket, read_timeout: 30)
239
+ ```
240
+
241
+ The timeout applies to each wait for more bytes rather than to the message as a whole, so a peer
242
+ that sends a byte at a time holds the read open without ever tripping it. It needs a stream that
243
+ answers `wait_readable`, which a socket does and a `StringIO` does not; asking for a timeout on
244
+ one that cannot honour it raises `ArgumentError` rather than dropping it silently.
245
+
246
+ The length goes out in the clear, as two big-endian bytes. Two are enough because a Noise message
247
+ may not exceed 65535 bytes, and a payload is shorter than that by its authentication tag. The
248
+ length being in the clear means this framing hides nothing about how long each message is; a
249
+ protocol that has to hide its message sizes pads them, or encrypts the length as BOLT #8 does.
250
+
251
+ Once a connection is framed, stop calling its `encrypt` and `decrypt` directly. A message that
252
+ goes out unframed leaves the reader taking the next message's bytes for a length.
253
+
254
+ ### Lightning Network (BOLT #8)
255
+
256
+ BOLT #8 is the transport the Lightning Network runs on: a `Noise_XK_secp256k1_ChaChaPoly_SHA256`
257
+ handshake, then a byte stream in which every message is preceded by its own encrypted length.
258
+ `Noise::Transport::Bolt8` owns that framing, so an application only has to run the handshake and
259
+ hand over the finished connection.
260
+
261
+ ```
262
+ name = Noise::Transport::Bolt8::PROTOCOL_NAME
263
+
264
+ initiator = Noise::Connection::Initiator.new(name, keypairs: { s: local_static, rs: node_id })
265
+ initiator.prologue = Noise::Transport::Bolt8::PROLOGUE
266
+ initiator.start_handshake
267
+ # ... exchange the three handshake messages over the socket ...
268
+
269
+ transport = Noise::Transport::Bolt8.new(initiator, socket)
270
+ transport.write("a lightning message")
271
+ message = transport.read
272
+ ```
273
+
274
+ It reads and writes exactly as `Framed` does above, `read_timeout:` and the `nil` that means the
275
+ other party closed between messages included. What differs is the framing: the length is encrypted as a
276
+ Noise message of its own rather than sent in the clear, so a watcher cannot tell how long each
277
+ message is. `DecryptError` means the same thing it does there, and BOLT #8 requires the connection
278
+ to be closed when it happens.
279
+
280
+ Each direction replaces its key with `HKDF(ck, k)` once its nonce reaches 1000, which is every 500
281
+ messages because each message is encrypted twice. This is not the `rekey_encryption` of the Noise
282
+ specification: it draws on the chaining key the handshake ended with, so a key stolen now says
283
+ nothing about the keys that direction used before it. The transport does it on its own; nothing
284
+ has to be called.
285
+
286
+ The transport takes over the connection's transport phase: it holds the very `CipherState`s the
287
+ connection does. Once a connection is wrapped, stop calling its `encrypt`, `decrypt`, nonce
288
+ accessors and rekey methods, because each of them moves the same key and nonce and the peer has no
289
+ way to learn that they did. A half-duplex connection is refused outright, since BOLT #8 gives each
290
+ direction a key of its own.
291
+
292
+ Nothing else in the gem loads this. An application that does not speak Lightning never names
293
+ `Noise::Transport::Bolt8`, and never pays for it.
294
+
295
+ ## Interoperability
296
+
297
+ The suite checks this gem against the official test vectors in `spec/vectors/`, which are replayed
298
+ transcripts. `interop/` runs the other kind of test: a real handshake, and then a real transport
299
+ exchange, between this gem and [snow](https://github.com/mcginty/snow), the Rust implementation of
300
+ the framework. It covers eighteen patterns against five cipher and hash suites, in both
301
+ directions.
302
+
303
+ It needs a Rust toolchain, so it is not part of `bundle exec rspec`:
304
+
305
+ ```
306
+ cd interop && cargo build --release && cd ..
307
+ bundle exec rake interop
308
+ ```
309
+
310
+ See [interop/README.md](interop/README.md) for what it covers and what it does not.
311
+
164
312
  ## Development
165
313
 
166
314
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -5,4 +5,16 @@ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
+ desc 'Build the harness the interoperability suite runs against'
9
+ task :interop_harness do
10
+ Dir.chdir('interop') { sh 'cargo build --release --locked' }
11
+ end
12
+
13
+ desc 'Run the interoperability suite against the snow reference implementation'
14
+ RSpec::Core::RakeTask.new(interop: :interop_harness) do |task|
15
+ task.pattern = 'interop/*_spec.rb'
16
+ end
17
+
18
+ # interop is deliberately left out: it needs a Rust toolchain, and the harness built from
19
+ # interop/. See interop/README.md.
8
20
  task default: :spec
@@ -2,6 +2,39 @@
2
2
 
3
3
  module Noise
4
4
  module Connection
5
+ # A connection moves through four states, and every public operation is legal in some of them
6
+ # and not in others:
7
+ #
8
+ # :created -- start_handshake
9
+ # :handshake_write -- write_message, fallback
10
+ # :handshake_read -- read_message, fallback
11
+ # :transport -- encrypt, decrypt, rekey_*, the nonce accessors
12
+ #
13
+ # start_handshake moves :created to the state this party starts the pattern in, which is
14
+ # :handshake_write for the initiator and :handshake_read for the responder. Writing or reading
15
+ # a handshake message passes the turn to the other party, and the message that completes the
16
+ # pattern moves the connection to :transport.
17
+ #
18
+ # == Threads
19
+ #
20
+ # One connection belongs to one thread. Nothing here is synchronised: the state above is a
21
+ # plain instance variable, and #encrypt and #decrypt advance the transport nonce with a plain
22
+ # +=, so two threads that share a connection can encrypt two different plaintexts under the
23
+ # same nonce. Neither ChaChaPoly nor AESGCM degrades gracefully when that happens: reusing a
24
+ # nonce breaks the confidentiality of both messages and lets an attacker forge further ones.
25
+ #
26
+ # A caller that has to reach one connection from more than one thread is responsible for
27
+ # serialising every call to it.
28
+ #
29
+ # This class deliberately does not lock. A Mutex would make each call atomic without making
30
+ # concurrent use correct: the transport nonce numbers the messages of a direction, so two
31
+ # threads that both encrypt still hand the transport layer a stream whose order the receiver
32
+ # cannot reconstruct. Only the application knows which message is meant to be first, and once
33
+ # it has said so, its own serialisation has already done what the lock would have done.
34
+ #
35
+ # The other direction is no better. Decrypting a message that arrived out of order is a
36
+ # #decryption_nonce= call followed by a #decrypt call, a sequence no per-call lock can hold
37
+ # together.
5
38
  class Base
6
39
  # The Noise spec caps a handshake or transport message at 65535 bytes. A transport message is
7
40
  # the ciphertext, so the plaintext a caller may hand to encrypt is shorter by the
@@ -9,7 +42,29 @@ module Noise
9
42
  MAX_MESSAGE_LENGTH = 65_535
10
43
  MAX_PLAINTEXT_LENGTH = MAX_MESSAGE_LENGTH - Noise::State::CipherState::TAG_LENGTH
11
44
 
12
- attr_reader :protocol, :handshake_started, :handshake_finished, :handshake_hash, :handshake_state,
45
+ # The states in which the connection is running a handshake, and what it expects next in
46
+ # each of them. The message explains a HandshakeTurnError.
47
+ TURN_MESSAGES = {
48
+ handshake_write: 'This party writes the next handshake message; it cannot read one yet.',
49
+ handshake_read: 'This party reads the next handshake message; it cannot write one yet.'
50
+ }.freeze
51
+ private_constant :TURN_MESSAGES
52
+
53
+ # The states in which a handshake is running, which are the ones #fallback may be called in.
54
+ HANDSHAKE_STATES = TURN_MESSAGES.keys.freeze
55
+ private_constant :HANDSHAKE_STATES
56
+
57
+ # @return [Symbol] the current state, one of :created, :handshake_write, :handshake_read and
58
+ # :transport.
59
+ attr_reader :state
60
+
61
+ # @return [String, nil] the chaining key the handshake ended with, once it has. Split()
62
+ # derives the transport keys from it and leaves it as it is, so a transport layer that
63
+ # derives its own keys from it later — BOLT #8 rotates its keys this way — needs it after
64
+ # the handshake. It is as secret as the transport keys it produced.
65
+ attr_reader :chaining_key
66
+
67
+ attr_reader :protocol, :handshake_hash, :handshake_state,
13
68
  :cipher_state_encrypt, :cipher_state_decrypt, :cipher_state_handshake, :s, :rs
14
69
  attr_accessor :psks, :prologue
15
70
 
@@ -23,37 +78,87 @@ module Noise
23
78
  # the keypair given here instead of generating a fresh one, and an ephemeral key reused
24
79
  # across handshakes gives up the forward secrecy every pattern depends on. The handshake
25
80
  # still succeeds, so nothing reports the loss.
26
- def initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil })
81
+ # @param [Boolean] half_duplex whether to run the transport phase in half-duplex mode, in
82
+ # which one CipherState encrypts both directions. See #half_duplex? for what that costs.
83
+ # @raise [Noise::Exceptions::NoiseValidationError] if half_duplex is asked for on a one-way
84
+ # pattern, which has no messages to alternate.
85
+ def initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil }, half_duplex: false)
27
86
  @protocol = Protocol.create(name)
87
+ @half_duplex = half_duplex
88
+ validate_half_duplex!
28
89
 
29
90
  # parameter keypairs[:e] and keypairs[:s] are strings, so should convert Noise::Key object.
30
91
  @local_keypairs = {}
31
92
  @local_keypairs[:e] = @protocol.dh_fn.class.from_private(keypairs[:e]) if keypairs[:e]
32
93
  @local_keypairs[:s] = @protocol.dh_fn.class.from_private(keypairs[:s]) if keypairs[:s]
33
94
  @remote_keys = { rs: keypairs[:rs], re: keypairs[:re] }
34
- @handshake_started = false
35
- @handshake_finished = false
36
- initialize_next_message
95
+ @state = :created
37
96
  end
38
97
 
98
+ # Whether the transport phase uses one CipherState for both directions, which is the
99
+ # half-duplex mode of section 11.5 of the Noise specification. Both parties keep the first
100
+ # CipherState that Split() returns and drop the second.
101
+ #
102
+ # It is only safe when the two parties strictly alternate their transport messages. They
103
+ # share one nonce, so if both encrypt before either decrypts, two different plaintexts go
104
+ # out under the same nonce, which breaks the confidentiality of both and lets an attacker
105
+ # forge further ones. Nothing here can check that the application alternates; that is the
106
+ # application protocol's guarantee to make.
107
+ #
108
+ # Because there is one CipherState, the accessors that name a direction all reach it:
109
+ # #encryption_nonce and #decryption_nonce report the same count, setting either sets both,
110
+ # and #rekey_encryption and #rekey_decryption replace the same key.
111
+ #
112
+ # @return [Boolean]
113
+ def half_duplex?
114
+ @half_duplex
115
+ end
116
+
117
+ # @return [Boolean] true once #start_handshake has been called.
118
+ def handshake_started?
119
+ @state != :created
120
+ end
121
+ alias handshake_started handshake_started?
122
+
123
+ # @return [Boolean] true once the last handshake message has been written or read, which is
124
+ # when the transport operations become available.
125
+ def handshake_finished?
126
+ @state == :transport
127
+ end
128
+ alias handshake_finished handshake_finished?
129
+
130
+ # Starts the handshake this connection was created for. It is legal once: a connection that
131
+ # already has a handshake, running or finished, has keys and a transcript that restarting
132
+ # would silently throw away. #fallback is the supported way to begin a second handshake.
39
133
  def start_handshake
134
+ ensure_state!(:created)
40
135
  validate
41
136
  initialise_handshake_state
42
- @handshake_started = true
137
+ @state = initial_turn
138
+ nil
43
139
  end
44
140
 
45
141
  # Restarts the handshake with a fallback pattern, carrying over the keys of the aborted one.
46
142
  #
47
143
  # The roles swap here: the party that wrote the aborted message now reads, and the one that
48
- # failed to read it now writes. Both sides are already in that state, so @next_message is
49
- # deliberately left as it is rather than reset through initialize_next_message.
144
+ # failed to read it now writes. Both sides are already on that turn, so the current turn is
145
+ # carried over rather than reset to the one this party started the aborted pattern on.
50
146
  def fallback(fallback_name)
51
- @protocol = Protocol.create(fallback_name)
52
- @handshake_started = false
53
- @handshake_finished = false
147
+ ensure_state!(*HANDSHAKE_STATES)
148
+
149
+ turn = @state
150
+ protocol = Protocol.create(fallback_name)
151
+ validate_half_duplex!(protocol)
152
+ @protocol = protocol
54
153
  @local_keypairs = { e: @handshake_state.e, s: @handshake_state.s }
55
154
  @remote_keys = { re: @handshake_state.re, rs: @handshake_state.rs }
155
+ # Everything that can fail has run by now, so a bad fallback name leaves the connection on
156
+ # the handshake it was already running. start_handshake accepts a connection in :created
157
+ # alone, so the aborted handshake is cleared here and the turn restored after it.
158
+ @state = :created
56
159
  start_handshake
160
+ @state = turn
161
+ nil
57
162
  end
58
163
 
59
164
  def initialise_handshake_state
@@ -69,10 +174,7 @@ module Noise
69
174
  end
70
175
 
71
176
  def write_message(payload = '')
72
- # Call NoiseConnection.start_handshake first
73
- raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
74
- raise Noise::Exceptions::NoiseHandshakeError if @next_message != :write
75
- raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
177
+ ensure_state!(:handshake_write)
76
178
 
77
179
  length = @handshake_state.expected_message_length(payload.bytesize)
78
180
  if length > MAX_MESSAGE_LENGTH
@@ -80,26 +182,27 @@ module Noise
80
182
  "Message would be #{length} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
81
183
  end
82
184
 
83
- @next_message = :read
185
+ # The turn passes before the message is built, so that a message this party failed to
186
+ # build still leaves the connection waiting for the other party, as fallback expects.
187
+ @state = :handshake_read
84
188
  buffer = +''
85
- @handshake_finished = @handshake_state.write_message(payload, buffer)
189
+ @state = :transport if @handshake_state.write_message(payload, buffer)
86
190
  buffer
87
191
  end
88
192
 
89
193
  def read_message(data)
90
- # Call NoiseConnection.start_handshake first
91
- raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
92
- raise Noise::Exceptions::NoiseHandshakeError if @next_message != :read
93
- raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
194
+ ensure_state!(:handshake_read)
94
195
 
95
196
  if data.bytesize > MAX_MESSAGE_LENGTH
96
197
  raise Noise::Exceptions::MessageTooLongError,
97
198
  "Message is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
98
199
  end
99
200
 
100
- @next_message = :write
201
+ # See #write_message: the turn passes before the message is read, so that a message that
202
+ # fails to decrypt leaves this party ready to write the fallback handshake message.
203
+ @state = :handshake_write
101
204
  buffer = +''
102
- @handshake_finished = @handshake_state.read_message(data, buffer)
205
+ @state = :transport if @handshake_state.read_message(data, buffer)
103
206
  buffer
104
207
  end
105
208
 
@@ -197,6 +300,7 @@ module Noise
197
300
 
198
301
  def handshake_done(_c1, _c2)
199
302
  @handshake_hash = @symmetric_state.handshake_hash
303
+ @chaining_key = @symmetric_state.ck
200
304
  @s = @handshake_state.s
201
305
  @rs = @handshake_state.rs
202
306
  @handshake_state = nil
@@ -206,6 +310,36 @@ module Noise
206
310
 
207
311
  private
208
312
 
313
+ # A one-way pattern gives the responder no way to reply, so its parties never alternate and
314
+ # half-duplex has nothing to share a CipherState between. Sharing one anyway would hand the
315
+ # responder an encryption key the pattern is meant to deny it.
316
+ #
317
+ # @param [Noise::Protocol] protocol the protocol to check, which is the one being fallen back
318
+ # to when #fallback asks.
319
+ # @raise [Noise::Exceptions::NoiseValidationError]
320
+ def validate_half_duplex!(protocol = @protocol)
321
+ return unless @half_duplex && protocol.pattern.one_way?
322
+
323
+ raise Noise::Exceptions::NoiseValidationError,
324
+ "#{protocol.name} is a one-way pattern, which cannot run half-duplex."
325
+ end
326
+
327
+ # The CipherState for the direction this party did not assign c1 to, which is decryption for
328
+ # the initiator and encryption for the responder.
329
+ #
330
+ # Half-duplex answers c1 both ways, so that the two parties encrypt and decrypt under the
331
+ # same key and nonce as they take turns. A one-way pattern has no second direction at all,
332
+ # so it has none.
333
+ #
334
+ # @param [Noise::State::CipherState] c1 the first CipherState Split() returned.
335
+ # @param [Noise::State::CipherState] c2 the second one.
336
+ # @return [Noise::State::CipherState, nil]
337
+ def other_direction_cipher_state(c1, c2)
338
+ return c1 if half_duplex?
339
+
340
+ @protocol.pattern.one_way? ? nil : c2
341
+ end
342
+
209
343
  # Returns the transport CipherState of the given direction.
210
344
  #
211
345
  # One-way patterns leave the direction the caller cannot use as nil, so the absence of a
@@ -213,13 +347,43 @@ module Noise
213
347
  #
214
348
  # @param [Symbol] direction :encrypt or :decrypt.
215
349
  def transport_cipher_state(direction)
216
- raise Noise::Exceptions::NoiseHandshakeError unless @handshake_finished
350
+ ensure_state!(:transport)
217
351
 
218
352
  cipher_state = direction == :encrypt ? @cipher_state_encrypt : @cipher_state_decrypt
219
353
  raise Noise::Exceptions::NoiseHandshakeError, "This party cannot #{direction} messages." unless cipher_state
220
354
 
221
355
  cipher_state
222
356
  end
357
+
358
+ # Raises unless the connection is in one of the states the calling operation is legal in.
359
+ # Every public operation goes through here, so a new one cannot forget its precondition.
360
+ #
361
+ # @param [Array<Symbol>] allowed the states the operation may be called in.
362
+ def ensure_state!(*allowed)
363
+ return if allowed.include?(@state)
364
+
365
+ raise state_error(allowed)
366
+ end
367
+
368
+ # Builds the exception for an operation called in the wrong state. All four descend from
369
+ # NoiseHandshakeError, so a caller that does not care which one it is can rescue the parent.
370
+ #
371
+ # @param [Array<Symbol>] allowed the states the operation may be called in.
372
+ # @return [Noise::Exceptions::NoiseHandshakeError]
373
+ def state_error(allowed)
374
+ case @state
375
+ when :created
376
+ Noise::Exceptions::HandshakeNotStartedError.new('The handshake has not started. Call #start_handshake.')
377
+ when :transport
378
+ Noise::Exceptions::HandshakeAlreadyFinishedError.new('The handshake has already finished.')
379
+ else
380
+ if allowed.include?(:transport)
381
+ Noise::Exceptions::HandshakeNotFinishedError.new('The handshake has not finished.')
382
+ else
383
+ Noise::Exceptions::HandshakeTurnError.new(TURN_MESSAGES.fetch(@state))
384
+ end
385
+ end
386
+ end
223
387
  end
224
388
  end
225
389
  end
@@ -3,8 +3,11 @@
3
3
  module Noise
4
4
  module Connection
5
5
  class Initiator < Base
6
- def initialize_next_message
7
- @next_message = :write
6
+ # The initiator writes the first handshake message of the pattern.
7
+ #
8
+ # @return [Symbol] the state start_handshake moves this party into.
9
+ def initial_turn
10
+ :handshake_write
8
11
  end
9
12
 
10
13
  def initiator?
@@ -14,7 +17,7 @@ module Noise
14
17
  def handshake_done(c1, c2)
15
18
  super
16
19
  @cipher_state_encrypt = c1
17
- @cipher_state_decrypt = @protocol.pattern.one_way? ? nil : c2
20
+ @cipher_state_decrypt = other_direction_cipher_state(c1, c2)
18
21
  end
19
22
  end
20
23
  end
@@ -3,8 +3,11 @@
3
3
  module Noise
4
4
  module Connection
5
5
  class Responder < Base
6
- def initialize_next_message
7
- @next_message = :read
6
+ # The responder reads the first handshake message of the pattern.
7
+ #
8
+ # @return [Symbol] the state start_handshake moves this party into.
9
+ def initial_turn
10
+ :handshake_read
8
11
  end
9
12
 
10
13
  def initiator?
@@ -14,7 +17,7 @@ module Noise
14
17
  def handshake_done(c1, c2)
15
18
  super
16
19
  @cipher_state_decrypt = c1
17
- @cipher_state_encrypt = @protocol.pattern.one_way? ? nil : c2
20
+ @cipher_state_encrypt = other_direction_cipher_state(c1, c2)
18
21
  end
19
22
  end
20
23
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when a handshake operation is called after the handshake has already finished.
6
+ class HandshakeAlreadyFinishedError < NoiseHandshakeError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when a transport operation is called while the handshake is still running.
6
+ class HandshakeNotFinishedError < NoiseHandshakeError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when an operation needs a handshake that Connection#start_handshake has not begun yet.
6
+ class HandshakeNotStartedError < NoiseHandshakeError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when a handshake message is written where one has to be read, or read where one has to
6
+ # be written. A Noise handshake alternates between the two parties, so only one of the two is
7
+ # legal at any point.
8
+ class HandshakeTurnError < NoiseHandshakeError
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when a transport layer gave up waiting for the rest of a message to arrive.
6
+ class ReadTimeoutError < RuntimeError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when a stream ends part way through a message that a transport layer was reading.
6
+ class TruncatedMessageError < RuntimeError
7
+ end
8
+ end
9
+ end