noise-ruby 0.10.1 → 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 +4 -4
- data/.dockerignore +8 -0
- data/.github/workflows/ruby.yml +37 -11
- data/.gitignore +3 -0
- data/.rubocop.yml +29 -10
- data/.ruby-version +1 -1
- data/Dockerfile +29 -0
- data/Gemfile +2 -7
- data/README.md +25 -12
- data/lib/noise/connection/base.rb +107 -16
- data/lib/noise/exceptions/decrypt_error.rb +1 -0
- data/lib/noise/exceptions/encrypt_error.rb +1 -0
- data/lib/noise/exceptions/invalid_nonce_error.rb +9 -0
- data/lib/noise/exceptions/invalid_public_key_error.rb +3 -0
- data/lib/noise/exceptions/message_too_long_error.rb +11 -0
- data/lib/noise/exceptions/missing_dependency_error.rb +8 -0
- data/lib/noise/exceptions.rb +3 -0
- data/lib/noise/functions/cipher/aes_gcm.rb +17 -6
- data/lib/noise/functions/cipher/cha_cha_poly.rb +3 -2
- data/lib/noise/functions/dh/ed25519.rb +12 -0
- data/lib/noise/functions/dh/ed448.rb +31 -9
- data/lib/noise/functions/dh/secp256k1.rb +20 -3
- data/lib/noise/functions/hash/blake2s.rb +2 -0
- data/lib/noise/functions/hash/blake3.rb +8 -3
- data/lib/noise/functions/hash.rb +10 -6
- data/lib/noise/pattern.rb +60 -26
- data/lib/noise/protocol.rb +21 -16
- data/lib/noise/state/cipher_state.rb +20 -1
- data/lib/noise/state/handshake_state.rb +22 -11
- data/lib/noise/state/symmetric_state.rb +1 -2
- data/lib/noise/utils/string.rb +20 -6
- data/lib/noise/version.rb +1 -1
- data/lib/noise.rb +25 -10
- data/noise.gemspec +16 -3
- metadata +86 -26
- data/lib/noise/utils/hash.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a3f4c1b701c2de6240cb5fc0fb2410e1790c390142b256e6b822af4fa327cef
|
|
4
|
+
data.tar.gz: a57735c978c1227262bc122e869aa6866e76a943654981fb57411d70cab34f80
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 915a8da57226d70b24a5a7289325eba851d1e7e40c7d7ef0d0081411008f388a78eec00a760fe92f05ef82ccd707bc776bc8d4d385a6e14d1c3184714b7437b3
|
|
7
|
+
data.tar.gz: dd660c060e00d5c1d0c0e89c434743d6e91749ba09d16a8217e0031c641a2788636ba0fbf64bcfcc031ed84c75e290e3526d95af40fc6518a0211b1e292f7d77
|
data/.dockerignore
ADDED
data/.github/workflows/ruby.yml
CHANGED
|
@@ -14,17 +14,43 @@ on:
|
|
|
14
14
|
branches: [ master ]
|
|
15
15
|
|
|
16
16
|
jobs:
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
ruby-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
data/.rubocop.yml
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
require:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
Metrics:
|
|
2
4
|
Enabled: false
|
|
3
5
|
|
|
4
|
-
|
|
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:
|
|
22
|
+
TargetRubyVersion: 3.0
|
|
23
|
+
SuggestExtensions: false
|
|
27
24
|
|
|
28
|
-
Naming/
|
|
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
|
-
|
|
1
|
+
3.3.5
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# check=skip=FromPlatformFlagConstDisallowed
|
|
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
|
+
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
|
@@ -47,18 +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
50
|
If you use Secp256k1, you must install [libsecp256k1](https://github.com/bitcoin-core/secp256k1).
|
|
63
51
|
|
|
64
52
|
$ git clone https://github.com/bitcoin-core/secp256k1
|
|
@@ -132,6 +120,31 @@ cipher = initiator.encrypt("Hello, World!") # => "\xDA\xC7\xD7as\v\xFA\xCC,\xB3\
|
|
|
132
120
|
plain = responder.decrypt(cipher) # => "Hello, World!"
|
|
133
121
|
```
|
|
134
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
|
+
|
|
135
148
|
## Development
|
|
136
149
|
|
|
137
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,10 +3,15 @@
|
|
|
3
3
|
module Noise
|
|
4
4
|
module Connection
|
|
5
5
|
class Base
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
9
|
+
MAX_MESSAGE_LENGTH = 65_535
|
|
10
|
+
MAX_PLAINTEXT_LENGTH = MAX_MESSAGE_LENGTH - Noise::State::CipherState::TAG_LENGTH
|
|
11
|
+
|
|
12
|
+
attr_reader :protocol, :handshake_started, :handshake_finished, :handshake_hash, :handshake_state,
|
|
13
|
+
:cipher_state_encrypt, :cipher_state_decrypt, :cipher_state_handshake, :s, :rs
|
|
8
14
|
attr_accessor :psks, :prologue
|
|
9
|
-
attr_reader :s, :rs
|
|
10
15
|
|
|
11
16
|
def initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil })
|
|
12
17
|
@protocol = Protocol.create(name)
|
|
@@ -27,11 +32,15 @@ module Noise
|
|
|
27
32
|
@handshake_started = true
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
# Restarts the handshake with a fallback pattern, carrying over the keys of the aborted one.
|
|
36
|
+
#
|
|
37
|
+
# The roles swap here: the party that wrote the aborted message now reads, and the one that
|
|
38
|
+
# failed to read it now writes. Both sides are already in that state, so @next_message is
|
|
39
|
+
# deliberately left as it is rather than reset through initialize_next_message.
|
|
30
40
|
def fallback(fallback_name)
|
|
31
41
|
@protocol = Protocol.create(fallback_name)
|
|
32
42
|
@handshake_started = false
|
|
33
43
|
@handshake_finished = false
|
|
34
|
-
# initialize_next_message
|
|
35
44
|
@local_keypairs = { e: @handshake_state.e, s: @handshake_state.s }
|
|
36
45
|
@remote_keys = { re: @handshake_state.re, rs: @handshake_state.rs }
|
|
37
46
|
start_handshake
|
|
@@ -55,10 +64,13 @@ module Noise
|
|
|
55
64
|
raise Noise::Exceptions::NoiseHandshakeError if @next_message != :write
|
|
56
65
|
raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
|
|
57
66
|
|
|
67
|
+
if @handshake_state.expected_message_length(payload.bytesize) > MAX_MESSAGE_LENGTH
|
|
68
|
+
raise Noise::Exceptions::NoiseHandshakeError, 'Message exceeds the maximum length.'
|
|
69
|
+
end
|
|
70
|
+
|
|
58
71
|
@next_message = :read
|
|
59
72
|
buffer = +''
|
|
60
|
-
|
|
61
|
-
@handshake_finished = true if result
|
|
73
|
+
@handshake_finished = @handshake_state.write_message(payload, buffer)
|
|
62
74
|
buffer
|
|
63
75
|
end
|
|
64
76
|
|
|
@@ -67,33 +79,95 @@ module Noise
|
|
|
67
79
|
raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
|
|
68
80
|
raise Noise::Exceptions::NoiseHandshakeError if @next_message != :read
|
|
69
81
|
raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished
|
|
82
|
+
raise Noise::Exceptions::NoiseHandshakeError, 'Message exceeds the maximum length.' if
|
|
83
|
+
data.bytesize > MAX_MESSAGE_LENGTH
|
|
70
84
|
|
|
71
85
|
@next_message = :write
|
|
72
86
|
buffer = +''
|
|
73
|
-
|
|
74
|
-
@handshake_finished = true if result
|
|
87
|
+
@handshake_finished = @handshake_state.read_message(data, buffer)
|
|
75
88
|
buffer
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
def encrypt(data)
|
|
79
|
-
|
|
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
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
cipher_state.encrypt_with_ad('', data)
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
def decrypt(data)
|
|
85
|
-
|
|
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
|
|
86
149
|
|
|
87
|
-
|
|
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
|
|
88
156
|
end
|
|
89
157
|
|
|
90
158
|
def validate_psk!
|
|
159
|
+
raise Noise::Exceptions::NoisePSKError, 'psks are not set.' if @psks.nil?
|
|
91
160
|
# Invalid psk length! Has to be 32 bytes long
|
|
92
|
-
raise Noise::Exceptions::NoisePSKError
|
|
93
|
-
|
|
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}."
|
|
94
168
|
end
|
|
95
169
|
|
|
96
|
-
def
|
|
170
|
+
def missing_keypairs?
|
|
97
171
|
keypairs = @local_keypairs.merge(@remote_keys)
|
|
98
172
|
@protocol.pattern.required_keypairs(initiator?).any? { |keypair| !keypairs[keypair] }
|
|
99
173
|
end
|
|
@@ -101,7 +175,7 @@ module Noise
|
|
|
101
175
|
def validate
|
|
102
176
|
validate_psk! if @protocol.psk?
|
|
103
177
|
|
|
104
|
-
raise Noise::Exceptions::NoiseValidationError if
|
|
178
|
+
raise Noise::Exceptions::NoiseValidationError if missing_keypairs?
|
|
105
179
|
|
|
106
180
|
true
|
|
107
181
|
end
|
|
@@ -114,6 +188,23 @@ module Noise
|
|
|
114
188
|
@symmetric_state = nil
|
|
115
189
|
@cipher_state_handshake = nil
|
|
116
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
|
|
117
208
|
end
|
|
118
209
|
end
|
|
119
210
|
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
|
data/lib/noise/exceptions.rb
CHANGED
|
@@ -4,8 +4,11 @@ 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'
|
|
11
|
+
autoload :MissingDependencyError, 'noise/exceptions/missing_dependency_error'
|
|
9
12
|
autoload :ProtocolNameError, 'noise/exceptions/protocol_name_error'
|
|
10
13
|
autoload :NoiseHandshakeError, 'noise/exceptions/noise_handshake_error'
|
|
11
14
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
25
|
-
|
|
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 +
|
|
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 +
|
|
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
|
|
35
|
+
encrypt(k, MAX_NONCE, '', "\x00" * 32)[0...32]
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
end
|
|
@@ -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,21 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_force('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
|
-
DHLEN =
|
|
13
|
+
DHLEN = 56
|
|
14
|
+
|
|
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'
|
|
10
18
|
|
|
11
19
|
def generate_keypair
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
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)
|
|
15
22
|
end
|
|
16
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.
|
|
17
32
|
def dh(private_key, public_key)
|
|
18
|
-
|
|
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
|
|
19
41
|
end
|
|
20
42
|
|
|
21
43
|
def dhlen
|
|
@@ -23,8 +45,8 @@ module Noise
|
|
|
23
45
|
end
|
|
24
46
|
|
|
25
47
|
def self.from_private(private_key)
|
|
26
|
-
|
|
27
|
-
Noise::Key.new(private_key,
|
|
48
|
+
pkey = OpenSSL::PKey.new_raw_private_key(ALGORITHM, private_key)
|
|
49
|
+
Noise::Key.new(private_key, pkey.raw_public_key)
|
|
28
50
|
end
|
|
29
51
|
end
|
|
30
52
|
end
|