noise-ruby 0.11.0 → 0.12.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: 876220b34ef23fb45d18cb2dd0dfd7627b5bb202e51f57af9c0cd5a62c211626
4
- data.tar.gz: 662c47506e70adf05928da7b8c5e3fb48503e0043242f4127459297e29dba476
3
+ metadata.gz: 4a3f4c1b701c2de6240cb5fc0fb2410e1790c390142b256e6b822af4fa327cef
4
+ data.tar.gz: a57735c978c1227262bc122e869aa6866e76a943654981fb57411d70cab34f80
5
5
  SHA512:
6
- metadata.gz: 8db525a248cd3a66f1597736745d70470ef109a3e7c7acd9758fda79c5c45aa78350d833c38a4d6d3d2ae9f8a06dd3b6b7f89a6c4928c12ea3dfe899efca4906
7
- data.tar.gz: 420bd4dbeff8a66570f9d71ec219375aafeb9e5a45d76b1f23f6d9dfe47776b73da860b918f585214983224fea488370dc423f27dbd7c8905c95f33d8a4dce13
6
+ metadata.gz: 915a8da57226d70b24a5a7289325eba851d1e7e40c7d7ef0d0081411008f388a78eec00a760fe92f05ef82ccd707bc776bc8d4d385a6e14d1c3184714b7437b3
7
+ data.tar.gz: dd660c060e00d5c1d0c0e89c434743d6e91749ba09d16a8217e0031c641a2788636ba0fbf64bcfcc031ed84c75e290e3526d95af40fc6518a0211b1e292f7d77
data/Dockerfile CHANGED
@@ -1,7 +1,7 @@
1
1
  # check=skip=FromPlatformFlagConstDisallowed
2
- # The test suite loads the x86_64 libsecp256k1 / libgoldilocks bundled in spec/lib, and
3
- # spec/spec_helper.rb points at them unconditionally, so this image has to be linux/amd64
4
- # even on an arm64 host. It mirrors the CI runner (ubuntu x86_64).
2
+ # The test suite loads the x86_64 libsecp256k1 bundled in spec/lib, and spec/spec_helper.rb
3
+ # points at it unconditionally, so this image has to be linux/amd64 even on an arm64 host.
4
+ # It mirrors the CI runner (ubuntu x86_64).
5
5
  FROM --platform=linux/amd64 ruby:3.3
6
6
 
7
7
  # libsodium: dlopen'd by rbnacl, which lib/noise.rb requires unconditionally.
data/README.md CHANGED
@@ -47,24 +47,6 @@ Or install it yourself as:
47
47
 
48
48
  $ gem install noise-ruby
49
49
 
50
- If you use Ed448 as DH function, you must install [libgoldilocks](https://github.com/otrv4/libgoldilocks).
51
-
52
- After installing, define an environment variable as follows:
53
-
54
- * on macOS
55
-
56
- $ export LIBGOLDILOCKS=/usr/local/lib/libgoldilocks.dylib
57
-
58
- * on Linux(Ubuntu)
59
-
60
- $ export LIBGOLDILOCKS=/usr/local/lib/libgoldilocks.so
61
-
62
- and, add this line to your Gemfile:
63
-
64
- ```
65
- gem 'ed448'
66
- ```
67
-
68
50
  If you use Secp256k1, you must install [libsecp256k1](https://github.com/bitcoin-core/secp256k1).
69
51
 
70
52
  $ git clone https://github.com/bitcoin-core/secp256k1
@@ -138,6 +120,31 @@ cipher = initiator.encrypt("Hello, World!") # => "\xDA\xC7\xD7as\v\xFA\xCC,\xB3\
138
120
  plain = responder.decrypt(cipher) # => "Hello, World!"
139
121
  ```
140
122
 
123
+ #### Out-of-order transport messages
124
+
125
+ Each party counts the transport messages of each direction with a nonce. If the transport layer can
126
+ deliver messages out of order or lose them, it has to carry the nonce of each message, and the
127
+ receiver sets the nonce before decrypting. Restore the previous value if the message fails to
128
+ authenticate, so that the following messages are still decryptable.
129
+
130
+ ```
131
+ initiator.encryption_nonce # => 0
132
+ responder.decryption_nonce = 2 # decrypt the message numbered 2 next
133
+ plain = responder.decrypt(cipher)
134
+ ```
135
+
136
+ #### Rekey
137
+
138
+ Rekeying replaces the key of one direction with `REKEY(k)`, so that a key compromised later cannot
139
+ decrypt the messages that came before it. The nonce keeps counting. Both parties must rekey the
140
+ matching direction at the same point of the message stream; when that happens is up to the
141
+ application protocol.
142
+
143
+ ```
144
+ initiator.rekey_encryption
145
+ responder.rekey_decryption
146
+ ```
147
+
141
148
  ## Development
142
149
 
143
150
  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.
@@ -3,8 +3,11 @@
3
3
  module Noise
4
4
  module Connection
5
5
  class Base
6
- # The Noise spec caps a handshake or transport message at 65535 bytes.
6
+ # The Noise spec caps a handshake or transport message at 65535 bytes. A transport message is
7
+ # the ciphertext, so the plaintext a caller may hand to encrypt is shorter by the
8
+ # authentication tag that ENCRYPT() appends.
7
9
  MAX_MESSAGE_LENGTH = 65_535
10
+ MAX_PLAINTEXT_LENGTH = MAX_MESSAGE_LENGTH - Noise::State::CipherState::TAG_LENGTH
8
11
 
9
12
  attr_reader :protocol, :handshake_started, :handshake_finished, :handshake_hash, :handshake_state,
10
13
  :cipher_state_encrypt, :cipher_state_decrypt, :cipher_state_handshake, :s, :rs
@@ -86,21 +89,82 @@ module Noise
86
89
  end
87
90
 
88
91
  def encrypt(data)
89
- raise Noise::Exceptions::NoiseHandshakeError unless @handshake_finished
92
+ cipher_state = transport_cipher_state(:encrypt)
93
+ if data.bytesize > MAX_PLAINTEXT_LENGTH
94
+ raise Noise::Exceptions::MessageTooLongError,
95
+ "Plaintext is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_PLAINTEXT_LENGTH}."
96
+ end
90
97
 
91
- @cipher_state_encrypt.encrypt_with_ad('', data)
98
+ cipher_state.encrypt_with_ad('', data)
92
99
  end
93
100
 
94
101
  def decrypt(data)
95
- raise Noise::Exceptions::NoiseHandshakeError unless @handshake_finished
102
+ cipher_state = transport_cipher_state(:decrypt)
103
+ # Rejected before the cipher state is used, so an over-long message leaves n untouched
104
+ # and the connection usable, exactly as a failed decryption does.
105
+ if data.bytesize > MAX_MESSAGE_LENGTH
106
+ raise Noise::Exceptions::MessageTooLongError,
107
+ "Message is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
108
+ end
109
+
110
+ cipher_state.decrypt_with_ad('', data)
111
+ end
112
+
113
+ # @return [Integer] the nonce the next #encrypt call uses.
114
+ def encryption_nonce
115
+ transport_cipher_state(:encrypt).n
116
+ end
117
+
118
+ # @return [Integer] the nonce the next #decrypt call uses.
119
+ def decryption_nonce
120
+ transport_cipher_state(:decrypt).n
121
+ end
122
+
123
+ # Sets the nonce of the next #encrypt call. Needed when the transport layer numbers the
124
+ # messages itself instead of relying on the sender and the receiver counting in step.
125
+ #
126
+ # @param [Integer] nonce a value between 0 and CipherState::MAX_NONCE.
127
+ def encryption_nonce=(nonce)
128
+ transport_cipher_state(:encrypt).nonce = nonce
129
+ end
130
+
131
+ # Sets the nonce of the next #decrypt call. This is how the Noise spec handles transport
132
+ # messages that arrive out of order: the receiver sets n to the nonce of the message it is
133
+ # about to decrypt, and restores the previous value if the message fails to authenticate.
134
+ #
135
+ # @param [Integer] nonce a value between 0 and CipherState::MAX_NONCE.
136
+ def decryption_nonce=(nonce)
137
+ transport_cipher_state(:decrypt).nonce = nonce
138
+ end
139
+
140
+ # Replaces the key used by #encrypt with REKEY(k), so that the old key cannot decrypt the
141
+ # messages that follow. Both parties must rekey the matching direction at the same point of
142
+ # the message stream, which is up to the application protocol to agree on.
143
+ #
144
+ # @return [void]
145
+ def rekey_encryption
146
+ transport_cipher_state(:encrypt).rekey
147
+ nil
148
+ end
96
149
 
97
- @cipher_state_decrypt.decrypt_with_ad('', data)
150
+ # Replaces the key used by #decrypt with REKEY(k). See #rekey_encryption.
151
+ #
152
+ # @return [void]
153
+ def rekey_decryption
154
+ transport_cipher_state(:decrypt).rekey
155
+ nil
98
156
  end
99
157
 
100
158
  def validate_psk!
159
+ raise Noise::Exceptions::NoisePSKError, 'psks are not set.' if @psks.nil?
101
160
  # Invalid psk length! Has to be 32 bytes long
102
- raise Noise::Exceptions::NoisePSKError if @psks.any? { |psk| psk.bytesize != 32 }
103
- raise Noise::Exceptions::NoisePSKError if @protocol.pattern.psk_count != @psks.count
161
+ raise Noise::Exceptions::NoisePSKError, 'psks have to be 32 bytes long.' if
162
+ @psks.any? { |psk| psk.bytesize != 32 }
163
+
164
+ return if @protocol.pattern.psk_count == @psks.count
165
+
166
+ raise Noise::Exceptions::NoisePSKError,
167
+ "This protocol needs #{@protocol.pattern.psk_count} psks, got #{@psks.count}."
104
168
  end
105
169
 
106
170
  def missing_keypairs?
@@ -124,6 +188,23 @@ module Noise
124
188
  @symmetric_state = nil
125
189
  @cipher_state_handshake = nil
126
190
  end
191
+
192
+ private
193
+
194
+ # Returns the transport CipherState of the given direction.
195
+ #
196
+ # One-way patterns leave the direction the caller cannot use as nil, so the absence of a
197
+ # cipher state is reported the same way as a handshake that has not finished yet.
198
+ #
199
+ # @param [Symbol] direction :encrypt or :decrypt.
200
+ def transport_cipher_state(direction)
201
+ raise Noise::Exceptions::NoiseHandshakeError unless @handshake_finished
202
+
203
+ cipher_state = direction == :encrypt ? @cipher_state_encrypt : @cipher_state_decrypt
204
+ raise Noise::Exceptions::NoiseHandshakeError, "This party cannot #{direction} messages." unless cipher_state
205
+
206
+ cipher_state
207
+ end
127
208
  end
128
209
  end
129
210
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ # Raised when a nonce given to CipherState#nonce= is not an unsigned 64-bit integer.
6
+ class InvalidNonceError < RuntimeError
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 transport message would exceed, or does exceed, the 65535 byte limit the Noise
6
+ # specification places on every message. Kept apart from DecryptError so that a caller can tell a
7
+ # badly framed message from a failed authentication, which has very different security meaning.
8
+ class MessageTooLongError < StandardError
9
+ end
10
+ end
11
+ end
@@ -4,8 +4,10 @@ module Noise
4
4
  module Exceptions
5
5
  autoload :DecryptError, 'noise/exceptions/decrypt_error'
6
6
  autoload :EncryptError, 'noise/exceptions/encrypt_error'
7
+ autoload :InvalidNonceError, 'noise/exceptions/invalid_nonce_error'
7
8
  autoload :InvalidPublicKeyError, 'noise/exceptions/invalid_public_key_error'
8
9
  autoload :MaxNonceError, 'noise/exceptions/max_nonce_error'
10
+ autoload :MessageTooLongError, 'noise/exceptions/message_too_long_error'
9
11
  autoload :MissingDependencyError, 'noise/exceptions/missing_dependency_error'
10
12
  autoload :ProtocolNameError, 'noise/exceptions/protocol_name_error'
11
13
  autoload :NoiseHandshakeError, 'noise/exceptions/noise_handshake_error'
@@ -12,8 +12,20 @@ module Noise
12
12
  Noise::Key.new(ECDSA::Format::IntegerOctetString.encode(private_key, 32), public_key.to_bytes)
13
13
  end
14
14
 
15
+ # Computes the X25519 shared secret for the given remote public key.
16
+ #
17
+ # RbNaCl reports a public key it cannot use in two different ways: a wrong length
18
+ # raises RbNaCl::LengthError, and an all-zero or low-order point raises
19
+ # RbNaCl::CryptoError. Both are translated to InvalidPublicKeyError so that a
20
+ # caller handling a peer-supplied key rescues the same class for every DH function.
21
+ # The length is checked before the call so that RbNaCl::LengthError raised for a
22
+ # malformed private key keeps propagating as itself.
15
23
  def dh(private_key, public_key)
24
+ raise Noise::Exceptions::InvalidPublicKeyError, public_key unless public_key.bytesize == DHLEN
25
+
16
26
  RbNaCl::GroupElement.new(public_key).mult(private_key).to_bytes
27
+ rescue RbNaCl::CryptoError
28
+ raise Noise::Exceptions::InvalidPublicKeyError, public_key
17
29
  end
18
30
 
19
31
  def dhlen
@@ -1,26 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Noise.require_optional('ed448') { Ed448.init }
4
-
5
3
  module Noise
6
4
  module Functions
7
5
  module DH
6
+ # The 448 DH function of the Noise specification, which is X448 as defined by RFC 7748 and
7
+ # not the Ed448 signature scheme the class name suggests. The name is kept because it is what
8
+ # Protocol::DH maps '448' to and what callers reference.
9
+ #
10
+ # OpenSSL implements X448 and exposes it through the raw key interface, so this function needs
11
+ # no gem and no system library beyond the OpenSSL that AesGcm and the HMAC helpers already use.
8
12
  class ED448
9
- # Ed448::X448::X448_PRIVATE_BYTES, spelled out so this file loads without the gem.
10
13
  DHLEN = 56
11
14
 
12
- def initialize
13
- Noise.optional_dependency!('ed448')
14
- end
15
+ # The name OpenSSL knows the curve by. Ed448 is a different algorithm, and asking for it
16
+ # here would produce a signing key rather than one that can derive a shared secret.
17
+ ALGORITHM = 'X448'
15
18
 
16
19
  def generate_keypair
17
- private_key = SecureRandom.random_bytes(DHLEN)
18
- public_key = Ed448::X448.derive_public_key(private_key)
19
- Noise::Key.new(private_key, public_key)
20
+ pkey = OpenSSL::PKey.generate_key(ALGORITHM)
21
+ Noise::Key.new(pkey.raw_private_key, pkey.raw_public_key)
20
22
  end
21
23
 
24
+ # Computes the X448 shared secret for the given remote public key.
25
+ #
26
+ # OpenSSL reports a public key it cannot use as OpenSSL::PKey::PKeyError, both when the key
27
+ # is not DHLEN bytes and when the derivation would produce the all-zero output that RFC 7748
28
+ # requires be rejected. Both are translated to InvalidPublicKeyError so that a caller
29
+ # handling a peer-supplied key rescues the same class for every DH function. The local key
30
+ # is built outside the rescue so that a malformed private key keeps propagating as
31
+ # PKeyError instead of being reported as the peer's fault.
22
32
  def dh(private_key, public_key)
23
- Ed448::X448.dh(public_key, private_key)
33
+ raise Noise::Exceptions::InvalidPublicKeyError, public_key unless public_key.bytesize == DHLEN
34
+
35
+ local = OpenSSL::PKey.new_raw_private_key(ALGORITHM, private_key)
36
+ begin
37
+ local.derive(OpenSSL::PKey.new_raw_public_key(ALGORITHM, public_key))
38
+ rescue OpenSSL::PKey::PKeyError
39
+ raise Noise::Exceptions::InvalidPublicKeyError, public_key
40
+ end
24
41
  end
25
42
 
26
43
  def dhlen
@@ -28,8 +45,8 @@ module Noise
28
45
  end
29
46
 
30
47
  def self.from_private(private_key)
31
- public_key = Ed448::X448.derive_public_key(private_key)
32
- Noise::Key.new(private_key, public_key)
48
+ pkey = OpenSSL::PKey.new_raw_private_key(ALGORITHM, private_key)
49
+ Noise::Key.new(private_key, pkey.raw_public_key)
33
50
  end
34
51
  end
35
52
  end
@@ -6,6 +6,10 @@ module Noise
6
6
  module Functions
7
7
  module DH
8
8
  class Secp256k1
9
+ # Length of a compressed secp256k1 point. libsecp256k1 also accepts the 65-byte
10
+ # uncompressed form, but Noise exchanges only the compressed one.
11
+ DHLEN = 33
12
+
9
13
  def initialize
10
14
  Noise.optional_dependency!('secp256k1')
11
15
  end
@@ -20,7 +24,16 @@ module Noise
20
24
  )
21
25
  end
22
26
 
27
+ # Computes the ECDH shared secret for the given remote public key.
28
+ #
29
+ # A point that is not on the curve makes libsecp256k1 raise Secp256k1::AssertError,
30
+ # while a public key of any other length raises ArgumentError before the point is
31
+ # even parsed. Both are translated to InvalidPublicKeyError, matching the other DH
32
+ # functions. The length is checked here rather than left to the gem so that
33
+ # ArgumentError raised for a malformed private key keeps propagating as itself.
23
34
  def dh(private_key, public_key)
35
+ raise Noise::Exceptions::InvalidPublicKeyError, public_key unless public_key.bytesize == DHLEN
36
+
24
37
  key = ::Secp256k1::PublicKey.new(pubkey: public_key, raw: true)
25
38
  key.ecdh(private_key)
26
39
  rescue ::Secp256k1::AssertError
@@ -28,7 +41,7 @@ module Noise
28
41
  end
29
42
 
30
43
  def dhlen
31
- 33
44
+ DHLEN
32
45
  end
33
46
 
34
47
  def self.from_private(private_key)
@@ -29,7 +29,17 @@ module Noise
29
29
  !@k.nil?
30
30
  end
31
31
 
32
+ # Sets n, the nonce the next encrypt_with_ad or decrypt_with_ad call uses. This is SetNonce()
33
+ # of the Noise spec, needed to decrypt transport messages that arrive out of order.
34
+ #
35
+ # The value is checked here because the ciphers pack n into 8 bytes: a value outside the
36
+ # unsigned 64-bit range silently produces a wrong nonce instead of an error.
37
+ #
38
+ # @param [Integer] nonce a value between 0 and MAX_NONCE.
39
+ # @raise [Noise::Exceptions::InvalidNonceError] if nonce is out of that range.
32
40
  def nonce=(nonce)
41
+ raise Noise::Exceptions::InvalidNonceError unless nonce.is_a?(Integer) && nonce.between?(0, MAX_NONCE)
42
+
33
43
  @n = nonce
34
44
  end
35
45
 
@@ -56,6 +66,9 @@ module Noise
56
66
  plaintext
57
67
  end
58
68
 
69
+ # Replaces k with REKEY(k). n is left as it is, as the Noise spec requires.
70
+ #
71
+ # @return [String] the new 32 bytes key.
59
72
  def rekey
60
73
  @k = @cipher.rekey(@k)
61
74
  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.11.0'
4
+ VERSION = '0.12.0'
5
5
  end
data/lib/noise.rb CHANGED
@@ -4,6 +4,7 @@ require 'noise/version'
4
4
 
5
5
  require 'ecdsa'
6
6
  require 'logger'
7
+ require 'openssl'
7
8
  require 'rbnacl'
8
9
  require 'ruby_hmac'
9
10
  require 'securerandom'
data/noise.gemspec CHANGED
@@ -35,10 +35,13 @@ Gem::Specification.new do |spec|
35
35
  # each also needs a system library that cannot be installed as a gem, so none of them is a runtime
36
36
  # dependency. Add the one you need to your own Gemfile; see the README for the system libraries.
37
37
  spec.add_development_dependency 'blake3'
38
- spec.add_development_dependency 'ed448'
39
38
  spec.add_development_dependency 'secp256k1-ruby'
40
39
 
41
40
  spec.add_runtime_dependency 'ecdsa'
41
+ # The 448 DH function needs the raw key API (OpenSSL::PKey.new_raw_private_key and friends),
42
+ # which arrived in openssl 3.0. Ruby 3.0 still ships 2.2 as its default gem, so the version has
43
+ # to be requested explicitly rather than left to whatever the interpreter bundles.
44
+ spec.add_runtime_dependency 'openssl', '>= 3.0'
42
45
  spec.add_runtime_dependency 'rbnacl'
43
46
  spec.add_runtime_dependency 'ruby-hmac'
44
47
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noise-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hajime Yamaguchi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-31 00:00:00.000000000 Z
11
+ date: 2026-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -123,7 +123,7 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: ed448
126
+ name: secp256k1-ruby
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
@@ -137,13 +137,13 @@ dependencies:
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
- name: secp256k1-ruby
140
+ name: ecdsa
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - ">="
144
144
  - !ruby/object:Gem::Version
145
145
  version: '0'
146
- type: :development
146
+ type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
@@ -151,19 +151,19 @@ dependencies:
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: ecdsa
154
+ name: openssl
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
158
158
  - !ruby/object:Gem::Version
159
- version: '0'
159
+ version: '3.0'
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
- version: '0'
166
+ version: '3.0'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: rbnacl
169
169
  requirement: !ruby/object:Gem::Requirement
@@ -220,8 +220,10 @@ files:
220
220
  - lib/noise/exceptions.rb
221
221
  - lib/noise/exceptions/decrypt_error.rb
222
222
  - lib/noise/exceptions/encrypt_error.rb
223
+ - lib/noise/exceptions/invalid_nonce_error.rb
223
224
  - lib/noise/exceptions/invalid_public_key_error.rb
224
225
  - lib/noise/exceptions/max_nonce_error.rb
226
+ - lib/noise/exceptions/message_too_long_error.rb
225
227
  - lib/noise/exceptions/missing_dependency_error.rb
226
228
  - lib/noise/exceptions/noise_handshake_error.rb
227
229
  - lib/noise/exceptions/noise_psk_error.rb