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.
@@ -4,16 +4,22 @@ module Noise
4
4
  module Exceptions
5
5
  autoload :DecryptError, 'noise/exceptions/decrypt_error'
6
6
  autoload :EncryptError, 'noise/exceptions/encrypt_error'
7
+ autoload :HandshakeAlreadyFinishedError, 'noise/exceptions/handshake_already_finished_error'
8
+ autoload :HandshakeNotFinishedError, 'noise/exceptions/handshake_not_finished_error'
9
+ autoload :HandshakeNotStartedError, 'noise/exceptions/handshake_not_started_error'
10
+ autoload :HandshakeTurnError, 'noise/exceptions/handshake_turn_error'
7
11
  autoload :InvalidNonceError, 'noise/exceptions/invalid_nonce_error'
8
12
  autoload :InvalidPublicKeyError, 'noise/exceptions/invalid_public_key_error'
9
13
  autoload :MaxNonceError, 'noise/exceptions/max_nonce_error'
10
14
  autoload :MessageTooLongError, 'noise/exceptions/message_too_long_error'
11
15
  autoload :MissingDependencyError, 'noise/exceptions/missing_dependency_error'
12
16
  autoload :ProtocolNameError, 'noise/exceptions/protocol_name_error'
17
+ autoload :ReadTimeoutError, 'noise/exceptions/read_timeout_error'
13
18
  autoload :NoiseHandshakeError, 'noise/exceptions/noise_handshake_error'
14
19
  autoload :NoiseValidationError, 'noise/exceptions/noise_validation_error'
15
20
  autoload :NoisePSKError, 'noise/exceptions/noise_psk_error'
16
21
  autoload :PSKValueError, 'noise/exceptions/psk_value_error'
22
+ autoload :TruncatedMessageError, 'noise/exceptions/truncated_message_error'
17
23
  autoload :UnsupportedModifierError, 'noise/exceptions/unsupported_modifier_error'
18
24
  end
19
25
  end
data/lib/noise/pattern.rb CHANGED
@@ -102,14 +102,16 @@ module Noise
102
102
  class Pattern
103
103
  attr_reader :name, :tokens, :modifiers, :psk_count, :fallback
104
104
 
105
- NAME_REGEX = /\A([A-Z1]+)([^A-Z]*)\z/
106
-
107
- def self.create(name)
108
- matched = NAME_REGEX.match(name)
109
- raise Noise::Exceptions::ProtocolNameError, "Malformed pattern name: #{name}" unless matched
110
-
111
- modifiers = matched[2].split('+').map { |s| Modifier.parse(s) }
112
- pattern_class(matched[1]).new(modifiers)
105
+ # Noise::ProtocolName tells a pattern name from the modifiers written after it, so both
106
+ # arrive here already separated.
107
+ #
108
+ # @param [String] name the handshake pattern name on its own, for example 'XX'.
109
+ # @param [Array<Noise::Modifier::Psk, Noise::Modifier::Fallback>] modifiers the modifiers to
110
+ # apply to it. Call #apply_pattern_modifiers to have them rewrite the token list.
111
+ # @raise [Noise::Exceptions::ProtocolNameError] if no pattern goes by that name.
112
+ # @return [Noise::Pattern]
113
+ def self.create(name, modifiers = [])
114
+ pattern_class(name).new(modifiers)
113
115
  end
114
116
 
115
117
  def self.pattern_class(pattern)
@@ -3,7 +3,7 @@
3
3
  module Noise
4
4
  class Protocol
5
5
  attr_accessor :cipher_fn, :hash_fn, :dh_fn, :hkdf_fn
6
- attr_reader :name, :pattern
6
+ attr_reader :name, :pattern, :protocol_name
7
7
 
8
8
  CIPHER = {
9
9
  'AESGCM' => Noise::Functions::Cipher::AesGcm,
@@ -24,35 +24,59 @@ module Noise
24
24
  'BLAKE3' => Noise::Functions::Hash::Blake3
25
25
  }.freeze
26
26
 
27
+ # @param [String] name the protocol name, for example 'Noise_XX_25519_ChaChaPoly_SHA256'.
28
+ # @raise [Noise::Exceptions::ProtocolNameError] if the name is malformed, or names a pattern
29
+ # or a function this gem does not implement.
30
+ # @raise [Noise::Exceptions::UnsupportedModifierError] if it names a modifier this gem does
31
+ # not implement.
32
+ # @return [Noise::Protocol]
27
33
  def self.create(name)
28
- parts = name.split('_')
29
- raise Noise::Exceptions::ProtocolNameError, "Malformed protocol name: #{name}" unless parts.size == 5
30
-
31
- prefix, pattern_name, dh_name, cipher_name, hash_name = parts
32
- raise Noise::Exceptions::ProtocolNameError, "Malformed protocol name: #{name}" if prefix != 'Noise'
33
-
34
- new(name, pattern_name, cipher_name, hash_name, dh_name)
34
+ new(Noise::ProtocolName.parse(name))
35
35
  end
36
36
 
37
- def initialize(name, pattern_name, cipher_name, hash_name, dh_name)
38
- @name = name
39
- @pattern = Noise::Pattern.create(pattern_name)
40
- @hkdf_fn = Noise::Functions::Hash.create_hkdf_fn(hash_name)
37
+ # @param [Noise::ProtocolName] protocol_name the parsed name this protocol runs.
38
+ def initialize(protocol_name)
39
+ @protocol_name = protocol_name
40
+ @name = protocol_name.name
41
+ @pattern = Noise::Pattern.create(protocol_name.pattern_name, protocol_name.modifiers)
42
+ @hkdf_fn = Noise::Functions::Hash.create_hkdf_fn(protocol_name.hash_name)
41
43
  @pattern.apply_pattern_modifiers
42
44
 
43
- initialize_fn!(cipher_name, hash_name, dh_name)
45
+ initialize_fn!
46
+ end
47
+
48
+ def psk?
49
+ @pattern.psk?
44
50
  end
45
51
 
46
- def initialize_fn!(cipher_name, hash_name, dh_name)
47
- @cipher_fn = CIPHER[cipher_name]&.new
48
- @hash_fn = HASH[hash_name]&.new
49
- @dh_fn = DH[dh_name]&.new
52
+ private
53
+
54
+ # Looks the three functions up by the names the protocol name gives them.
55
+ #
56
+ # @raise [Noise::Exceptions::ProtocolNameError] if any of the three is one this gem does not
57
+ # implement.
58
+ def initialize_fn!
59
+ @cipher_fn = CIPHER[@protocol_name.cipher_name]&.new
60
+ @hash_fn = HASH[@protocol_name.hash_name]&.new
61
+ @dh_fn = create_dh_fn(@protocol_name.dh_names)
50
62
  raise Noise::Exceptions::ProtocolNameError, "Unsupported function in: #{@name}" unless
51
63
  @cipher_fn && @hash_fn && @dh_fn
52
64
  end
53
65
 
54
- def psk?
55
- @pattern.psk?
66
+ # A name may list more than one DH function, joined with '+', which is how a hybrid handshake
67
+ # is written. This gem runs a single DH function, so any name that lists more than one, or
68
+ # that leaves a member of the list empty, resolves to nothing and is reported as unsupported.
69
+ #
70
+ # The hybrid names the Noise extensions define also carry a modifier, hfs, which
71
+ # Noise::ProtocolName rejects first, so what reaches here today is a name that lists several
72
+ # DH functions and nothing else.
73
+ #
74
+ # @param [Array<String>] names the DH function names the protocol name lists.
75
+ # @return [Object, nil] the DH function, or nil if the name asks for one this gem lacks.
76
+ def create_dh_fn(names)
77
+ return nil unless names.size == 1
78
+
79
+ DH[names.first]&.new
56
80
  end
57
81
  end
58
82
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ # A Noise protocol name, split into the parts that name a handshake pattern and the three
5
+ # functions the protocol runs on.
6
+ #
7
+ # A name has five parts joined with '_': the prefix 'Noise', the handshake pattern with its
8
+ # modifiers, the DH function, the cipher function and the hash function, for example
9
+ # 'Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s'.
10
+ #
11
+ # Two of those parts hold a list of their own. The pattern part is a pattern name followed by
12
+ # the modifiers applied to it, and the DH part is a list joined with '+', because a hybrid
13
+ # handshake names two DH functions at once ('Noise_NNhfs_25519+448_ChaChaPoly_BLAKE2s'). Both
14
+ # are split here, so that every rule about the shape of a name lives in this class alone.
15
+ #
16
+ # Which functions exist is Noise::Protocol's question, not this one's: a name that asks for a
17
+ # cipher, hash or DH function this gem does not implement still parses. Modifiers are the one
18
+ # exception, because a modifier has to be understood before it can be held as a value, so an
19
+ # unknown one is rejected here.
20
+ #
21
+ # The members are the names as written, not the functions themselves. Noise::Protocol resolves
22
+ # them, and calls its resolved functions cipher_fn, hash_fn and dh_fn.
23
+ ProtocolName = Data.define(:name, :pattern_name, :modifiers, :dh_names, :cipher_name,
24
+ :hash_name) do
25
+ # Parses a protocol name.
26
+ #
27
+ # @param [String] name for example 'Noise_XX_25519_ChaChaPoly_SHA256'.
28
+ # @raise [Noise::Exceptions::ProtocolNameError] if the name is not five parts with the 'Noise'
29
+ # prefix and a pattern part whose modifiers can be told from the pattern name.
30
+ # @raise [Noise::Exceptions::UnsupportedModifierError] if a modifier is written correctly but
31
+ # is not one this gem implements.
32
+ # @return [Noise::ProtocolName]
33
+ def self.parse(name)
34
+ # 'Noise', the pattern with its modifiers, the DH functions, the cipher and the hash.
35
+ parts = name.split('_')
36
+ malformed!(name) unless parts.size == 5
37
+
38
+ prefix, pattern_part, dh_part, cipher_name, hash_name = parts
39
+ malformed!(name) unless prefix == 'Noise'
40
+
41
+ # A pattern name is capitals, plus the digit 1 that deferred patterns use (X1K1). Whatever
42
+ # follows it is the modifiers.
43
+ matched = /\A([A-Z1]+)([^A-Z]*)\z/.match(pattern_part)
44
+ malformed!(name) unless matched
45
+
46
+ new(name: name, pattern_name: matched[1], modifiers: parse_modifiers(matched[2]),
47
+ dh_names: split_dh(dh_part), cipher_name: cipher_name, hash_name: hash_name)
48
+ end
49
+
50
+ # @param [String] part the modifiers as written, which is '' when there are none.
51
+ # @return [Array] the modifiers, in the order the name writes them.
52
+ def self.parse_modifiers(part)
53
+ part.split('+').map { |modifier| Modifier.parse(modifier) }.freeze
54
+ end
55
+ private_class_method :parse_modifiers
56
+
57
+ # Keeps the empty members a stray '+' leaves behind, so that '25519+' reads as two DH
58
+ # functions, the second of which has no name, rather than as the single function '25519'.
59
+ # Nothing resolves an empty name, so such a name is reported as unsupported.
60
+ #
61
+ # @param [String] part the DH functions as written.
62
+ # @return [Array<String>] one name per member.
63
+ def self.split_dh(part)
64
+ part.split('+', -1).freeze
65
+ end
66
+ private_class_method :split_dh
67
+
68
+ def self.malformed!(name)
69
+ raise Noise::Exceptions::ProtocolNameError, "Malformed protocol name: #{name}"
70
+ end
71
+ private_class_method :malformed!
72
+
73
+ # @return [String] the name this was parsed from.
74
+ def to_s
75
+ name
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Transport
5
+ # The BOLT #8 transport layer, which turns the message-oriented Noise::Connection into the
6
+ # byte stream the Lightning Network sends over TCP.
7
+ #
8
+ # A Lightning message goes out as two Noise messages: the length of the payload as a two-byte
9
+ # big-endian integer, encrypted on its own, and then the payload. The receiver decrypts the
10
+ # first to learn how many bytes to read for the second. Each direction rotates its key on a
11
+ # schedule of its own, so that a key stolen later cannot decrypt what came before it.
12
+ #
13
+ # Run the handshake with Noise::Connection first, and wrap the finished connection:
14
+ #
15
+ # connection = Noise::Connection::Initiator.new(Noise::Transport::Bolt8::PROTOCOL_NAME,
16
+ # keypairs: { s: local_static, rs: node_id })
17
+ # connection.prologue = Noise::Transport::Bolt8::PROLOGUE
18
+ # connection.start_handshake
19
+ # # ... exchange the three handshake messages over the socket ...
20
+ # transport = Noise::Transport::Bolt8.new(connection, socket)
21
+ #
22
+ # transport.write('a lightning message')
23
+ # message = transport.read
24
+ #
25
+ # The transport takes over the connection's transport phase: it holds the very CipherStates
26
+ # the connection does, so once one is wrapped, stop calling the connection's #encrypt,
27
+ # #decrypt, nonce accessors and rekey methods. Every one of them moves the same key and nonce,
28
+ # and the peer has no way to learn that they did.
29
+ #
30
+ # One transport belongs to one thread, for the reason Noise::Connection::Base gives.
31
+ class Bolt8
32
+ # BOLT #8 fixes the handshake to this protocol, and this prologue.
33
+ PROTOCOL_NAME = 'Noise_XK_secp256k1_ChaChaPoly_SHA256'
34
+ PROLOGUE = 'lightning'
35
+
36
+ # The payload length goes out as a two-byte big-endian integer, so the header is those two
37
+ # bytes plus the authentication tag over them.
38
+ LENGTH_PREFIX_LENGTH = 2
39
+ HEADER_LENGTH = LENGTH_PREFIX_LENGTH + Noise::State::CipherState::TAG_LENGTH
40
+
41
+ # What two bytes of length can express, and what BOLT #8 allows a Lightning message to be.
42
+ # Its authentication tag brings such a message to 65551 bytes, past the 65535 a single Noise
43
+ # transport message may be, so Noise::Connection#encrypt would refuse the largest messages
44
+ # the Lightning Network permits. That is why this class encrypts through the CipherState.
45
+ MAX_PAYLOAD_LENGTH = 65_535
46
+
47
+ # @param [Noise::Connection::Base] connection a finished BOLT #8 handshake.
48
+ # @param [IO] io the stream to read from and write to. See Noise::Transport::Stream.
49
+ # @param [Numeric, nil] read_timeout how long #read waits for the next bytes of a message
50
+ # before giving up, in seconds. See Noise::Transport::Stream.
51
+ # @raise [Noise::Exceptions::HandshakeNotFinishedError] if the handshake has not finished.
52
+ # @raise [Noise::Exceptions::ProtocolNameError] if the connection runs another protocol.
53
+ # @raise [Noise::Exceptions::NoiseValidationError] if the connection is half-duplex.
54
+ # @raise [ArgumentError] if read_timeout is negative, or if the IO cannot be waited on.
55
+ def initialize(connection, io, read_timeout: nil)
56
+ validate(connection)
57
+
58
+ @stream = Stream.new(io, read_timeout: read_timeout)
59
+ hkdf_fn = connection.protocol.hkdf_fn
60
+ # Both directions start from the chaining key the handshake ended with, and rotate away
61
+ # from it independently.
62
+ @send = Direction.new(connection.cipher_state_encrypt, connection.chaining_key, hkdf_fn)
63
+ @receive = Direction.new(connection.cipher_state_decrypt, connection.chaining_key, hkdf_fn)
64
+ end
65
+
66
+ # Encrypts one Lightning message and writes it to the IO: the encrypted length, then the
67
+ # encrypted payload. It returns once all of it has gone out.
68
+ #
69
+ # @param [String] payload the message, at most MAX_PAYLOAD_LENGTH bytes.
70
+ # @raise [Noise::Exceptions::MessageTooLongError] if the payload is longer than that.
71
+ # @raise [IOError] if the IO accepts none of the bytes it is handed.
72
+ # @return [Integer] how many bytes were written.
73
+ def write(payload)
74
+ if payload.bytesize > MAX_PAYLOAD_LENGTH
75
+ raise Noise::Exceptions::MessageTooLongError,
76
+ "Message is #{payload.bytesize} bytes, which exceeds the maximum of #{MAX_PAYLOAD_LENGTH}."
77
+ end
78
+
79
+ @stream.write(@send.encrypt([payload.bytesize].pack('n')) + @send.encrypt(payload))
80
+ end
81
+
82
+ # Reads one Lightning message, blocking until all of it has arrived.
83
+ #
84
+ # Every failure ends the transport. BOLT #8 requires the connection to be closed on a
85
+ # decryption failure, and a message that stops part way through leaves the stream in the
86
+ # middle of one, where the next read would take a payload for a length.
87
+ #
88
+ # An error the IO itself raises, such as Errno::ECONNRESET, comes through as it is.
89
+ #
90
+ # @raise [Noise::Exceptions::TruncatedMessageError] if the stream ends part way through a
91
+ # message.
92
+ # @raise [Noise::Exceptions::ReadTimeoutError] if no more bytes arrive in time.
93
+ # @raise [Noise::Exceptions::DecryptError] if either half fails to authenticate.
94
+ # @return [String, nil] the payload, or nil if the stream ended between messages.
95
+ def read
96
+ header = @stream.read_exactly_or_nil(HEADER_LENGTH)
97
+ return nil if header.nil?
98
+
99
+ length = @receive.decrypt(header).unpack1('n')
100
+ @receive.decrypt(@stream.read_exactly(length + Noise::State::CipherState::TAG_LENGTH))
101
+ end
102
+
103
+ private
104
+
105
+ # BOLT #8 gives each direction a key, a nonce and a chaining key of its own, so a half-duplex
106
+ # connection, which shares one CipherState both ways, cannot carry it. Two Directions over
107
+ # one CipherState would advance a single nonce on both sending and receiving, and rotate to
108
+ # two different chaining keys, which no BOLT #8 peer would follow. Two such parties still
109
+ # talk to each other, so nothing but this check would report it.
110
+ #
111
+ # @param [Noise::Connection::Base] connection the connection to wrap.
112
+ # @return [void]
113
+ def validate(connection)
114
+ unless connection.handshake_finished?
115
+ raise Noise::Exceptions::HandshakeNotFinishedError, 'The handshake has not finished.'
116
+ end
117
+
118
+ unless connection.protocol.name == PROTOCOL_NAME
119
+ raise Noise::Exceptions::ProtocolNameError,
120
+ "BOLT #8 runs #{PROTOCOL_NAME}, not #{connection.protocol.name}."
121
+ end
122
+
123
+ return unless connection.half_duplex?
124
+
125
+ raise Noise::Exceptions::NoiseValidationError,
126
+ 'BOLT #8 gives each direction a key of its own, so it cannot run half-duplex.'
127
+ end
128
+
129
+ # One direction of the transport: the key it encrypts or decrypts with, and the chaining key
130
+ # the next one is derived from. BOLT #8 calls these sk and sck for sending, rk and rck for
131
+ # receiving, and rotates each direction on its own count.
132
+ class Direction
133
+ # BOLT #8 rotates a key once the nonce dedicated to it reaches 1000. A Lightning message
134
+ # is encrypted twice, once for its length and once for its payload, so that is every 500
135
+ # messages.
136
+ KEY_ROTATION_NONCE = 1000
137
+
138
+ # @param [Noise::State::CipherState] cipher_state the transport CipherState Split() gave
139
+ # this direction.
140
+ # @param [String] chaining_key the chaining key the handshake ended with.
141
+ # @param [Proc] hkdf_fn the protocol's HKDF.
142
+ def initialize(cipher_state, chaining_key, hkdf_fn)
143
+ @cipher_state = cipher_state
144
+ @chaining_key = chaining_key
145
+ @hkdf_fn = hkdf_fn
146
+ end
147
+
148
+ # @param [String] plaintext
149
+ # @return [String] the ciphertext and its authentication tag.
150
+ def encrypt(plaintext)
151
+ @cipher_state.encrypt_with_ad('', plaintext).tap { rotate_key_if_due }
152
+ end
153
+
154
+ # @param [String] ciphertext
155
+ # @raise [Noise::Exceptions::DecryptError] if it fails to authenticate, in which case the
156
+ # key is not rotated and the nonce does not move.
157
+ # @return [String] the plaintext.
158
+ def decrypt(ciphertext)
159
+ @cipher_state.decrypt_with_ad('', ciphertext).tap { rotate_key_if_due }
160
+ end
161
+
162
+ private
163
+
164
+ # Replaces this direction's key with HKDF(ck, k), which also restarts the nonce at zero.
165
+ # This is not Noise's REKEY(k): it draws on the chaining key, so a key stolen now says
166
+ # nothing about the keys the direction used before it.
167
+ #
168
+ # @return [void]
169
+ def rotate_key_if_due
170
+ # Every message through this class advances the nonce by one, so the test could be an
171
+ # equality. It is not, so that a nonce moved from outside cannot carry the direction
172
+ # past the point it rotates at and leave it on one key for good.
173
+ return unless @cipher_state.n >= KEY_ROTATION_NONCE
174
+
175
+ @chaining_key, key = @hkdf_fn.call(@chaining_key, @cipher_state.k, 2)
176
+ @cipher_state.initialize_key(key)
177
+ nil
178
+ end
179
+ end
180
+ private_constant :Direction
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Transport
5
+ # A Noise connection framed for a byte stream: every message goes out preceded by its length,
6
+ # so that the reader knows how many bytes to take before decrypting.
7
+ #
8
+ # transport = Noise::Transport::Framed.new(connection, socket)
9
+ # transport.write('a message')
10
+ # message = transport.read
11
+ #
12
+ # The connection learns nothing about the IO, and this class learns nothing about the
13
+ # handshake: it takes one that has already finished.
14
+ #
15
+ # The length goes out in the clear, as two big-endian bytes. It is not secret in any useful
16
+ # sense — anyone watching the stream can time the bytes and count them either way — but it does
17
+ # mean this framing hides nothing about how long each message is. A protocol that has to hide
18
+ # its message sizes pads them, or encrypts the length as BOLT #8 does.
19
+ #
20
+ # Two bytes are enough because a Noise transport message may not exceed 65535 bytes, so a
21
+ # length this can express is never one the connection would refuse. The payload limit is lower
22
+ # by the authentication tag, and Noise::Connection#encrypt is what enforces it.
23
+ #
24
+ # This class holds the connection's transport phase, so once a connection is framed, stop
25
+ # calling its #encrypt and #decrypt directly: a message that goes out unframed leaves the
26
+ # reader taking the next message's bytes for a length. One transport belongs to one thread,
27
+ # for the reason Noise::Connection::Base gives.
28
+ class Framed
29
+ # The length prefix, as two big-endian bytes.
30
+ LENGTH_PREFIX_LENGTH = 2
31
+
32
+ # The longest payload #write takes, which is a Noise transport message less its tag. Two
33
+ # bytes of length cannot announce more than the 65535 a transport message may be, so a
34
+ # declared length is never one the connection would refuse.
35
+ MAX_PAYLOAD_LENGTH = Noise::Connection::Base::MAX_PLAINTEXT_LENGTH
36
+
37
+ # @param [Noise::Connection::Base] connection a connection whose handshake has finished.
38
+ # @param [IO] io the stream to read from and write to. See Noise::Transport::Stream.
39
+ # @param [Numeric, nil] read_timeout how long #read waits for the next bytes of a frame
40
+ # before giving up, in seconds. See Noise::Transport::Stream.
41
+ # @raise [Noise::Exceptions::HandshakeNotFinishedError] if the handshake has not finished.
42
+ # @raise [ArgumentError] if read_timeout is negative, or if the IO cannot be waited on and
43
+ # the timeout would therefore do nothing.
44
+ def initialize(connection, io, read_timeout: nil)
45
+ unless connection.handshake_finished?
46
+ raise Noise::Exceptions::HandshakeNotFinishedError, 'The handshake has not finished.'
47
+ end
48
+
49
+ @connection = connection
50
+ @stream = Stream.new(io, read_timeout: read_timeout)
51
+ end
52
+
53
+ # Encrypts one message and writes it to the IO, preceded by its length. It returns once the
54
+ # whole frame has been written, however many writes that takes.
55
+ #
56
+ # An error the IO itself raises, such as Errno::EPIPE when the peer has gone, comes through
57
+ # as it is.
58
+ #
59
+ # @param [String] payload at most MAX_PAYLOAD_LENGTH bytes.
60
+ # @raise [Noise::Exceptions::MessageTooLongError] if the payload is longer than that.
61
+ # @raise [IOError] if the IO accepts none of the bytes it is handed, which no IO that keeps
62
+ # its side of the bargain does.
63
+ # @return [Integer] how many bytes were written, the payload plus its tag and its length.
64
+ def write(payload)
65
+ ciphertext = @connection.encrypt(payload)
66
+
67
+ @stream.write([ciphertext.bytesize].pack('n') + ciphertext)
68
+ end
69
+
70
+ # Reads one message, waiting until all of it has arrived.
71
+ #
72
+ # Every failure ends the transport, for a different reason each time. A DecryptError says
73
+ # the frame was not written by the party this connection shares a key with, or not written
74
+ # in the order it claims; the stream is still at a frame boundary and the nonce has not
75
+ # moved, so reading on is possible, but reading on from a peer that just failed to
76
+ # authenticate is not something to do. A TruncatedMessageError or a ReadTimeoutError leaves
77
+ # the bytes already taken out of the stream and nowhere to put them, so the next read would
78
+ # take the middle of a frame for a length. Build a new connection instead.
79
+ #
80
+ # An error the IO itself raises, such as Errno::ECONNRESET when the peer disappears without
81
+ # closing, comes through as it is rather than as one of these.
82
+ #
83
+ # @raise [Noise::Exceptions::TruncatedMessageError] if the stream ends part way through a
84
+ # frame.
85
+ # @raise [Noise::Exceptions::ReadTimeoutError] if no more bytes arrive in time.
86
+ # @raise [Noise::Exceptions::DecryptError] if the frame fails to authenticate, which is also
87
+ # how a length too short to hold an authentication tag is reported.
88
+ # @return [String, nil] the payload, or nil if the stream ended between frames, which is how
89
+ # the other party closes without cutting a message in half.
90
+ def read
91
+ prefix = @stream.read_exactly_or_nil(LENGTH_PREFIX_LENGTH)
92
+ return nil if prefix.nil?
93
+
94
+ @connection.decrypt(@stream.read_exactly(prefix.unpack1('n')))
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Transport
5
+ # The IO a framed transport sits on, and the only place that deals with the ways a stream
6
+ # differs from a message: it hands over fewer bytes than were asked for, takes fewer than it
7
+ # was given, ends in the middle of something, or goes quiet.
8
+ #
9
+ # A transport holds one of these and asks it for whole frames. Like the transport above it,
10
+ # one belongs to one thread: a stream has a position, and two threads reading it take halves
11
+ # of each other's frames.
12
+ class Stream
13
+ # @param [IO] io the stream. Anything that answers #write and #readpartial will do, which is
14
+ # what makes a StringIO usable in a test.
15
+ # @param [Numeric, nil] read_timeout how long to wait for the next bytes before giving up,
16
+ # in seconds. nil waits as long as the IO does. It applies to each wait rather than to a
17
+ # frame as a whole, so a peer that sends a byte at a time holds a read open without ever
18
+ # tripping it.
19
+ # @raise [ArgumentError] if read_timeout is negative, or if the IO cannot be waited on and
20
+ # the timeout would therefore do nothing.
21
+ def initialize(io, read_timeout: nil)
22
+ validate_read_timeout!(io, read_timeout)
23
+
24
+ @io = io
25
+ @read_timeout = read_timeout
26
+ end
27
+
28
+ # Writes all of it, however many writes that takes.
29
+ #
30
+ # @param [String] frame the bytes to write.
31
+ # @raise [IOError] if a write takes none of them, which would otherwise spin here forever.
32
+ # @return [Integer] frame.bytesize, once all of it has gone out.
33
+ def write(frame)
34
+ written = 0
35
+ while written < frame.bytesize
36
+ taken = @io.write(frame.byteslice(written, frame.bytesize - written))
37
+ raise IOError, "#{@io.class} took #{taken.inspect} of the #{frame.bytesize} bytes it was given." unless
38
+ taken.is_a?(Integer) && taken.positive?
39
+
40
+ written += taken
41
+ end
42
+ written
43
+ end
44
+
45
+ # @param [Integer] length how many bytes to read.
46
+ # @raise [Noise::Exceptions::TruncatedMessageError] if the stream ends first.
47
+ # @raise [Noise::Exceptions::ReadTimeoutError] if no more arrive in time.
48
+ # @return [String] exactly length bytes.
49
+ def read_exactly(length)
50
+ gather(length) || truncated!(length, 0)
51
+ end
52
+
53
+ # The same, for the first bytes of a frame, where a stream that has ended has not been cut
54
+ # short: it is the other party saying goodbye between one frame and the next.
55
+ #
56
+ # @param [Integer] length how many bytes to read.
57
+ # @raise [Noise::Exceptions::TruncatedMessageError] if the stream ends after some of them.
58
+ # @raise [Noise::Exceptions::ReadTimeoutError] if no more arrive in time.
59
+ # @return [String, nil] exactly length bytes, or nil if the stream had already ended.
60
+ def read_exactly_or_nil(length)
61
+ gather(length)
62
+ end
63
+
64
+ private
65
+
66
+ # @param [Integer] length how many bytes to read.
67
+ # @raise [Noise::Exceptions::TruncatedMessageError] if the stream ends after some of them.
68
+ # @return [String, nil] the bytes, or nil if the stream ended before any of them arrived.
69
+ def gather(length)
70
+ # Binary, because a frame is bytes: appending a chunk to a buffer of another encoding
71
+ # would count characters where this counts bytes.
72
+ buffer = ''.b
73
+ while buffer.bytesize < length
74
+ chunk = read_chunk(length - buffer.bytesize)
75
+ return buffer.empty? ? nil : truncated!(length, buffer.bytesize) if chunk.nil?
76
+
77
+ buffer << chunk
78
+ end
79
+ buffer
80
+ end
81
+
82
+ # @param [Integer] length how many bytes were needed.
83
+ # @param [Integer] arrived how many turned up.
84
+ # @raise [Noise::Exceptions::TruncatedMessageError] always.
85
+ def truncated!(length, arrived)
86
+ raise Noise::Exceptions::TruncatedMessageError,
87
+ "Needed #{length} bytes, the stream ended after #{arrived}."
88
+ end
89
+
90
+ # Takes whatever has arrived, which on a socket is rarely a whole frame at once.
91
+ #
92
+ # @param [Integer] length the most to take.
93
+ # @return [String, nil] the bytes, or nil at end of stream.
94
+ def read_chunk(length)
95
+ wait_readable
96
+ @io.readpartial(length)
97
+ rescue EOFError
98
+ nil
99
+ end
100
+
101
+ # Asks the IO itself to wait, rather than selecting on its file descriptor, because bytes it
102
+ # has already read and buffered are ready to be taken while the descriptor is quiet.
103
+ #
104
+ # @raise [Noise::Exceptions::ReadTimeoutError] if nothing arrives within read_timeout.
105
+ # @return [void]
106
+ def wait_readable
107
+ return if @read_timeout.nil?
108
+ return if @io.wait_readable(@read_timeout)
109
+
110
+ raise Noise::Exceptions::ReadTimeoutError,
111
+ "No more of the frame arrived within #{@read_timeout} seconds."
112
+ end
113
+
114
+ # @param [IO] io the stream this was given.
115
+ # @param [Numeric, nil] read_timeout the timeout it was given.
116
+ # @raise [ArgumentError] if the timeout could not be honoured as asked.
117
+ # @return [void]
118
+ def validate_read_timeout!(io, read_timeout)
119
+ return if read_timeout.nil?
120
+
121
+ raise ArgumentError, "read_timeout is #{read_timeout}, which is not a length of time." if read_timeout.negative?
122
+ return if io.respond_to?(:wait_readable)
123
+
124
+ raise ArgumentError,
125
+ "read_timeout cannot be honoured on a #{io.class}, which does not answer #wait_readable. " \
126
+ 'Leave it out, or pass an IO that does.'
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ # The layers that put a Noise::Connection on top of an IO. A connection encrypts and decrypts
5
+ # whole messages, which is the shape the Noise specification describes but not the shape a
6
+ # socket has, so something has to say where one message ends and the next begins.
7
+ # Stream is not one of those layers but the thing they stand on: the IO, and the ways a stream
8
+ # differs from a message.
9
+ module Transport
10
+ autoload :Bolt8, 'noise/transport/bolt8'
11
+ autoload :Framed, 'noise/transport/framed'
12
+ autoload :Stream, 'noise/transport/stream'
13
+ end
14
+ end
data/lib/noise/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Noise
4
- VERSION = '0.14.0'
4
+ VERSION = '0.15.0'
5
5
  end
data/lib/noise.rb CHANGED
@@ -15,9 +15,14 @@ module Noise
15
15
  autoload :KeyPair, 'noise/key_pair'
16
16
  autoload :Protocol, 'noise/protocol'
17
17
  autoload :Pattern, 'noise/pattern'
18
+ # Noise::Modifier and Noise::Token are declared in the same file as Noise::Pattern.
19
+ autoload :Modifier, 'noise/pattern'
20
+ autoload :Token, 'noise/pattern'
21
+ autoload :ProtocolName, 'noise/protocol_name'
18
22
  autoload :Exceptions, 'noise/exceptions'
19
23
  autoload :Functions, 'noise/functions'
20
24
  autoload :State, 'noise/state'
25
+ autoload :Transport, 'noise/transport'
21
26
 
22
27
  # Some DH and hash functions are backed by a gem, and often a system library, that is only needed
23
28
  # when the function appears in a protocol name. Loading one is therefore allowed to fail; the