noise-ruby 0.13.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 +4 -4
- data/.dockerignore +1 -0
- data/.github/workflows/ruby.yml +39 -0
- data/Dockerfile +3 -3
- data/README.md +164 -7
- data/Rakefile +12 -0
- data/lib/noise/connection/base.rb +188 -24
- data/lib/noise/connection/initiator.rb +6 -3
- data/lib/noise/connection/responder.rb +6 -3
- data/lib/noise/exceptions/handshake_already_finished_error.rb +9 -0
- data/lib/noise/exceptions/handshake_not_finished_error.rb +9 -0
- data/lib/noise/exceptions/handshake_not_started_error.rb +9 -0
- data/lib/noise/exceptions/handshake_turn_error.rb +11 -0
- data/lib/noise/exceptions/read_timeout_error.rb +9 -0
- data/lib/noise/exceptions/truncated_message_error.rb +9 -0
- data/lib/noise/exceptions.rb +6 -0
- data/lib/noise/functions/cipher/cha_cha_poly.rb +31 -6
- data/lib/noise/functions/dh/ed25519.rb +27 -15
- data/lib/noise/functions/hash/blake2b.rb +24 -2
- data/lib/noise/functions/hash/blake3.rb +4 -4
- data/lib/noise/functions/hash/sha256.rb +1 -1
- data/lib/noise/functions/hash/sha512.rb +1 -1
- data/lib/noise/pattern.rb +10 -8
- data/lib/noise/protocol.rb +43 -19
- data/lib/noise/protocol_name.rb +78 -0
- data/lib/noise/transport/bolt8.rb +183 -0
- data/lib/noise/transport/framed.rb +98 -0
- data/lib/noise/transport/stream.rb +130 -0
- data/lib/noise/transport.rb +14 -0
- data/lib/noise/version.rb +1 -1
- data/lib/noise.rb +5 -1
- data/noise.gemspec +10 -10
- metadata +13 -16
|
@@ -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
data/lib/noise.rb
CHANGED
|
@@ -4,7 +4,6 @@ require 'noise/version'
|
|
|
4
4
|
|
|
5
5
|
require 'ecdsa'
|
|
6
6
|
require 'openssl'
|
|
7
|
-
require 'rbnacl'
|
|
8
7
|
require 'ruby_hmac'
|
|
9
8
|
require 'securerandom'
|
|
10
9
|
|
|
@@ -16,9 +15,14 @@ module Noise
|
|
|
16
15
|
autoload :KeyPair, 'noise/key_pair'
|
|
17
16
|
autoload :Protocol, 'noise/protocol'
|
|
18
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'
|
|
19
22
|
autoload :Exceptions, 'noise/exceptions'
|
|
20
23
|
autoload :Functions, 'noise/functions'
|
|
21
24
|
autoload :State, 'noise/state'
|
|
25
|
+
autoload :Transport, 'noise/transport'
|
|
22
26
|
|
|
23
27
|
# Some DH and hash functions are backed by a gem, and often a system library, that is only needed
|
|
24
28
|
# when the function appears in a protocol name. Loading one is therefore allowed to fail; the
|
data/noise.gemspec
CHANGED
|
@@ -15,8 +15,10 @@ Gem::Specification.new do |spec|
|
|
|
15
15
|
spec.description = 'A Ruby implementation of the Noise Protocol framework(http://noiseprotocol.org/).'
|
|
16
16
|
spec.homepage = 'https://github.com/Yamaguchi/noise'
|
|
17
17
|
|
|
18
|
+
# interop/ holds the Rust harness the interoperability suite runs against, which is test
|
|
19
|
+
# material like spec/ and has no business in the packaged gem.
|
|
18
20
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
19
|
-
f.match(%r{^(test|spec|features)/})
|
|
21
|
+
f.match(%r{^(test|spec|features|interop)/})
|
|
20
22
|
end
|
|
21
23
|
spec.bindir = 'exe'
|
|
22
24
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
@@ -30,17 +32,15 @@ Gem::Specification.new do |spec|
|
|
|
30
32
|
spec.add_development_dependency 'simplecov'
|
|
31
33
|
spec.add_development_dependency 'simplecov-json'
|
|
32
34
|
|
|
33
|
-
# Optional backend. BLAKE3 is needed only when it appears in a protocol name,
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
spec.add_development_dependency 'blake3'
|
|
35
|
+
# Optional backend. BLAKE3 is needed only when it appears in a protocol name, so it is not a
|
|
36
|
+
# runtime dependency. Add it to your own Gemfile if you need it; see the README.
|
|
37
|
+
spec.add_development_dependency 'blake3-rb'
|
|
37
38
|
|
|
38
39
|
spec.add_runtime_dependency 'ecdsa'
|
|
39
|
-
# The 448 DH
|
|
40
|
-
# which arrived in openssl 3.0. Every supported interpreter bundles a newer default
|
|
41
|
-
# floor is stated so an older openssl pinned in an application's Gemfile fails to
|
|
42
|
-
# than failing at runtime.
|
|
40
|
+
# The 25519 and 448 DH functions need the raw key API (OpenSSL::PKey.new_raw_private_key and
|
|
41
|
+
# friends), which arrived in openssl 3.0. Every supported interpreter bundles a newer default
|
|
42
|
+
# gem, but the floor is stated so an older openssl pinned in an application's Gemfile fails to
|
|
43
|
+
# resolve rather than failing at runtime.
|
|
43
44
|
spec.add_runtime_dependency 'openssl', '>= 3.0'
|
|
44
|
-
spec.add_runtime_dependency 'rbnacl'
|
|
45
45
|
spec.add_runtime_dependency 'ruby-hmac'
|
|
46
46
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: noise-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hajime Yamaguchi
|
|
@@ -94,7 +94,7 @@ dependencies:
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0'
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: blake3
|
|
97
|
+
name: blake3-rb
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
99
99
|
requirements:
|
|
100
100
|
- - ">="
|
|
@@ -135,20 +135,6 @@ dependencies:
|
|
|
135
135
|
- - ">="
|
|
136
136
|
- !ruby/object:Gem::Version
|
|
137
137
|
version: '3.0'
|
|
138
|
-
- !ruby/object:Gem::Dependency
|
|
139
|
-
name: rbnacl
|
|
140
|
-
requirement: !ruby/object:Gem::Requirement
|
|
141
|
-
requirements:
|
|
142
|
-
- - ">="
|
|
143
|
-
- !ruby/object:Gem::Version
|
|
144
|
-
version: '0'
|
|
145
|
-
type: :runtime
|
|
146
|
-
prerelease: false
|
|
147
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
-
requirements:
|
|
149
|
-
- - ">="
|
|
150
|
-
- !ruby/object:Gem::Version
|
|
151
|
-
version: '0'
|
|
152
138
|
- !ruby/object:Gem::Dependency
|
|
153
139
|
name: ruby-hmac
|
|
154
140
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -191,6 +177,10 @@ files:
|
|
|
191
177
|
- lib/noise/exceptions.rb
|
|
192
178
|
- lib/noise/exceptions/decrypt_error.rb
|
|
193
179
|
- lib/noise/exceptions/encrypt_error.rb
|
|
180
|
+
- lib/noise/exceptions/handshake_already_finished_error.rb
|
|
181
|
+
- lib/noise/exceptions/handshake_not_finished_error.rb
|
|
182
|
+
- lib/noise/exceptions/handshake_not_started_error.rb
|
|
183
|
+
- lib/noise/exceptions/handshake_turn_error.rb
|
|
194
184
|
- lib/noise/exceptions/invalid_nonce_error.rb
|
|
195
185
|
- lib/noise/exceptions/invalid_public_key_error.rb
|
|
196
186
|
- lib/noise/exceptions/max_nonce_error.rb
|
|
@@ -201,6 +191,8 @@ files:
|
|
|
201
191
|
- lib/noise/exceptions/noise_validation_error.rb
|
|
202
192
|
- lib/noise/exceptions/protocol_name_error.rb
|
|
203
193
|
- lib/noise/exceptions/psk_value_error.rb
|
|
194
|
+
- lib/noise/exceptions/read_timeout_error.rb
|
|
195
|
+
- lib/noise/exceptions/truncated_message_error.rb
|
|
204
196
|
- lib/noise/exceptions/unsupported_modifier_error.rb
|
|
205
197
|
- lib/noise/functions.rb
|
|
206
198
|
- lib/noise/functions/cipher.rb
|
|
@@ -220,10 +212,15 @@ files:
|
|
|
220
212
|
- lib/noise/key_pair.rb
|
|
221
213
|
- lib/noise/pattern.rb
|
|
222
214
|
- lib/noise/protocol.rb
|
|
215
|
+
- lib/noise/protocol_name.rb
|
|
223
216
|
- lib/noise/state.rb
|
|
224
217
|
- lib/noise/state/cipher_state.rb
|
|
225
218
|
- lib/noise/state/handshake_state.rb
|
|
226
219
|
- lib/noise/state/symmetric_state.rb
|
|
220
|
+
- lib/noise/transport.rb
|
|
221
|
+
- lib/noise/transport/bolt8.rb
|
|
222
|
+
- lib/noise/transport/framed.rb
|
|
223
|
+
- lib/noise/transport/stream.rb
|
|
227
224
|
- lib/noise/utils/string.rb
|
|
228
225
|
- lib/noise/version.rb
|
|
229
226
|
- noise.gemspec
|