noise-ruby 0.10.1 → 0.11.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: cffe6f7ffc789455f6b27bf3246cce8870cf57600e551d8f8edf91437ccd7afd
4
- data.tar.gz: f9c83042c8aa626a7a6e81ad62dd64ccf6c5515c33f1f64e12904b2ffcaa5fa9
3
+ metadata.gz: 876220b34ef23fb45d18cb2dd0dfd7627b5bb202e51f57af9c0cd5a62c211626
4
+ data.tar.gz: 662c47506e70adf05928da7b8c5e3fb48503e0043242f4127459297e29dba476
5
5
  SHA512:
6
- metadata.gz: a1c2fe0e62f0939b48052d348511a97cedcf222316491d43f1d6128d146108587eedeb972d1fbce732a15aa7b73684f76e013e7a57c2f27fe85a3f7331a140bd
7
- data.tar.gz: fa9aea76917642726a8688fbc7b3d0f5fb9b32d1fe058ba7fbf81757d9754da92c1809633378b518753cf105e25e452f65975cfbef0f9b679ec12c68c5198d80
6
+ metadata.gz: 8db525a248cd3a66f1597736745d70470ef109a3e7c7acd9758fda79c5c45aa78350d833c38a4d6d3d2ae9f8a06dd3b6b7f89a6c4928c12ea3dfe899efca4906
7
+ data.tar.gz: 420bd4dbeff8a66570f9d71ec219375aafeb9e5a45d76b1f23f6d9dfe47776b73da860b918f585214983224fea488370dc423f27dbd7c8905c95f33d8a4dce13
data/.dockerignore ADDED
@@ -0,0 +1,8 @@
1
+ # Gemfile.lock is deliberately not listed: the Dockerfile copies it so the image installs the same
2
+ # versions the mounted working tree will ask for.
3
+ .git/
4
+ .rspec_status
5
+ .ruby-lsp/
6
+ coverage/
7
+ pkg/
8
+ tmp/
@@ -14,17 +14,43 @@ on:
14
14
  branches: [ master ]
15
15
 
16
16
  jobs:
17
- test:
18
-
17
+ lint:
19
18
  runs-on: ubuntu-latest
19
+ strategy:
20
+ matrix:
21
+ ruby:
22
+ - '3.3'
23
+ steps:
24
+ - name: Checkout code
25
+ uses: actions/checkout@v4
26
+
27
+ - name: Set up Ruby ${{ matrix.ruby }}
28
+ uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ bundler-cache: true
32
+ - name: Lint code for consistent style
33
+ run: bundle exec rubocop -f github
20
34
 
35
+ test:
36
+ runs-on: ubuntu-latest
37
+ strategy:
38
+ matrix:
39
+ ruby:
40
+ - '3.0'
41
+ - '3.1'
42
+ - '3.2'
43
+ - '3.3'
44
+ - '3.4'
21
45
  steps:
22
- - uses: actions/checkout@v2
23
- - name: Set up Ruby
24
- uses: ruby/setup-ruby@v1
25
- with:
26
- ruby-version: 2.6
27
- - name: Install dependencies
28
- run: bundle install
29
- - name: Run tests
30
- run: bundle exec rake
46
+ - name: Checkout code
47
+ uses: actions/checkout@v4
48
+
49
+ - name: Set up Ruby ${{ matrix.ruby }}
50
+ uses: ruby/setup-ruby@v1
51
+ with:
52
+ ruby-version: ${{ matrix.ruby }}
53
+ bundler-cache: true
54
+ - name: Run tests
55
+ run: bundle exec rspec spec
56
+
data/.gitignore CHANGED
@@ -10,3 +10,6 @@
10
10
 
11
11
  # rspec failure tracking
12
12
  .rspec_status
13
+
14
+ AGENTS.md
15
+ CLAUDE.md
data/.rubocop.yml CHANGED
@@ -1,15 +1,11 @@
1
- Metrics/BlockLength:
1
+ require:
2
+ - rubocop-rspec
3
+ Metrics:
2
4
  Enabled: false
3
5
 
4
- Metrics/LineLength:
6
+ Layout/LineLength:
5
7
  Max: 120
6
8
 
7
- Metrics/MethodLength:
8
- Max: 30
9
-
10
- Metrics/AbcSize:
11
- Max: 20
12
-
13
9
  Style/Documentation:
14
10
  Enabled: false
15
11
 
@@ -23,7 +19,30 @@ Style/WordArray:
23
19
  MinSize: 100
24
20
 
25
21
  AllCops:
26
- TargetRubyVersion: 2.4.1
22
+ TargetRubyVersion: 3.0
23
+ SuggestExtensions: false
27
24
 
28
- Naming/UncommunicativeMethodParamName:
25
+ Naming/MethodParameterName:
29
26
  MinNameLength: 1
27
+
28
+ RSpec:
29
+ Exclude:
30
+ - "spec/factories/*"
31
+ RSpec/AnyInstance:
32
+ Enabled: false
33
+ RSpec/ContextWording:
34
+ Enabled: false
35
+ RSpec/ExampleLength:
36
+ Enabled: false
37
+ RSpec/MultipleExpectations:
38
+ Enabled: false
39
+ RSpec/NamedSubject:
40
+ Enabled: false
41
+ RSpec/NestedGroups:
42
+ Enabled: false
43
+ RSpec/MultipleMemoizedHelpers:
44
+ Enabled: false
45
+ RSpec/SpecFilePathFormat:
46
+ Enabled: false
47
+ RSpec/IndexedLet:
48
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.6
1
+ 3.3.5
data/Dockerfile ADDED
@@ -0,0 +1,29 @@
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).
5
+ FROM --platform=linux/amd64 ruby:3.3
6
+
7
+ # libsodium: dlopen'd by rbnacl, which lib/noise.rb requires unconditionally.
8
+ # cargo: the blake3 gem is a Rust extension and is built at install time.
9
+ # git: noise.gemspec calls `git ls-files` while the gemspec is evaluated.
10
+ RUN apt-get update -qq \
11
+ && apt-get install -y --no-install-recommends build-essential git libsodium23 cargo \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ WORKDIR /work
15
+
16
+ # Install the gems from the dependency definitions alone so the layer stays cached when only
17
+ # the source changes. Gems live in BUNDLE_PATH (/usr/local/bundle), outside /work, so mounting
18
+ # the working tree over /work at run time does not hide them.
19
+ #
20
+ # Gemfile.lock is gitignored, hence the glob: it is copied when the host has one so the image
21
+ # installs the exact versions the mounted lock will ask for at run time, and skipped otherwise.
22
+ COPY Gemfile* noise.gemspec ./
23
+ COPY lib/noise/version.rb lib/noise/version.rb
24
+ RUN bundle install
25
+
26
+ # Run with the working tree mounted:
27
+ # docker run --rm -v "$PWD":/work noise-dev
28
+ # docker run --rm -v "$PWD":/work noise-dev bundle exec rubocop
29
+ CMD ["bundle", "exec", "rspec"]
data/Gemfile CHANGED
@@ -4,11 +4,6 @@ source 'https://rubygems.org'
4
4
 
5
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
6
 
7
- # Specify your gem's dependencies in noise.gemspec
7
+ # Specify your gem's dependencies in noise.gemspec. The optional ed448, secp256k1-ruby and blake3
8
+ # backends are development dependencies there, so the test suite covers all of them.
8
9
  gemspec
9
-
10
- # Use secp256k1 as ecdh function
11
- # gem 'secp256k1-ruby'
12
-
13
- # Use blake3 as hash function
14
- # gem 'blake3'
data/README.md CHANGED
@@ -59,6 +59,12 @@ After installing, define an environment variable as follows:
59
59
 
60
60
  $ export LIBGOLDILOCKS=/usr/local/lib/libgoldilocks.so
61
61
 
62
+ and, add this line to your Gemfile:
63
+
64
+ ```
65
+ gem 'ed448'
66
+ ```
67
+
62
68
  If you use Secp256k1, you must install [libsecp256k1](https://github.com/bitcoin-core/secp256k1).
63
69
 
64
70
  $ git clone https://github.com/bitcoin-core/secp256k1
@@ -3,10 +3,12 @@
3
3
  module Noise
4
4
  module Connection
5
5
  class Base
6
- attr_reader :protocol, :handshake_started, :handshake_finished, :handshake_hash, :handshake_state
7
- attr_reader :cipher_state_encrypt, :cipher_state_decrypt, :cipher_state_handshake
6
+ # The Noise spec caps a handshake or transport message at 65535 bytes.
7
+ MAX_MESSAGE_LENGTH = 65_535
8
+
9
+ attr_reader :protocol, :handshake_started, :handshake_finished, :handshake_hash, :handshake_state,
10
+ :cipher_state_encrypt, :cipher_state_decrypt, :cipher_state_handshake, :s, :rs
8
11
  attr_accessor :psks, :prologue
9
- attr_reader :s, :rs
10
12
 
11
13
  def initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil })
12
14
  @protocol = Protocol.create(name)
@@ -27,11 +29,15 @@ module Noise
27
29
  @handshake_started = true
28
30
  end
29
31
 
32
+ # Restarts the handshake with a fallback pattern, carrying over the keys of the aborted one.
33
+ #
34
+ # The roles swap here: the party that wrote the aborted message now reads, and the one that
35
+ # failed to read it now writes. Both sides are already in that state, so @next_message is
36
+ # deliberately left as it is rather than reset through initialize_next_message.
30
37
  def fallback(fallback_name)
31
38
  @protocol = Protocol.create(fallback_name)
32
39
  @handshake_started = false
33
40
  @handshake_finished = false
34
- # initialize_next_message
35
41
  @local_keypairs = { e: @handshake_state.e, s: @handshake_state.s }
36
42
  @remote_keys = { re: @handshake_state.re, rs: @handshake_state.rs }
37
43
  start_handshake
@@ -55,10 +61,13 @@ module Noise
55
61
  raise Noise::Exceptions::NoiseHandshakeError if @next_message != :write
56
62
  raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
57
63
 
64
+ if @handshake_state.expected_message_length(payload.bytesize) > MAX_MESSAGE_LENGTH
65
+ raise Noise::Exceptions::NoiseHandshakeError, 'Message exceeds the maximum length.'
66
+ end
67
+
58
68
  @next_message = :read
59
69
  buffer = +''
60
- result = @handshake_state.write_message(payload, buffer)
61
- @handshake_finished = true if result
70
+ @handshake_finished = @handshake_state.write_message(payload, buffer)
62
71
  buffer
63
72
  end
64
73
 
@@ -67,11 +76,12 @@ module Noise
67
76
  raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
68
77
  raise Noise::Exceptions::NoiseHandshakeError if @next_message != :read
69
78
  raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
79
+ raise Noise::Exceptions::NoiseHandshakeError, 'Message exceeds the maximum length.' if
80
+ data.bytesize > MAX_MESSAGE_LENGTH
70
81
 
71
82
  @next_message = :write
72
83
  buffer = +''
73
- result = @handshake_state.read_message(data, buffer)
74
- @handshake_finished = true if result
84
+ @handshake_finished = @handshake_state.read_message(data, buffer)
75
85
  buffer
76
86
  end
77
87
 
@@ -93,7 +103,7 @@ module Noise
93
103
  raise Noise::Exceptions::NoisePSKError if @protocol.pattern.psk_count != @psks.count
94
104
  end
95
105
 
96
- def valid_keypairs?
106
+ def missing_keypairs?
97
107
  keypairs = @local_keypairs.merge(@remote_keys)
98
108
  @protocol.pattern.required_keypairs(initiator?).any? { |keypair| !keypairs[keypair] }
99
109
  end
@@ -101,7 +111,7 @@ module Noise
101
111
  def validate
102
112
  validate_psk! if @protocol.psk?
103
113
 
104
- raise Noise::Exceptions::NoiseValidationError if valid_keypairs?
114
+ raise Noise::Exceptions::NoiseValidationError if missing_keypairs?
105
115
 
106
116
  true
107
117
  end
@@ -4,6 +4,7 @@ module Noise
4
4
  module Exceptions
5
5
  class DecryptError < StandardError
6
6
  def initialize(cause)
7
+ super
7
8
  @cause = cause
8
9
  end
9
10
  end
@@ -4,6 +4,7 @@ module Noise
4
4
  module Exceptions
5
5
  class EncryptError < StandardError
6
6
  def initialize(cause)
7
+ super
7
8
  @cause = cause
8
9
  end
9
10
  end
@@ -3,7 +3,10 @@
3
3
  module Noise
4
4
  module Exceptions
5
5
  class InvalidPublicKeyError < StandardError
6
+ attr_reader :public_key
7
+
6
8
  def initialize(public_key)
9
+ super("Invalid public key: #{public_key.unpack1('H*')}")
7
10
  @public_key = public_key
8
11
  end
9
12
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module Exceptions
5
+ class MissingDependencyError < RuntimeError
6
+ end
7
+ end
8
+ end
@@ -6,6 +6,7 @@ module Noise
6
6
  autoload :EncryptError, 'noise/exceptions/encrypt_error'
7
7
  autoload :InvalidPublicKeyError, 'noise/exceptions/invalid_public_key_error'
8
8
  autoload :MaxNonceError, 'noise/exceptions/max_nonce_error'
9
+ autoload :MissingDependencyError, 'noise/exceptions/missing_dependency_error'
9
10
  autoload :ProtocolNameError, 'noise/exceptions/protocol_name_error'
10
11
  autoload :NoiseHandshakeError, 'noise/exceptions/noise_handshake_error'
11
12
  autoload :NoiseValidationError, 'noise/exceptions/noise_validation_error'
@@ -7,28 +7,29 @@ module Noise
7
7
  MAX_NONCE = 2**64 - 1
8
8
 
9
9
  def encrypt(k, n, ad, plaintext)
10
- cipher = OpenSSL::Cipher::AES.new(256, :GCM).encrypt
10
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
11
11
  cipher.key = k
12
12
  cipher.iv = nonce_to_bytes(n)
13
13
  cipher.auth_data = ad
14
- cipher.update(plaintext) + cipher.final + cipher.auth_tag
14
+ update(cipher, plaintext) + cipher.final + cipher.auth_tag
15
15
  rescue OpenSSL::Cipher::CipherError => e
16
16
  raise Noise::Exceptions::EncryptError, "Encrypt failed. #{e.message}", e.backtrace
17
17
  end
18
18
 
19
19
  def decrypt(k, n, ad, ciphertext)
20
- cipher = OpenSSL::Cipher::AES.new(256, :GCM).decrypt
20
+ cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
21
21
  cipher.key = k
22
22
  cipher.iv = nonce_to_bytes(n)
23
23
  cipher.auth_data = ad
24
- cipher.auth_tag = ciphertext[-16..-1]
25
- cipher.update(ciphertext[0...-16]) + cipher.final
24
+ cipher.auth_tag = ciphertext[-16..]
25
+ update(cipher, ciphertext[0...-16]) + cipher.final
26
26
  rescue OpenSSL::Cipher::CipherError => e
27
27
  raise Noise::Exceptions::DecryptError, "Decrpyt failed. #{e.message}", e.backtrace
28
28
  end
29
29
 
30
+ # 4 zero bytes followed by n as a big-endian 64 bit integer.
30
31
  def nonce_to_bytes(n)
31
- "\x00" * 4 + format('%16x', n).htb
32
+ "\x00" * 4 + [n].pack('Q>')
32
33
  end
33
34
 
34
35
  # Returns a new 32-byte cipher key as a pseudorandom function of k.
@@ -40,6 +41,16 @@ module Noise
40
41
  def rekey(k)
41
42
  encrypt(k, MAX_NONCE, '', "\x00" * 32)[0...32]
42
43
  end
44
+
45
+ private
46
+
47
+ # A zero-length payload is normal in a Noise message, but the openssl gem shipped with
48
+ # Ruby 3.0 raises ArgumentError('data must not be empty') instead of returning ''.
49
+ def update(cipher, data)
50
+ return String.new if data.empty?
51
+
52
+ cipher.update(data)
53
+ end
43
54
  end
44
55
  end
45
56
  end
@@ -20,8 +20,9 @@ module Noise
20
20
  raise Noise::Exceptions::DecryptError, "Decrpyt failed. #{e.message}", e.backtrace
21
21
  end
22
22
 
23
+ # 4 zero bytes followed by n as a little-endian 64 bit integer.
23
24
  def nonce_to_bytes(n)
24
- "\x00" * 4 + format('%16x', n).htb.reverse
25
+ "\x00" * 4 + [n].pack('Q<')
25
26
  end
26
27
 
27
28
  # Returns a new 32-byte cipher key as a pseudorandom function of k.
@@ -31,7 +32,7 @@ module Noise
31
32
  # zerolen is a zero-length byte sequence, and zeros is a sequence of
32
33
  # 32 bytes filled with zeros.
33
34
  def rekey(k)
34
- encrypt(k, MAX_NONCE, '', "\x00" * 32)[0..32]
35
+ encrypt(k, MAX_NONCE, '', "\x00" * 32)[0...32]
35
36
  end
36
37
  end
37
38
  end
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_force('ed448') { Ed448.init }
3
+ Noise.require_optional('ed448') { Ed448.init }
4
4
 
5
5
  module Noise
6
6
  module Functions
7
7
  module DH
8
8
  class ED448
9
- DHLEN = Ed448::X448::X448_PRIVATE_BYTES
9
+ # Ed448::X448::X448_PRIVATE_BYTES, spelled out so this file loads without the gem.
10
+ DHLEN = 56
11
+
12
+ def initialize
13
+ Noise.optional_dependency!('ed448')
14
+ end
10
15
 
11
16
  def generate_keypair
12
17
  private_key = SecureRandom.random_bytes(DHLEN)
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_force 'secp256k1'
3
+ Noise.require_optional 'secp256k1'
4
4
 
5
5
  module Noise
6
6
  module Functions
7
7
  module DH
8
8
  class Secp256k1
9
+ def initialize
10
+ Noise.optional_dependency!('secp256k1')
11
+ end
12
+
9
13
  def generate_keypair
10
14
  group = ECDSA::Group::Secp256k1
11
15
  private_key = 1 + SecureRandom.random_number(group.order - 1)
@@ -19,7 +23,7 @@ module Noise
19
23
  def dh(private_key, public_key)
20
24
  key = ::Secp256k1::PublicKey.new(pubkey: public_key, raw: true)
21
25
  key.ecdh(private_key)
22
- rescue ::Secp256k1::AssertError => _
26
+ rescue ::Secp256k1::AssertError
23
27
  raise Noise::Exceptions::InvalidPublicKeyError, public_key
24
28
  end
25
29
 
@@ -60,6 +60,7 @@ module Noise
60
60
  # @return context
61
61
  def init(out_len, key)
62
62
  raise ArgumentError if out_len.zero? || out_len > 32
63
+
63
64
  h = IV.dup
64
65
  h[0] ^= 0x01010000 ^ (key.size << 8) ^ out_len
65
66
  t = 0
@@ -168,6 +169,7 @@ module Noise
168
169
 
169
170
  class Context
170
171
  attr_accessor :b, :h, :t, :c, :out_len
172
+
171
173
  def initialize(b, h, t, c, out_len)
172
174
  @b = b
173
175
  @h = h
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_force 'blake3'
3
+ Noise.require_optional 'blake3'
4
4
 
5
5
  module Noise
6
6
  module Functions
7
7
  module Hash
8
8
  class Blake3
9
- HASHLEN = 64
10
- BLOCKLEN = 128
9
+ HASHLEN = 32
10
+ BLOCKLEN = 64
11
+
12
+ def initialize
13
+ Noise.optional_dependency!('blake3')
14
+ end
15
+
11
16
  def hash(data)
12
17
  ::Blake3.digest(data)
13
18
  end
@@ -10,14 +10,17 @@ module Noise
10
10
  autoload :Blake3, 'noise/functions/hash/blake3'
11
11
 
12
12
  def self.hmac_hash(key, data, digest)
13
- if digest.include?('SHA')
13
+ case digest
14
+ when /SHA/
14
15
  OpenSSL::HMAC.digest(OpenSSL::Digest.new(digest), key, data)
15
- elsif digest.include?('BLAKE2b')
16
+ when /BLAKE2b/
16
17
  Noise::Functions::Hash::Blake2bHMAC.new(key).update(data).digest
17
- elsif digest.include?('BLAKE2s')
18
+ when /BLAKE2s/
18
19
  Noise::Functions::Hash::Blake2sHMAC.new(key).update(data).digest
19
- elsif digest.include?('BLAKE3')
20
+ when /BLAKE3/
20
21
  Noise::Functions::Hash::Blake3HMAC.new(key).update(data).digest
22
+ else
23
+ raise Noise::Exceptions::ProtocolNameError, "Unsupported hash function: #{digest}"
21
24
  end
22
25
  end
23
26
 
@@ -30,9 +33,10 @@ module Noise
30
33
  def self.hkdf(chaining_key, input_key_material, num_outputs, digest)
31
34
  temp_key = hmac_hash(chaining_key, input_key_material, digest)
32
35
  output1 = hmac_hash(temp_key, "\x01", digest)
33
- output2 = hmac_hash(temp_key, output1 + "\x02", digest)
36
+ output2 = hmac_hash(temp_key, "#{output1}\u0002", digest)
34
37
  return [output1, output2] if num_outputs == 2
35
- output3 = hmac_hash(temp_key, output2 + "\x03", digest)
38
+
39
+ output3 = hmac_hash(temp_key, "#{output2}\u0003", digest)
36
40
  [output1, output2, output3]
37
41
  end
38
42
  end
data/lib/noise/pattern.rb CHANGED
@@ -50,6 +50,7 @@ module Noise
50
50
  'se'
51
51
  end
52
52
  end
53
+
53
54
  class TokenSS < TokenDH
54
55
  def get_key(keypair, _initiator)
55
56
  [keypair.s.private_key, keypair.rs]
@@ -59,6 +60,7 @@ module Noise
59
60
  'ss'
60
61
  end
61
62
  end
63
+
62
64
  class TokenPSK
63
65
  def to_s
64
66
  'psk'
@@ -77,6 +79,7 @@ module Noise
77
79
  module Modifier
78
80
  class Psk
79
81
  attr_reader :index
82
+
80
83
  def initialize(index)
81
84
  @index = index
82
85
  end
@@ -85,29 +88,39 @@ module Noise
85
88
  class Fallback
86
89
  end
87
90
 
91
+ PSK_REGEX = /\Apsk(\d+)\z/
92
+
88
93
  def self.parse(s)
89
- if s.start_with?('psk')
90
- index = s.gsub(/psk/, '').to_i
91
- Modifier::Psk.new(index)
92
- elsif s == 'fallback'
93
- Modifier::Fallback.new
94
- else
95
- raise Noise::Exceptions::UnsupportedModifierError
96
- end
94
+ matched = PSK_REGEX.match(s)
95
+ return Modifier::Psk.new(matched[1].to_i) if matched
96
+ return Modifier::Fallback.new if s == 'fallback'
97
+
98
+ raise Noise::Exceptions::UnsupportedModifierError, "Unsupported modifier: #{s}"
97
99
  end
98
100
  end
99
101
 
100
102
  class Pattern
101
- attr_reader :tokens, :modifiers, :psk_count, :fallback
103
+ attr_reader :name, :tokens, :modifiers, :psk_count, :fallback
104
+
105
+ NAME_REGEX = /\A([A-Z1]+)([^A-Z]*)\z/
102
106
 
103
107
  def self.create(name)
104
- pattern_set = name.scan(/([A-Z1]+)([^A-Z]*)/)&.first
105
- pattern = pattern_set&.first
106
- modifiers = pattern_set[1].split('+').map { |s| Modifier.parse(s) }
107
- class_name = "Noise::Pattern#{pattern}"
108
- klass = Object.const_get(class_name)
109
- klass.new(modifiers)
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)
113
+ end
114
+
115
+ def self.pattern_class(pattern)
116
+ klass = Object.const_get("Noise::Pattern#{pattern}")
117
+ raise NameError unless klass.is_a?(Class) && klass < Pattern
118
+
119
+ klass
120
+ rescue NameError
121
+ raise Noise::Exceptions::ProtocolNameError, "Unsupported pattern: #{pattern}"
110
122
  end
123
+ private_class_method :pattern_class
111
124
 
112
125
  def initialize(modifiers)
113
126
  @pre_messages = [[], []]
@@ -126,7 +139,8 @@ module Noise
126
139
  case modifier
127
140
  when Modifier::Psk
128
141
  index = modifier.index
129
- raise Noise::Exceptions::PSKValueError if index / 2 > @tokens.size
142
+ # psk0 prepends to the first message, pskN appends to the Nth one.
143
+ raise Noise::Exceptions::PSKValueError if index > @tokens.size
130
144
 
131
145
  if index.zero?
132
146
  @tokens[0].insert(0, Token::PSK)
@@ -147,24 +161,48 @@ module Noise
147
161
 
148
162
  def required_keypairs_of_initiator
149
163
  required = []
150
- required << :s if %w[K X I].include?(@name[0])
151
- required << :rs if one_way? || @name[1] == 'K'
164
+ required << :s if initiator_static?
165
+ required << :rs if responder_static_pre_shared?
152
166
  required
153
167
  end
154
168
 
155
169
  def required_keypairs_of_responder
156
170
  required = []
157
- required << :rs if @name[0] == 'K'
158
- required << :s if one_way? || %w[K X].include?(@name[1])
171
+ required << :rs if initiator_static_pre_shared?
172
+ required << :s if responder_static?
159
173
  required
160
174
  end
161
175
 
162
176
  def initiator_pre_messages
163
- @pre_messages[0].dup
177
+ (@pre_messages[0] || []).dup
164
178
  end
165
179
 
166
180
  def responder_pre_messages
167
- @pre_messages[1].dup
181
+ (@pre_messages[1] || []).dup
182
+ end
183
+
184
+ # A party needs a static keypair when its static public key is either pre-shared with the peer or
185
+ # transmitted during the handshake. Deriving this from the pattern itself keeps deferred patterns
186
+ # (whose names carry a '1', e.g. X1K) working, unlike inspecting single characters of the name.
187
+ def initiator_static?
188
+ initiator_static_pre_shared? || sends_static?(0)
189
+ end
190
+
191
+ def responder_static?
192
+ responder_static_pre_shared? || sends_static?(1)
193
+ end
194
+
195
+ def initiator_static_pre_shared?
196
+ initiator_pre_messages.include?(Token::S)
197
+ end
198
+
199
+ def responder_static_pre_shared?
200
+ responder_pre_messages.include?(Token::S)
201
+ end
202
+
203
+ # The initiator writes the messages at even indexes, the responder the ones at odd indexes.
204
+ def sends_static?(offset)
205
+ offset.step(@tokens.size - 1, 2).any? { |i| @tokens[i].include?(Token::S) }
168
206
  end
169
207
 
170
208
  def one_way?
@@ -173,10 +211,6 @@ module Noise
173
211
  end
174
212
 
175
213
  class OneWayPattern < Pattern
176
- def initialize(modifiers)
177
- super(modifiers)
178
- end
179
-
180
214
  def one_way?
181
215
  true
182
216
  end
@@ -6,27 +6,31 @@ module Noise
6
6
  attr_reader :name, :pattern
7
7
 
8
8
  CIPHER = {
9
- 'AESGCM': Noise::Functions::Cipher::AesGcm,
10
- 'ChaChaPoly': Noise::Functions::Cipher::ChaChaPoly
11
- }.stringify_keys.freeze
9
+ 'AESGCM' => Noise::Functions::Cipher::AesGcm,
10
+ 'ChaChaPoly' => Noise::Functions::Cipher::ChaChaPoly
11
+ }.freeze
12
12
 
13
13
  DH = {
14
- '25519': Noise::Functions::DH::ED25519,
15
- '448': Noise::Functions::DH::ED448,
16
- 'secp256k1': Noise::Functions::DH::Secp256k1
17
- }.stringify_keys.freeze
14
+ '25519' => Noise::Functions::DH::ED25519,
15
+ '448' => Noise::Functions::DH::ED448,
16
+ 'secp256k1' => Noise::Functions::DH::Secp256k1
17
+ }.freeze
18
18
 
19
19
  HASH = {
20
- 'BLAKE2b': Noise::Functions::Hash::Blake2b,
21
- 'BLAKE2s': Noise::Functions::Hash::Blake2s,
22
- 'SHA256': Noise::Functions::Hash::Sha256,
23
- 'SHA512': Noise::Functions::Hash::Sha512,
24
- 'BLAKE3': Noise::Functions::Hash::Blake3
25
- }.stringify_keys.freeze
20
+ 'BLAKE2b' => Noise::Functions::Hash::Blake2b,
21
+ 'BLAKE2s' => Noise::Functions::Hash::Blake2s,
22
+ 'SHA256' => Noise::Functions::Hash::Sha256,
23
+ 'SHA512' => Noise::Functions::Hash::Sha512,
24
+ 'BLAKE3' => Noise::Functions::Hash::Blake3
25
+ }.freeze
26
26
 
27
27
  def self.create(name)
28
- prefix, pattern_name, dh_name, cipher_name, hash_name = name.split('_')
29
- raise Noise::Exceptions::ProtocolNameError if prefix != 'Noise'
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
+
30
34
  new(name, pattern_name, cipher_name, hash_name, dh_name)
31
35
  end
32
36
 
@@ -43,7 +47,8 @@ module Noise
43
47
  @cipher_fn = CIPHER[cipher_name]&.new
44
48
  @hash_fn = HASH[hash_name]&.new
45
49
  @dh_fn = DH[dh_name]&.new
46
- raise Noise::Exceptions::ProtocolNameError unless @cipher_fn && @hash_fn && @dh_fn
50
+ raise Noise::Exceptions::ProtocolNameError, "Unsupported function in: #{@name}" unless
51
+ @cipher_fn && @hash_fn && @dh_fn
47
52
  end
48
53
 
49
54
  def psk?
@@ -10,10 +10,11 @@ module Noise
10
10
  #
11
11
  class CipherState
12
12
  MAX_NONCE = 2**64 - 1
13
+ TAG_LENGTH = 16
13
14
 
14
15
  attr_reader :k, :n
15
16
 
16
- def initialize(cipher: AesGcm.new)
17
+ def initialize(cipher:)
17
18
  @cipher = cipher
18
19
  end
19
20
 
@@ -36,6 +37,7 @@ module Noise
36
37
  def encrypt_with_ad(ad, plaintext)
37
38
  return plaintext unless key?
38
39
  raise Noise::Exceptions::MaxNonceError if @n == MAX_NONCE
40
+
39
41
  ciphertext = @cipher.encrypt(@k, @n, ad, plaintext)
40
42
  @n += 1
41
43
  ciphertext
@@ -45,6 +47,10 @@ module Noise
45
47
  def decrypt_with_ad(ad, ciphertext)
46
48
  return ciphertext unless key?
47
49
  raise Noise::Exceptions::MaxNonceError if @n == MAX_NONCE
50
+ # Without this the ciphers slice a nil authentication tag out of the truncated input.
51
+ raise Noise::Exceptions::DecryptError, 'Ciphertext is shorter than the tag.' if
52
+ ciphertext.bytesize < TAG_LENGTH
53
+
48
54
  plaintext = @cipher.decrypt(@k, @n, ad, ciphertext)
49
55
  @n += 1
50
56
  plaintext
@@ -17,8 +17,7 @@ module Noise
17
17
  # message_patterns: A sequence of message patterns.
18
18
  # Each message pattern is a sequence of tokens from the set ("e", "s", "ee", "es", "se", "ss").
19
19
  class HandshakeState
20
- attr_reader :message_patterns, :symmetric_state
21
- attr_reader :s, :rs, :e, :re
20
+ attr_reader :message_patterns, :symmetric_state, :s, :rs, :e, :re
22
21
 
23
22
  def initialize(connection, initiator, prologue, local_keypairs, remote_keys)
24
23
  @connection = connection
@@ -32,8 +31,9 @@ module Noise
32
31
 
33
32
  initiator_keypair_getter, responder_keypair_getter = get_keypair_getter(initiator)
34
33
 
35
- # Sets message_patterns to the message patterns from handshake_pattern
36
- @message_patterns = @protocol.pattern.tokens.dup
34
+ # Sets message_patterns to the message patterns from handshake_pattern. The inner arrays are
35
+ # copied too, so consuming them here can never reach back into the shared Pattern.
36
+ @message_patterns = @protocol.pattern.tokens.map(&:dup)
37
37
 
38
38
  process_initiator_pre_messages(initiator_keypair_getter)
39
39
  process_fallback(initiator_keypair_getter)
@@ -49,11 +49,11 @@ module Noise
49
49
  end
50
50
 
51
51
  def local_keypair_getter
52
- ->(token) { instance_variable_get('@' + token.to_s).public_key }
52
+ ->(token) { instance_variable_get("@#{token}").public_key }
53
53
  end
54
54
 
55
55
  def remote_keypair_getter
56
- ->(token) { instance_variable_get('@r' + token.to_s) }
56
+ ->(token) { instance_variable_get("@r#{token}") }
57
57
  end
58
58
 
59
59
  def process_initiator_pre_messages(keypair_getter)
@@ -100,6 +100,7 @@ module Noise
100
100
  end
101
101
 
102
102
  # Takes a payload byte sequence which may be zero-length, and a message_buffer to write the output into
103
+ # @return [Boolean] true if this was the last handshake message, false otherwise.
103
104
  def write_message(payload, message_buffer)
104
105
  pattern = @message_patterns.shift
105
106
 
@@ -118,18 +119,18 @@ module Noise
118
119
  end
119
120
  end
120
121
  message_buffer << @symmetric_state.encrypt_and_hash(payload)
121
- @symmetric_state.split if @message_patterns.empty?
122
+ finish_handshake
122
123
  end
123
124
 
124
125
  # Takes a byte sequence containing a Noise handshake message,
125
126
  # and a payload_buffer to write the message's plaintext payload into
127
+ # @return [Boolean] true if this was the last handshake message, false otherwise.
126
128
  def read_message(message, payload_buffer)
127
129
  pattern = @message_patterns.shift
128
130
  pattern.each do |token|
129
131
  case token
130
132
  when Noise::Token::E
131
- message, re = extract_key(message, false)
132
- @re ||= re
133
+ message, @re = extract_key(message, false)
133
134
  mix_e(@re)
134
135
  when Noise::Token::S
135
136
  message, @rs = extract_key(message, true)
@@ -140,11 +141,19 @@ module Noise
140
141
  end
141
142
  end
142
143
  payload_buffer << @symmetric_state.decrypt_and_hash(message)
143
- @symmetric_state.split if @message_patterns.empty?
144
+ finish_handshake
144
145
  end
145
146
 
146
147
  private
147
148
 
149
+ # Splits into the transport cipher states once every message pattern has been processed.
150
+ def finish_handshake
151
+ return false unless @message_patterns.empty?
152
+
153
+ @symmetric_state.split
154
+ true
155
+ end
156
+
148
157
  def extract_key(message, is_encrypted)
149
158
  len = @protocol.dh_fn.dhlen
150
159
  offset =
@@ -153,8 +162,10 @@ module Noise
153
162
  else
154
163
  0
155
164
  end
165
+ raise Noise::Exceptions::NoiseHandshakeError, 'Message is too short.' if message.bytesize < len + offset
166
+
156
167
  key = message[0...len + offset]
157
- message = message[(len + offset)..-1]
168
+ message = message[(len + offset)..]
158
169
  key = @symmetric_state.decrypt_and_hash(key) if is_encrypted
159
170
  [message, key]
160
171
  end
@@ -8,8 +8,7 @@ module Noise
8
8
  # - h: A hash output of HASHLEN bytes.
9
9
  #
10
10
  class SymmetricState
11
- attr_reader :h, :ck
12
- attr_reader :cipher_state
11
+ attr_reader :h, :ck, :cipher_state
13
12
 
14
13
  def initialize(protocol, connection)
15
14
  @protocol = protocol
@@ -1,11 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class String
4
- def htb
5
- [self].pack('H*')
6
- end
3
+ module Noise
4
+ module Utils
5
+ # Hex conversion helpers. Offered as a refinement so that requiring this gem does not add methods
6
+ # to String for the whole process.
7
+ #
8
+ # using Noise::Utils::HexString
9
+ # '0102'.htb # => "\x01\x02"
10
+ # "\x01\x02".bth # => "0102"
11
+ module HexString
12
+ refine ::String do
13
+ # hex to binary
14
+ def htb
15
+ [self].pack('H*')
16
+ end
7
17
 
8
- def bth
9
- unpack('H*').first
18
+ # binary to hex
19
+ def bth
20
+ unpack1('H*')
21
+ end
22
+ end
23
+ end
10
24
  end
11
25
  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.10.1'
4
+ VERSION = '0.11.0'
5
5
  end
data/lib/noise.rb CHANGED
@@ -8,7 +8,6 @@ require 'rbnacl'
8
8
  require 'ruby_hmac'
9
9
  require 'securerandom'
10
10
 
11
- require 'noise/utils/hash'
12
11
  require 'noise/utils/string'
13
12
 
14
13
  module Noise
@@ -21,14 +20,29 @@ module Noise
21
20
  autoload :Functions, 'noise/functions'
22
21
  autoload :State, 'noise/state'
23
22
 
24
- def self.logger
25
- @logger ||= Logger.new(STDOUT)
26
- end
27
- end
23
+ # Some DH and hash functions are backed by a gem, and often a system library, that is only needed
24
+ # when the function appears in a protocol name. Loading one is therefore allowed to fail; the
25
+ # failure is remembered and reported by optional_dependency! when the function is used.
26
+ @unavailable_dependencies = {}
27
+
28
+ class << self
29
+ def logger
30
+ @logger ||= Logger.new($stdout)
31
+ end
28
32
 
29
- def require_force(name)
30
- require name
31
- yield if block_given?
32
- rescue LoadError => e
33
- Noise.logger.warn(e.message)
33
+ def require_optional(name)
34
+ require name
35
+ yield if block_given?
36
+ rescue LoadError => e
37
+ @unavailable_dependencies[name] = e.message
38
+ logger.warn("Optional dependency '#{name}' is unavailable: #{e.message}")
39
+ end
40
+
41
+ def optional_dependency!(name)
42
+ reason = @unavailable_dependencies[name]
43
+ return if reason.nil?
44
+
45
+ raise Noise::Exceptions::MissingDependencyError, "'#{name}' could not be loaded: #{reason}"
46
+ end
47
+ end
34
48
  end
data/noise.gemspec CHANGED
@@ -5,7 +5,8 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'noise/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = 'noise-ruby'
8
+ spec.name = 'noise-ruby'
9
+ spec.required_ruby_version = '~> 3.0'
9
10
  spec.version = Noise::VERSION
10
11
  spec.authors = ['Hajime Yamaguchi']
11
12
  spec.email = ['gen.yamaguchi0@gmail.com']
@@ -21,14 +22,23 @@ Gem::Specification.new do |spec|
21
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
23
  spec.require_paths = ['lib']
23
24
 
24
- spec.add_development_dependency 'blake3'
25
25
  spec.add_development_dependency 'bundler', '~> 2.0'
26
26
  spec.add_development_dependency 'rake', '>= 12.3.3'
27
27
  spec.add_development_dependency 'rspec', '~> 3.0'
28
+
29
+ spec.add_development_dependency 'rubocop'
30
+ spec.add_development_dependency 'rubocop-rspec'
31
+ spec.add_development_dependency 'simplecov'
32
+ spec.add_development_dependency 'simplecov-json'
33
+
34
+ # Optional backends. Each one is needed only when its function appears in a protocol name, and
35
+ # each also needs a system library that cannot be installed as a gem, so none of them is a runtime
36
+ # dependency. Add the one you need to your own Gemfile; see the README for the system libraries.
37
+ spec.add_development_dependency 'blake3'
38
+ spec.add_development_dependency 'ed448'
28
39
  spec.add_development_dependency 'secp256k1-ruby'
29
40
 
30
41
  spec.add_runtime_dependency 'ecdsa'
31
- spec.add_runtime_dependency 'ed448'
32
42
  spec.add_runtime_dependency 'rbnacl'
33
43
  spec.add_runtime_dependency 'ruby-hmac'
34
44
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noise-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hajime Yamaguchi
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-27 00:00:00.000000000 Z
11
+ date: 2026-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: blake3
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +53,7 @@ dependencies:
67
53
  - !ruby/object:Gem::Version
68
54
  version: '3.0'
69
55
  - !ruby/object:Gem::Dependency
70
- name: secp256k1-ruby
56
+ name: rubocop
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - ">="
@@ -81,13 +67,55 @@ dependencies:
81
67
  - !ruby/object:Gem::Version
82
68
  version: '0'
83
69
  - !ruby/object:Gem::Dependency
84
- name: ecdsa
70
+ name: rubocop-rspec
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - ">="
88
74
  - !ruby/object:Gem::Version
89
75
  version: '0'
90
- type: :runtime
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov-json
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: blake3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
@@ -96,6 +124,34 @@ dependencies:
96
124
  version: '0'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: ed448
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: secp256k1-ruby
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: ecdsa
99
155
  requirement: !ruby/object:Gem::Requirement
100
156
  requirements:
101
157
  - - ">="
@@ -143,12 +199,14 @@ executables: []
143
199
  extensions: []
144
200
  extra_rdoc_files: []
145
201
  files:
202
+ - ".dockerignore"
146
203
  - ".github/workflows/ruby.yml"
147
204
  - ".gitignore"
148
205
  - ".rspec"
149
206
  - ".rubocop.yml"
150
207
  - ".ruby-version"
151
208
  - CODE_OF_CONDUCT.md
209
+ - Dockerfile
152
210
  - Gemfile
153
211
  - README.md
154
212
  - Rakefile
@@ -164,6 +222,7 @@ files:
164
222
  - lib/noise/exceptions/encrypt_error.rb
165
223
  - lib/noise/exceptions/invalid_public_key_error.rb
166
224
  - lib/noise/exceptions/max_nonce_error.rb
225
+ - lib/noise/exceptions/missing_dependency_error.rb
167
226
  - lib/noise/exceptions/noise_handshake_error.rb
168
227
  - lib/noise/exceptions/noise_psk_error.rb
169
228
  - lib/noise/exceptions/noise_validation_error.rb
@@ -192,30 +251,29 @@ files:
192
251
  - lib/noise/state/cipher_state.rb
193
252
  - lib/noise/state/handshake_state.rb
194
253
  - lib/noise/state/symmetric_state.rb
195
- - lib/noise/utils/hash.rb
196
254
  - lib/noise/utils/string.rb
197
255
  - lib/noise/version.rb
198
256
  - noise.gemspec
199
257
  homepage: https://github.com/Yamaguchi/noise
200
258
  licenses: []
201
259
  metadata: {}
202
- post_install_message:
260
+ post_install_message:
203
261
  rdoc_options: []
204
262
  require_paths:
205
263
  - lib
206
264
  required_ruby_version: !ruby/object:Gem::Requirement
207
265
  requirements:
208
- - - ">="
266
+ - - "~>"
209
267
  - !ruby/object:Gem::Version
210
- version: '0'
268
+ version: '3.0'
211
269
  required_rubygems_version: !ruby/object:Gem::Requirement
212
270
  requirements:
213
271
  - - ">="
214
272
  - !ruby/object:Gem::Version
215
273
  version: '0'
216
274
  requirements: []
217
- rubygems_version: 3.1.6
218
- signing_key:
275
+ rubygems_version: 3.5.16
276
+ signing_key:
219
277
  specification_version: 4
220
278
  summary: A Ruby implementation of the Noise Protocol framework
221
279
  test_files: []
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Hash
4
- def stringify_keys
5
- keys.each_with_object({}) do |key, h|
6
- h[key.to_s] = self[key]
7
- end
8
- end
9
- end