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.
data/lib/noise/pattern.rb CHANGED
@@ -2,26 +2,125 @@
2
2
 
3
3
  module Noise
4
4
  module Token
5
- E = 'e'
6
- S = 's'
7
- EE = 'ee'
8
- ES = 'es'
9
- SE = 'se'
10
- SS = 'ss'
11
- PSK = 'psk'
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
- pattern_set = name.scan(/([A-Z1]+)([^A-Z]*)/)&.first
19
- pattern = pattern_set&.first
20
- modifiers = pattern_set[1].split('+')
21
- class_name = "Noise::Pattern#{pattern}"
22
- klass = Object.const_get(class_name)
23
- 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}"
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
- if modifier.start_with?('psk')
37
- index = modifier.gsub(/psk/, '').to_i
38
- raise Noise::Exceptions::PSKValueError if index / 2 > @tokens.size
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
- elsif modifier == 'fallback'
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 %w[K X I].include?(@name[0])
62
- required << :rs if one_way? || @name[1] == 'K'
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 @name[0] == 'K'
69
- 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?
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
@@ -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': Noise::Functions::Cipher::AesGcm,
11
- 'ChaChaPoly': Noise::Functions::Cipher::ChaChaPoly
12
- }.stringify_keys.freeze
9
+ 'AESGCM' => Noise::Functions::Cipher::AesGcm,
10
+ 'ChaChaPoly' => Noise::Functions::Cipher::ChaChaPoly
11
+ }.freeze
13
12
 
14
13
  DH = {
15
- '25519': Noise::Functions::DH::ED25519,
16
- '448': Noise::Functions::DH::ED448,
17
- 'secp256k1': Noise::Functions::DH::Secp256k1
18
- }.stringify_keys.freeze
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': Noise::Functions::Hash::Blake2b,
22
- 'BLAKE2s': Noise::Functions::Hash::Blake2s,
23
- 'SHA256': Noise::Functions::Hash::Sha256,
24
- 'SHA512': Noise::Functions::Hash::Sha512,
25
- 'BLAKE3': Noise::Functions::Hash::Blake3,
26
- }.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
27
26
 
28
27
  def self.create(name)
29
- prefix, pattern_name, dh_name, cipher_name, hash_name = name.split('_')
30
- 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
+
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 unless @cipher_fn && @hash_fn && @dh_fn
50
+ raise Noise::Exceptions::ProtocolNameError, "Unsupported function in: #{@name}" unless
51
+ @cipher_fn && @hash_fn && @dh_fn
50
52
  end
51
53
 
52
- def psk_handshake?
53
- @is_psk_handshake
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: 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,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, protocol, initiator, prologue, local_keypairs, remote_keys)
22
+ def initialize(connection, initiator, prologue, local_keypairs, remote_keys)
24
23
  @connection = connection
25
- @protocol = protocol
26
- @symmetric_state = SymmetricState.new
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
- get_local_keypair = ->(token) { instance_variable_get('@' + token).public_key }
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
- initiator_keypair_getter = get_local_keypair
40
- responder_keypair_getter = get_remote_keypair
45
+ [local_keypair_getter, remote_keypair_getter]
41
46
  else
42
- initiator_keypair_getter = get_remote_keypair
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
- # Sets message_patterns to the message patterns from handshake_pattern
47
- @message_patterns = @protocol.pattern.tokens.dup
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 = initiator_keypair_getter.call(token)
61
+ keypair = keypair_getter.call(token)
51
62
  @symmetric_state.mix_hash(keypair)
52
63
  end
64
+ end
53
65
 
54
- if @protocol.pattern.fallback
55
- message = @message_patterns.delete_at(0).first
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 = responder_keypair_getter.call(token)
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 'e'
86
+ when Noise::Token::E
72
87
  l += @protocol.dh_fn.dhlen
73
- has_key = true if @protocol.psk_handshake?
74
- when 's'
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
- when 'ee', 'es', 'se', 'ss', 'psk'
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 'e'
95
- @e = dh_fn.generate_keypair if @e.nil?
109
+ when Noise::Token::E
110
+ @e ||= @protocol.dh_fn.generate_keypair
96
111
  message_buffer << @e.public_key
97
- @symmetric_state.mix_hash(@e.public_key)
98
- @symmetric_state.mix_key(@e.public_key) if @protocol.psk_handshake?
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 'ee'
102
- @symmetric_state.mix_key(dh_fn.dh(@e.private_key, @re))
103
- when 'es'
104
- private_key, public_key = @initiator ? [@e.private_key, @rs] : [@s.private_key, @re]
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
- @symmetric_state.split if @message_patterns.empty?
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 'e'
128
- @re = message[0...len] if @re.nil?
129
- message = message[len..-1]
130
- @symmetric_state.mix_hash(@re)
131
- @symmetric_state.mix_key(@re) if @protocol.psk_handshake?
132
- when 's'
133
- offset = @connection.cipher_state_handshake.key? ? 16 : 0
134
- temp = message[0...len + offset]
135
- message = message[(len + offset)..-1]
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
- @symmetric_state.split if @message_patterns.empty?
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 initialize_symmetric(protocol, connection)
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)
@@ -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.0'
4
+ VERSION = '0.11.0'
5
5
  end
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