noise-ruby 0.10.0 → 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 +4 -4
- data/.dockerignore +8 -0
- data/.github/workflows/ruby.yml +56 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +29 -7
- data/.ruby-version +1 -1
- data/Dockerfile +29 -0
- data/Gemfile +2 -7
- data/README.md +6 -0
- data/lib/noise/connection/base.rb +21 -16
- data/lib/noise/exceptions/decrypt_error.rb +1 -0
- data/lib/noise/exceptions/encrypt_error.rb +1 -0
- data/lib/noise/exceptions/invalid_public_key_error.rb +3 -0
- data/lib/noise/exceptions/missing_dependency_error.rb +8 -0
- data/lib/noise/exceptions.rb +1 -0
- data/lib/noise/functions/cipher/aes_gcm.rb +19 -8
- data/lib/noise/functions/cipher/cha_cha_poly.rb +5 -4
- data/lib/noise/functions/dh/ed448.rb +7 -6
- data/lib/noise/functions/dh/secp256k1.rb +6 -5
- data/lib/noise/functions/hash/blake2s.rb +2 -0
- data/lib/noise/functions/hash/blake3.rb +8 -6
- data/lib/noise/functions/hash.rb +10 -6
- data/lib/noise/pattern.rb +153 -30
- data/lib/noise/protocol.rb +23 -21
- data/lib/noise/state/cipher_state.rb +7 -1
- data/lib/noise/state/handshake_state.rb +97 -70
- data/lib/noise/state/symmetric_state.rb +6 -3
- data/lib/noise/utils/string.rb +20 -6
- data/lib/noise/version.rb +1 -1
- data/lib/noise.rb +27 -1
- data/noise.gemspec +16 -6
- metadata +87 -30
- data/.travis.yml +0 -5
- data/lib/noise/utils/hash.rb +0 -9
data/lib/noise/pattern.rb
CHANGED
|
@@ -2,26 +2,125 @@
|
|
|
2
2
|
|
|
3
3
|
module Noise
|
|
4
4
|
module Token
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
class TokenE
|
|
6
|
+
def to_s
|
|
7
|
+
'e'
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class TokenS
|
|
12
|
+
def to_s
|
|
13
|
+
's'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TokenDH
|
|
18
|
+
def mix(symmetric_state, dh_fn, initiator, keypair)
|
|
19
|
+
private_key, public_key = get_key(keypair, initiator)
|
|
20
|
+
symmetric_state.mix_key(dh_fn.dh(private_key, public_key))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class TokenEE < TokenDH
|
|
25
|
+
def get_key(keypair, _initiator)
|
|
26
|
+
[keypair.e.private_key, keypair.re]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
'ee'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class TokenES < TokenDH
|
|
35
|
+
def get_key(keypair, initiator)
|
|
36
|
+
initiator ? [keypair.e.private_key, keypair.rs] : [keypair.s.private_key, keypair.re]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
'es'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class TokenSE < TokenDH
|
|
45
|
+
def get_key(keypair, initiator)
|
|
46
|
+
initiator ? [keypair.s.private_key, keypair.re] : [keypair.e.private_key, keypair.rs]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
'se'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class TokenSS < TokenDH
|
|
55
|
+
def get_key(keypair, _initiator)
|
|
56
|
+
[keypair.s.private_key, keypair.rs]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_s
|
|
60
|
+
'ss'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class TokenPSK
|
|
65
|
+
def to_s
|
|
66
|
+
'psk'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
E = TokenE.new
|
|
71
|
+
S = TokenS.new
|
|
72
|
+
EE = TokenEE.new
|
|
73
|
+
ES = TokenES.new
|
|
74
|
+
SE = TokenSE.new
|
|
75
|
+
SS = TokenSS.new
|
|
76
|
+
PSK = TokenPSK.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
module Modifier
|
|
80
|
+
class Psk
|
|
81
|
+
attr_reader :index
|
|
82
|
+
|
|
83
|
+
def initialize(index)
|
|
84
|
+
@index = index
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Fallback
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
PSK_REGEX = /\Apsk(\d+)\z/
|
|
92
|
+
|
|
93
|
+
def self.parse(s)
|
|
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}"
|
|
99
|
+
end
|
|
12
100
|
end
|
|
13
101
|
|
|
14
102
|
class Pattern
|
|
15
|
-
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/
|
|
16
106
|
|
|
17
107
|
def self.create(name)
|
|
18
|
-
|
|
19
|
-
pattern
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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}"
|
|
24
122
|
end
|
|
123
|
+
private_class_method :pattern_class
|
|
25
124
|
|
|
26
125
|
def initialize(modifiers)
|
|
27
126
|
@pre_messages = [[], []]
|
|
@@ -31,11 +130,17 @@ module Noise
|
|
|
31
130
|
@modifiers = modifiers
|
|
32
131
|
end
|
|
33
132
|
|
|
133
|
+
def psk?
|
|
134
|
+
modifiers.any? { |m| m.is_a? Modifier::Psk }
|
|
135
|
+
end
|
|
136
|
+
|
|
34
137
|
def apply_pattern_modifiers
|
|
35
138
|
@modifiers.each do |modifier|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
139
|
+
case modifier
|
|
140
|
+
when Modifier::Psk
|
|
141
|
+
index = modifier.index
|
|
142
|
+
# psk0 prepends to the first message, pskN appends to the Nth one.
|
|
143
|
+
raise Noise::Exceptions::PSKValueError if index > @tokens.size
|
|
39
144
|
|
|
40
145
|
if index.zero?
|
|
41
146
|
@tokens[0].insert(0, Token::PSK)
|
|
@@ -43,10 +148,8 @@ module Noise
|
|
|
43
148
|
@tokens[index - 1] << Token::PSK
|
|
44
149
|
end
|
|
45
150
|
@psk_count += 1
|
|
46
|
-
|
|
151
|
+
when Modifier::Fallback
|
|
47
152
|
@fallback = true
|
|
48
|
-
else
|
|
49
|
-
raise Noise::Exceptions::UnsupportedModifierError
|
|
50
153
|
end
|
|
51
154
|
end
|
|
52
155
|
end
|
|
@@ -58,24 +161,48 @@ module Noise
|
|
|
58
161
|
|
|
59
162
|
def required_keypairs_of_initiator
|
|
60
163
|
required = []
|
|
61
|
-
required << :s if
|
|
62
|
-
required << :rs if
|
|
164
|
+
required << :s if initiator_static?
|
|
165
|
+
required << :rs if responder_static_pre_shared?
|
|
63
166
|
required
|
|
64
167
|
end
|
|
65
168
|
|
|
66
169
|
def required_keypairs_of_responder
|
|
67
170
|
required = []
|
|
68
|
-
required << :rs if
|
|
69
|
-
required << :s if
|
|
171
|
+
required << :rs if initiator_static_pre_shared?
|
|
172
|
+
required << :s if responder_static?
|
|
70
173
|
required
|
|
71
174
|
end
|
|
72
175
|
|
|
73
176
|
def initiator_pre_messages
|
|
74
|
-
@pre_messages[0].dup
|
|
177
|
+
(@pre_messages[0] || []).dup
|
|
75
178
|
end
|
|
76
179
|
|
|
77
180
|
def responder_pre_messages
|
|
78
|
-
@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) }
|
|
79
206
|
end
|
|
80
207
|
|
|
81
208
|
def one_way?
|
|
@@ -84,10 +211,6 @@ module Noise
|
|
|
84
211
|
end
|
|
85
212
|
|
|
86
213
|
class OneWayPattern < Pattern
|
|
87
|
-
def initialize(modifiers)
|
|
88
|
-
super(modifiers)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
214
|
def one_way?
|
|
92
215
|
true
|
|
93
216
|
end
|
data/lib/noise/protocol.rb
CHANGED
|
@@ -2,32 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module Noise
|
|
4
4
|
class Protocol
|
|
5
|
-
attr_accessor :is_psk_handshake
|
|
6
5
|
attr_accessor :cipher_fn, :hash_fn, :dh_fn, :hkdf_fn
|
|
7
6
|
attr_reader :name, :pattern
|
|
8
7
|
|
|
9
8
|
CIPHER = {
|
|
10
|
-
'AESGCM'
|
|
11
|
-
'ChaChaPoly'
|
|
12
|
-
}.
|
|
9
|
+
'AESGCM' => Noise::Functions::Cipher::AesGcm,
|
|
10
|
+
'ChaChaPoly' => Noise::Functions::Cipher::ChaChaPoly
|
|
11
|
+
}.freeze
|
|
13
12
|
|
|
14
13
|
DH = {
|
|
15
|
-
'25519'
|
|
16
|
-
'448'
|
|
17
|
-
'secp256k1'
|
|
18
|
-
}.
|
|
14
|
+
'25519' => Noise::Functions::DH::ED25519,
|
|
15
|
+
'448' => Noise::Functions::DH::ED448,
|
|
16
|
+
'secp256k1' => Noise::Functions::DH::Secp256k1
|
|
17
|
+
}.freeze
|
|
19
18
|
|
|
20
19
|
HASH = {
|
|
21
|
-
'BLAKE2b'
|
|
22
|
-
'BLAKE2s'
|
|
23
|
-
'SHA256'
|
|
24
|
-
'SHA512'
|
|
25
|
-
'BLAKE3'
|
|
26
|
-
}.
|
|
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
|
|
27
26
|
|
|
28
27
|
def self.create(name)
|
|
29
|
-
|
|
30
|
-
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
|
+
|
|
31
34
|
new(name, pattern_name, cipher_name, hash_name, dh_name)
|
|
32
35
|
end
|
|
33
36
|
|
|
@@ -35,8 +38,6 @@ module Noise
|
|
|
35
38
|
@name = name
|
|
36
39
|
@pattern = Noise::Pattern.create(pattern_name)
|
|
37
40
|
@hkdf_fn = Noise::Functions::Hash.create_hkdf_fn(hash_name)
|
|
38
|
-
@is_psk_handshake = @pattern.modifiers.any? { |m| m.start_with?('psk') }
|
|
39
|
-
|
|
40
41
|
@pattern.apply_pattern_modifiers
|
|
41
42
|
|
|
42
43
|
initialize_fn!(cipher_name, hash_name, dh_name)
|
|
@@ -46,11 +47,12 @@ module Noise
|
|
|
46
47
|
@cipher_fn = CIPHER[cipher_name]&.new
|
|
47
48
|
@hash_fn = HASH[hash_name]&.new
|
|
48
49
|
@dh_fn = DH[dh_name]&.new
|
|
49
|
-
raise Noise::Exceptions::ProtocolNameError
|
|
50
|
+
raise Noise::Exceptions::ProtocolNameError, "Unsupported function in: #{@name}" unless
|
|
51
|
+
@cipher_fn && @hash_fn && @dh_fn
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
def
|
|
53
|
-
@
|
|
54
|
+
def psk?
|
|
55
|
+
@pattern.psk?
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
end
|
|
@@ -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
|
|
|
@@ -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,48 +17,63 @@ 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
|
-
def initialize(connection,
|
|
22
|
+
def initialize(connection, initiator, prologue, local_keypairs, remote_keys)
|
|
24
23
|
@connection = connection
|
|
25
|
-
@protocol = protocol
|
|
26
|
-
@symmetric_state = SymmetricState.
|
|
27
|
-
@symmetric_state.initialize_symmetric(@protocol, connection)
|
|
28
|
-
@symmetric_state.mix_hash(prologue)
|
|
24
|
+
@protocol = connection.protocol
|
|
25
|
+
@symmetric_state = SymmetricState.initialize_symmetric(@protocol, connection, prologue: prologue)
|
|
29
26
|
@initiator = initiator
|
|
30
27
|
@s = local_keypairs[:s]
|
|
31
28
|
@e = local_keypairs[:e]
|
|
32
29
|
@rs = remote_keys[:rs]
|
|
33
30
|
@re = remote_keys[:re]
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
get_remote_keypair = ->(token) { instance_variable_get('@r' + token) }
|
|
32
|
+
initiator_keypair_getter, responder_keypair_getter = get_keypair_getter(initiator)
|
|
37
33
|
|
|
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
|
+
|
|
38
|
+
process_initiator_pre_messages(initiator_keypair_getter)
|
|
39
|
+
process_fallback(initiator_keypair_getter)
|
|
40
|
+
process_responder_pre_messages(responder_keypair_getter)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_keypair_getter(initiator)
|
|
38
44
|
if initiator
|
|
39
|
-
|
|
40
|
-
responder_keypair_getter = get_remote_keypair
|
|
45
|
+
[local_keypair_getter, remote_keypair_getter]
|
|
41
46
|
else
|
|
42
|
-
|
|
43
|
-
responder_keypair_getter = get_local_keypair
|
|
47
|
+
[remote_keypair_getter, local_keypair_getter]
|
|
44
48
|
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def local_keypair_getter
|
|
52
|
+
->(token) { instance_variable_get("@#{token}").public_key }
|
|
53
|
+
end
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
def remote_keypair_getter
|
|
56
|
+
->(token) { instance_variable_get("@r#{token}") }
|
|
57
|
+
end
|
|
48
58
|
|
|
59
|
+
def process_initiator_pre_messages(keypair_getter)
|
|
49
60
|
@protocol.pattern.initiator_pre_messages&.map do |token|
|
|
50
|
-
keypair =
|
|
61
|
+
keypair = keypair_getter.call(token)
|
|
51
62
|
@symmetric_state.mix_hash(keypair)
|
|
52
63
|
end
|
|
64
|
+
end
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
public_key = initiator_keypair_getter.call(message)
|
|
57
|
-
@symmetric_state.mix_hash(public_key)
|
|
58
|
-
end
|
|
66
|
+
def process_fallback(initiator_keypair_getter)
|
|
67
|
+
return unless @protocol.pattern.fallback
|
|
59
68
|
|
|
69
|
+
message = @message_patterns.delete_at(0).first
|
|
70
|
+
public_key = initiator_keypair_getter.call(message)
|
|
71
|
+
@symmetric_state.mix_hash(public_key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def process_responder_pre_messages(keypair_getter)
|
|
60
75
|
@protocol.pattern.responder_pre_messages&.map do |token|
|
|
61
|
-
keypair =
|
|
76
|
+
keypair = keypair_getter.call(token)
|
|
62
77
|
@symmetric_state.mix_hash(keypair)
|
|
63
78
|
end
|
|
64
79
|
end
|
|
@@ -68,13 +83,13 @@ module Noise
|
|
|
68
83
|
pattern = @message_patterns.first
|
|
69
84
|
len = pattern.inject(0) do |l, token|
|
|
70
85
|
case token
|
|
71
|
-
when
|
|
86
|
+
when Noise::Token::E
|
|
72
87
|
l += @protocol.dh_fn.dhlen
|
|
73
|
-
has_key = true if @protocol.
|
|
74
|
-
when
|
|
88
|
+
has_key = true if @protocol.psk?
|
|
89
|
+
when Noise::Token::S
|
|
75
90
|
l += @protocol.dh_fn.dhlen
|
|
76
91
|
l += 16 if has_key
|
|
77
|
-
|
|
92
|
+
else
|
|
78
93
|
has_key = true
|
|
79
94
|
end
|
|
80
95
|
l
|
|
@@ -85,71 +100,83 @@ module Noise
|
|
|
85
100
|
end
|
|
86
101
|
|
|
87
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.
|
|
88
104
|
def write_message(payload, message_buffer)
|
|
89
105
|
pattern = @message_patterns.shift
|
|
90
|
-
dh_fn = @protocol.dh_fn
|
|
91
106
|
|
|
92
107
|
pattern.each do |token|
|
|
93
108
|
case token
|
|
94
|
-
when
|
|
95
|
-
@e
|
|
109
|
+
when Noise::Token::E
|
|
110
|
+
@e ||= @protocol.dh_fn.generate_keypair
|
|
96
111
|
message_buffer << @e.public_key
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
when 's'
|
|
112
|
+
mix_e(@e.public_key)
|
|
113
|
+
when Noise::Token::S
|
|
100
114
|
message_buffer << @symmetric_state.encrypt_and_hash(@s.public_key)
|
|
101
|
-
when
|
|
102
|
-
@symmetric_state.
|
|
103
|
-
when
|
|
104
|
-
|
|
105
|
-
@symmetric_state.mix_key(dh_fn.dh(private_key, public_key))
|
|
106
|
-
when 'se'
|
|
107
|
-
private_key, public_key = @initiator ? [@s.private_key, @re] : [@e.private_key, @rs]
|
|
108
|
-
@symmetric_state.mix_key(dh_fn.dh(private_key, public_key))
|
|
109
|
-
when 'ss'
|
|
110
|
-
@symmetric_state.mix_key(dh_fn.dh(@s.private_key, @rs))
|
|
111
|
-
when 'psk'
|
|
112
|
-
@symmetric_state.mix_key_and_hash(@connection.psks.shift)
|
|
115
|
+
when Noise::Token::EE, Noise::Token::ES, Noise::Token::SE, Noise::Token::SS
|
|
116
|
+
token.mix(@symmetric_state, @protocol.dh_fn, @initiator, self)
|
|
117
|
+
when Noise::Token::PSK
|
|
118
|
+
mix_psk
|
|
113
119
|
end
|
|
114
120
|
end
|
|
115
121
|
message_buffer << @symmetric_state.encrypt_and_hash(payload)
|
|
116
|
-
|
|
122
|
+
finish_handshake
|
|
117
123
|
end
|
|
118
124
|
|
|
119
125
|
# Takes a byte sequence containing a Noise handshake message,
|
|
120
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.
|
|
121
128
|
def read_message(message, payload_buffer)
|
|
122
129
|
pattern = @message_patterns.shift
|
|
123
|
-
dh_fn = @protocol.dh_fn
|
|
124
|
-
len = dh_fn.dhlen
|
|
125
130
|
pattern.each do |token|
|
|
126
131
|
case token
|
|
127
|
-
when
|
|
128
|
-
@re = message
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@
|
|
132
|
-
when
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
@rs = @symmetric_state.decrypt_and_hash(temp)
|
|
137
|
-
when 'ee'
|
|
138
|
-
@symmetric_state.mix_key(dh_fn.dh(@e.private_key, @re))
|
|
139
|
-
when 'es'
|
|
140
|
-
private_key, public_key = @initiator ? [@e.private_key, @rs] : [@s.private_key, @re]
|
|
141
|
-
@symmetric_state.mix_key(dh_fn.dh(private_key, public_key))
|
|
142
|
-
when 'se'
|
|
143
|
-
private_key, public_key = @initiator ? [@s.private_key, @re] : [@e.private_key, @rs]
|
|
144
|
-
@symmetric_state.mix_key(dh_fn.dh(private_key, public_key))
|
|
145
|
-
when 'ss'
|
|
146
|
-
@symmetric_state.mix_key(dh_fn.dh(@s.private_key, @rs))
|
|
147
|
-
when 'psk'
|
|
148
|
-
@symmetric_state.mix_key_and_hash(@connection.psks.shift)
|
|
132
|
+
when Noise::Token::E
|
|
133
|
+
message, @re = extract_key(message, false)
|
|
134
|
+
mix_e(@re)
|
|
135
|
+
when Noise::Token::S
|
|
136
|
+
message, @rs = extract_key(message, true)
|
|
137
|
+
when Noise::Token::EE, Noise::Token::ES, Noise::Token::SE, Noise::Token::SS
|
|
138
|
+
token.mix(@symmetric_state, @protocol.dh_fn, @initiator, self)
|
|
139
|
+
when Noise::Token::PSK
|
|
140
|
+
mix_psk
|
|
149
141
|
end
|
|
150
142
|
end
|
|
151
143
|
payload_buffer << @symmetric_state.decrypt_and_hash(message)
|
|
152
|
-
|
|
144
|
+
finish_handshake
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
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
|
+
|
|
157
|
+
def extract_key(message, is_encrypted)
|
|
158
|
+
len = @protocol.dh_fn.dhlen
|
|
159
|
+
offset =
|
|
160
|
+
if is_encrypted && @connection.cipher_state_handshake.key?
|
|
161
|
+
16
|
|
162
|
+
else
|
|
163
|
+
0
|
|
164
|
+
end
|
|
165
|
+
raise Noise::Exceptions::NoiseHandshakeError, 'Message is too short.' if message.bytesize < len + offset
|
|
166
|
+
|
|
167
|
+
key = message[0...len + offset]
|
|
168
|
+
message = message[(len + offset)..]
|
|
169
|
+
key = @symmetric_state.decrypt_and_hash(key) if is_encrypted
|
|
170
|
+
[message, key]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def mix_e(public_key)
|
|
174
|
+
@symmetric_state.mix_hash(public_key)
|
|
175
|
+
@symmetric_state.mix_key(public_key) if @protocol.psk?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def mix_psk
|
|
179
|
+
@symmetric_state.mix_key_and_hash(@connection.psks.shift)
|
|
153
180
|
end
|
|
154
181
|
end
|
|
155
182
|
end
|
|
@@ -8,10 +8,9 @@ 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
|
-
def
|
|
13
|
+
def initialize(protocol, connection)
|
|
15
14
|
@protocol = protocol
|
|
16
15
|
@connection = connection
|
|
17
16
|
@ck = @h = initialize_h(protocol)
|
|
@@ -20,6 +19,10 @@ module Noise
|
|
|
20
19
|
@cipher_state.initialize_key(nil)
|
|
21
20
|
end
|
|
22
21
|
|
|
22
|
+
def self.initialize_symmetric(protocol, connection, prologue: nil)
|
|
23
|
+
new(protocol, connection).tap { |s| s.mix_hash(prologue) if prologue }
|
|
24
|
+
end
|
|
25
|
+
|
|
23
26
|
def mix_key(input_key_meterial)
|
|
24
27
|
@ck, temp_k = @protocol.hkdf_fn.call(@ck, input_key_meterial, 2)
|
|
25
28
|
temp_k = truncate(temp_k)
|
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
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
require 'noise/version'
|
|
4
4
|
|
|
5
5
|
require 'ecdsa'
|
|
6
|
+
require 'logger'
|
|
6
7
|
require 'rbnacl'
|
|
7
8
|
require 'ruby_hmac'
|
|
8
9
|
require 'securerandom'
|
|
9
10
|
|
|
10
|
-
require 'noise/utils/hash'
|
|
11
11
|
require 'noise/utils/string'
|
|
12
12
|
|
|
13
13
|
module Noise
|
|
@@ -19,4 +19,30 @@ module Noise
|
|
|
19
19
|
autoload :Exceptions, 'noise/exceptions'
|
|
20
20
|
autoload :Functions, 'noise/functions'
|
|
21
21
|
autoload :State, 'noise/state'
|
|
22
|
+
|
|
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
|
|
32
|
+
|
|
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
|
|
22
48
|
end
|