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
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Noise.require_optional 'secp256k1'
|
|
4
4
|
|
|
5
5
|
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
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
Noise.optional_dependency!('secp256k1')
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
def generate_keypair
|
|
10
18
|
group = ECDSA::Group::Secp256k1
|
|
11
19
|
private_key = 1 + SecureRandom.random_number(group.order - 1)
|
|
@@ -16,15 +24,24 @@ module Noise
|
|
|
16
24
|
)
|
|
17
25
|
end
|
|
18
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.
|
|
19
34
|
def dh(private_key, public_key)
|
|
35
|
+
raise Noise::Exceptions::InvalidPublicKeyError, public_key unless public_key.bytesize == DHLEN
|
|
36
|
+
|
|
20
37
|
key = ::Secp256k1::PublicKey.new(pubkey: public_key, raw: true)
|
|
21
38
|
key.ecdh(private_key)
|
|
22
|
-
rescue ::Secp256k1::AssertError
|
|
39
|
+
rescue ::Secp256k1::AssertError
|
|
23
40
|
raise Noise::Exceptions::InvalidPublicKeyError, public_key
|
|
24
41
|
end
|
|
25
42
|
|
|
26
43
|
def dhlen
|
|
27
|
-
|
|
44
|
+
DHLEN
|
|
28
45
|
end
|
|
29
46
|
|
|
30
47
|
def self.from_private(private_key)
|
|
@@ -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
|
-
|
|
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 =
|
|
10
|
-
BLOCKLEN =
|
|
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
|
data/lib/noise/functions/hash.rb
CHANGED
|
@@ -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
|
-
|
|
13
|
+
case digest
|
|
14
|
+
when /SHA/
|
|
14
15
|
OpenSSL::HMAC.digest(OpenSSL::Digest.new(digest), key, data)
|
|
15
|
-
|
|
16
|
+
when /BLAKE2b/
|
|
16
17
|
Noise::Functions::Hash::Blake2bHMAC.new(key).update(data).digest
|
|
17
|
-
|
|
18
|
+
when /BLAKE2s/
|
|
18
19
|
Noise::Functions::Hash::Blake2sHMAC.new(key).update(data).digest
|
|
19
|
-
|
|
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
|
|
36
|
+
output2 = hmac_hash(temp_key, "#{output1}\u0002", digest)
|
|
34
37
|
return [output1, output2] if num_outputs == 2
|
|
35
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
105
|
-
pattern
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
151
|
-
required << :rs if
|
|
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
|
|
158
|
-
required << :s if
|
|
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
|
data/lib/noise/protocol.rb
CHANGED
|
@@ -6,27 +6,31 @@ module Noise
|
|
|
6
6
|
attr_reader :name, :pattern
|
|
7
7
|
|
|
8
8
|
CIPHER = {
|
|
9
|
-
'AESGCM'
|
|
10
|
-
'ChaChaPoly'
|
|
11
|
-
}.
|
|
9
|
+
'AESGCM' => Noise::Functions::Cipher::AesGcm,
|
|
10
|
+
'ChaChaPoly' => Noise::Functions::Cipher::ChaChaPoly
|
|
11
|
+
}.freeze
|
|
12
12
|
|
|
13
13
|
DH = {
|
|
14
|
-
'25519'
|
|
15
|
-
'448'
|
|
16
|
-
'secp256k1'
|
|
17
|
-
}.
|
|
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'
|
|
21
|
-
'BLAKE2s'
|
|
22
|
-
'SHA256'
|
|
23
|
-
'SHA512'
|
|
24
|
-
'BLAKE3'
|
|
25
|
-
}.
|
|
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
|
-
|
|
29
|
-
raise Noise::Exceptions::ProtocolNameError
|
|
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
|
|
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:
|
|
17
|
+
def initialize(cipher:)
|
|
17
18
|
@cipher = cipher
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -28,7 +29,17 @@ module Noise
|
|
|
28
29
|
!@k.nil?
|
|
29
30
|
end
|
|
30
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.
|
|
31
40
|
def nonce=(nonce)
|
|
41
|
+
raise Noise::Exceptions::InvalidNonceError unless nonce.is_a?(Integer) && nonce.between?(0, MAX_NONCE)
|
|
42
|
+
|
|
32
43
|
@n = nonce
|
|
33
44
|
end
|
|
34
45
|
|
|
@@ -36,6 +47,7 @@ module Noise
|
|
|
36
47
|
def encrypt_with_ad(ad, plaintext)
|
|
37
48
|
return plaintext unless key?
|
|
38
49
|
raise Noise::Exceptions::MaxNonceError if @n == MAX_NONCE
|
|
50
|
+
|
|
39
51
|
ciphertext = @cipher.encrypt(@k, @n, ad, plaintext)
|
|
40
52
|
@n += 1
|
|
41
53
|
ciphertext
|
|
@@ -45,11 +57,18 @@ module Noise
|
|
|
45
57
|
def decrypt_with_ad(ad, ciphertext)
|
|
46
58
|
return ciphertext unless key?
|
|
47
59
|
raise Noise::Exceptions::MaxNonceError if @n == MAX_NONCE
|
|
60
|
+
# Without this the ciphers slice a nil authentication tag out of the truncated input.
|
|
61
|
+
raise Noise::Exceptions::DecryptError, 'Ciphertext is shorter than the tag.' if
|
|
62
|
+
ciphertext.bytesize < TAG_LENGTH
|
|
63
|
+
|
|
48
64
|
plaintext = @cipher.decrypt(@k, @n, ad, ciphertext)
|
|
49
65
|
@n += 1
|
|
50
66
|
plaintext
|
|
51
67
|
end
|
|
52
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.
|
|
53
72
|
def rekey
|
|
54
73
|
@k = @cipher.rekey(@k)
|
|
55
74
|
end
|
|
@@ -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
|
-
|
|
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(
|
|
52
|
+
->(token) { instance_variable_get("@#{token}").public_key }
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def remote_keypair_getter
|
|
56
|
-
->(token) { instance_variable_get(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
168
|
+
message = message[(len + offset)..]
|
|
158
169
|
key = @symmetric_state.decrypt_and_hash(key) if is_encrypted
|
|
159
170
|
[message, key]
|
|
160
171
|
end
|
data/lib/noise/utils/string.rb
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
data/lib/noise.rb
CHANGED
|
@@ -4,11 +4,11 @@ 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'
|
|
10
11
|
|
|
11
|
-
require 'noise/utils/hash'
|
|
12
12
|
require 'noise/utils/string'
|
|
13
13
|
|
|
14
14
|
module Noise
|
|
@@ -21,14 +21,29 @@ module Noise
|
|
|
21
21
|
autoload :Functions, 'noise/functions'
|
|
22
22
|
autoload :State, 'noise/state'
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
# Some DH and hash functions are backed by a gem, and often a system library, that is only needed
|
|
25
|
+
# when the function appears in a protocol name. Loading one is therefore allowed to fail; the
|
|
26
|
+
# failure is remembered and reported by optional_dependency! when the function is used.
|
|
27
|
+
@unavailable_dependencies = {}
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def logger
|
|
31
|
+
@logger ||= Logger.new($stdout)
|
|
32
|
+
end
|
|
28
33
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
rescue LoadError => e
|
|
33
|
-
|
|
34
|
+
def require_optional(name)
|
|
35
|
+
require name
|
|
36
|
+
yield if block_given?
|
|
37
|
+
rescue LoadError => e
|
|
38
|
+
@unavailable_dependencies[name] = e.message
|
|
39
|
+
logger.warn("Optional dependency '#{name}' is unavailable: #{e.message}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def optional_dependency!(name)
|
|
43
|
+
reason = @unavailable_dependencies[name]
|
|
44
|
+
return if reason.nil?
|
|
45
|
+
|
|
46
|
+
raise Noise::Exceptions::MissingDependencyError, "'#{name}' could not be loaded: #{reason}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
34
49
|
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
|
|
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,26 @@ 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'
|
|
28
38
|
spec.add_development_dependency 'secp256k1-ruby'
|
|
29
39
|
|
|
30
40
|
spec.add_runtime_dependency 'ecdsa'
|
|
31
|
-
|
|
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'
|
|
32
45
|
spec.add_runtime_dependency 'rbnacl'
|
|
33
46
|
spec.add_runtime_dependency 'ruby-hmac'
|
|
34
47
|
end
|