noise-ruby 0.13.0 → 0.15.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 +1 -0
- data/.github/workflows/ruby.yml +39 -0
- data/Dockerfile +3 -3
- data/README.md +164 -7
- data/Rakefile +12 -0
- data/lib/noise/connection/base.rb +188 -24
- data/lib/noise/connection/initiator.rb +6 -3
- data/lib/noise/connection/responder.rb +6 -3
- data/lib/noise/exceptions/handshake_already_finished_error.rb +9 -0
- data/lib/noise/exceptions/handshake_not_finished_error.rb +9 -0
- data/lib/noise/exceptions/handshake_not_started_error.rb +9 -0
- data/lib/noise/exceptions/handshake_turn_error.rb +11 -0
- data/lib/noise/exceptions/read_timeout_error.rb +9 -0
- data/lib/noise/exceptions/truncated_message_error.rb +9 -0
- data/lib/noise/exceptions.rb +6 -0
- data/lib/noise/functions/cipher/cha_cha_poly.rb +31 -6
- data/lib/noise/functions/dh/ed25519.rb +27 -15
- data/lib/noise/functions/hash/blake2b.rb +24 -2
- data/lib/noise/functions/hash/blake3.rb +4 -4
- data/lib/noise/functions/hash/sha256.rb +1 -1
- data/lib/noise/functions/hash/sha512.rb +1 -1
- data/lib/noise/pattern.rb +10 -8
- data/lib/noise/protocol.rb +43 -19
- data/lib/noise/protocol_name.rb +78 -0
- data/lib/noise/transport/bolt8.rb +183 -0
- data/lib/noise/transport/framed.rb +98 -0
- data/lib/noise/transport/stream.rb +130 -0
- data/lib/noise/transport.rb +14 -0
- data/lib/noise/version.rb +1 -1
- data/lib/noise.rb +5 -1
- data/noise.gemspec +10 -10
- metadata +13 -16
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noise
|
|
4
|
+
module Exceptions
|
|
5
|
+
# Raised when a handshake message is written where one has to be read, or read where one has to
|
|
6
|
+
# be written. A Noise handshake alternates between the two parties, so only one of the two is
|
|
7
|
+
# legal at any point.
|
|
8
|
+
class HandshakeTurnError < NoiseHandshakeError
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/noise/exceptions.rb
CHANGED
|
@@ -4,16 +4,22 @@ module Noise
|
|
|
4
4
|
module Exceptions
|
|
5
5
|
autoload :DecryptError, 'noise/exceptions/decrypt_error'
|
|
6
6
|
autoload :EncryptError, 'noise/exceptions/encrypt_error'
|
|
7
|
+
autoload :HandshakeAlreadyFinishedError, 'noise/exceptions/handshake_already_finished_error'
|
|
8
|
+
autoload :HandshakeNotFinishedError, 'noise/exceptions/handshake_not_finished_error'
|
|
9
|
+
autoload :HandshakeNotStartedError, 'noise/exceptions/handshake_not_started_error'
|
|
10
|
+
autoload :HandshakeTurnError, 'noise/exceptions/handshake_turn_error'
|
|
7
11
|
autoload :InvalidNonceError, 'noise/exceptions/invalid_nonce_error'
|
|
8
12
|
autoload :InvalidPublicKeyError, 'noise/exceptions/invalid_public_key_error'
|
|
9
13
|
autoload :MaxNonceError, 'noise/exceptions/max_nonce_error'
|
|
10
14
|
autoload :MessageTooLongError, 'noise/exceptions/message_too_long_error'
|
|
11
15
|
autoload :MissingDependencyError, 'noise/exceptions/missing_dependency_error'
|
|
12
16
|
autoload :ProtocolNameError, 'noise/exceptions/protocol_name_error'
|
|
17
|
+
autoload :ReadTimeoutError, 'noise/exceptions/read_timeout_error'
|
|
13
18
|
autoload :NoiseHandshakeError, 'noise/exceptions/noise_handshake_error'
|
|
14
19
|
autoload :NoiseValidationError, 'noise/exceptions/noise_validation_error'
|
|
15
20
|
autoload :NoisePSKError, 'noise/exceptions/noise_psk_error'
|
|
16
21
|
autoload :PSKValueError, 'noise/exceptions/psk_value_error'
|
|
22
|
+
autoload :TruncatedMessageError, 'noise/exceptions/truncated_message_error'
|
|
17
23
|
autoload :UnsupportedModifierError, 'noise/exceptions/unsupported_modifier_error'
|
|
18
24
|
end
|
|
19
25
|
end
|
|
@@ -6,17 +6,32 @@ module Noise
|
|
|
6
6
|
class ChaChaPoly
|
|
7
7
|
MAX_NONCE = 2**64 - 1
|
|
8
8
|
|
|
9
|
+
# The name OpenSSL knows the AEAD construction of RFC 8439 by. It takes a 12 byte nonce and
|
|
10
|
+
# produces a 16 byte authentication tag, which is what the Noise specification requires of
|
|
11
|
+
# the ChaChaPoly cipher functions.
|
|
12
|
+
ALGORITHM = 'chacha20-poly1305'
|
|
13
|
+
|
|
14
|
+
# Length in bytes of the authentication tag that encrypt appends to the ciphertext.
|
|
15
|
+
TAGLEN = 16
|
|
16
|
+
|
|
9
17
|
def encrypt(k, n, ad, plaintext)
|
|
10
|
-
cipher =
|
|
11
|
-
cipher.
|
|
12
|
-
|
|
18
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM).encrypt
|
|
19
|
+
cipher.key = k
|
|
20
|
+
cipher.iv = nonce_to_bytes(n)
|
|
21
|
+
cipher.auth_data = ad
|
|
22
|
+
update(cipher, plaintext) + cipher.final + cipher.auth_tag
|
|
23
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
13
24
|
raise Noise::Exceptions::EncryptError, "Encrypt failed. #{e.message}", e.backtrace
|
|
14
25
|
end
|
|
15
26
|
|
|
16
27
|
def decrypt(k, n, ad, ciphertext)
|
|
17
|
-
cipher =
|
|
18
|
-
cipher.
|
|
19
|
-
|
|
28
|
+
cipher = OpenSSL::Cipher.new(ALGORITHM).decrypt
|
|
29
|
+
cipher.key = k
|
|
30
|
+
cipher.iv = nonce_to_bytes(n)
|
|
31
|
+
cipher.auth_data = ad
|
|
32
|
+
cipher.auth_tag = ciphertext[-TAGLEN..]
|
|
33
|
+
update(cipher, ciphertext[0...-TAGLEN]) + cipher.final
|
|
34
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
20
35
|
raise Noise::Exceptions::DecryptError, "Decrpyt failed. #{e.message}", e.backtrace
|
|
21
36
|
end
|
|
22
37
|
|
|
@@ -34,6 +49,16 @@ module Noise
|
|
|
34
49
|
def rekey(k)
|
|
35
50
|
encrypt(k, MAX_NONCE, '', "\x00" * 32)[0...32]
|
|
36
51
|
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# A zero-length payload is normal in a Noise message, but OpenSSL::Cipher#update raises
|
|
56
|
+
# ArgumentError('data must not be empty') instead of returning ''.
|
|
57
|
+
def update(cipher, data)
|
|
58
|
+
return String.new if data.empty?
|
|
59
|
+
|
|
60
|
+
cipher.update(data)
|
|
61
|
+
end
|
|
37
62
|
end
|
|
38
63
|
end
|
|
39
64
|
end
|
|
@@ -3,29 +3,41 @@
|
|
|
3
3
|
module Noise
|
|
4
4
|
module Functions
|
|
5
5
|
module DH
|
|
6
|
+
# The 25519 DH function of the Noise specification, which is X25519 as defined by RFC 7748 and
|
|
7
|
+
# not the Ed25519 signature scheme the class name suggests. The name is kept because it is
|
|
8
|
+
# what Protocol::DH maps '25519' to and what callers reference.
|
|
9
|
+
#
|
|
10
|
+
# OpenSSL implements X25519 and exposes it through the raw key interface, so this function
|
|
11
|
+
# needs no gem and no system library beyond the OpenSSL the other functions already use.
|
|
6
12
|
class ED25519
|
|
7
13
|
DHLEN = 32
|
|
14
|
+
|
|
15
|
+
# The name OpenSSL knows the curve by. Ed25519 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 = 'X25519'
|
|
18
|
+
|
|
8
19
|
def generate_keypair
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
public_key = RbNaCl::GroupElements::Curve25519.base.mult(scalar_as_string)
|
|
12
|
-
Noise::Key.new(ECDSA::Format::IntegerOctetString.encode(private_key, 32), public_key.to_bytes)
|
|
20
|
+
pkey = OpenSSL::PKey.generate_key(ALGORITHM)
|
|
21
|
+
Noise::Key.new(pkey.raw_private_key, pkey.raw_public_key)
|
|
13
22
|
end
|
|
14
23
|
|
|
15
24
|
# Computes the X25519 shared secret for the given remote public key.
|
|
16
25
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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.
|
|
23
32
|
def dh(private_key, public_key)
|
|
24
33
|
raise Noise::Exceptions::InvalidPublicKeyError, public_key unless public_key.bytesize == DHLEN
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
def dhlen
|
|
@@ -33,8 +45,8 @@ module Noise
|
|
|
33
45
|
end
|
|
34
46
|
|
|
35
47
|
def self.from_private(private_key)
|
|
36
|
-
|
|
37
|
-
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)
|
|
38
50
|
end
|
|
39
51
|
end
|
|
40
52
|
end
|
|
@@ -6,8 +6,14 @@ module Noise
|
|
|
6
6
|
class Blake2b
|
|
7
7
|
HASHLEN = 64
|
|
8
8
|
BLOCKLEN = 128
|
|
9
|
+
|
|
10
|
+
# The name OpenSSL knows BLAKE2b with a 64 byte output by. OpenSSL only implements the
|
|
11
|
+
# fixed 512 bit output length, which is exactly the HASHLEN the Noise specification gives
|
|
12
|
+
# the BLAKE2b hash function.
|
|
13
|
+
DIGEST_NAME = 'BLAKE2b512'
|
|
14
|
+
|
|
9
15
|
def hash(data)
|
|
10
|
-
|
|
16
|
+
OpenSSL::Digest.digest(DIGEST_NAME, data)
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def hashlen
|
|
@@ -19,9 +25,25 @@ module Noise
|
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
|
|
28
|
+
# Builds an OpenSSL BLAKE2b digest with no argument, which is how HMAC::Base creates the
|
|
29
|
+
# digests it feeds the inner and outer blocks to. OpenSSL defines no OpenSSL::Digest::BLAKE2b512
|
|
30
|
+
# constant, so the algorithm name is bound here instead of being passed in at every call.
|
|
31
|
+
class Blake2bDigester < OpenSSL::Digest
|
|
32
|
+
def initialize
|
|
33
|
+
super(Blake2b::DIGEST_NAME)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# HMAC::Base hashes a key longer than the block size through this class method. The one
|
|
37
|
+
# inherited from OpenSSL::Digest takes the algorithm name as its first argument, which this
|
|
38
|
+
# class has already bound, so it is redefined to take the data alone.
|
|
39
|
+
def self.digest(data)
|
|
40
|
+
new.digest(data)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
22
44
|
class Blake2bHMAC < HMAC::Base
|
|
23
45
|
def initialize(key = nil)
|
|
24
|
-
super(
|
|
46
|
+
super(Blake2bDigester, Blake2b::BLOCKLEN, Blake2b::HASHLEN, key)
|
|
25
47
|
end
|
|
26
48
|
public_class_method :new, :digest, :hexdigest
|
|
27
49
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
Noise.require_optional 'blake3'
|
|
3
|
+
Noise.require_optional 'blake3-rb'
|
|
4
4
|
|
|
5
5
|
module Noise
|
|
6
6
|
module Functions
|
|
@@ -10,11 +10,11 @@ module Noise
|
|
|
10
10
|
BLOCKLEN = 64
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
|
-
Noise.optional_dependency!('blake3')
|
|
13
|
+
Noise.optional_dependency!('blake3-rb')
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def hash(data)
|
|
17
|
-
::Blake3.digest(data)
|
|
17
|
+
::Digest::Blake3.digest(data)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def hashlen
|
|
@@ -28,7 +28,7 @@ module Noise
|
|
|
28
28
|
|
|
29
29
|
class Blake3HMAC < HMAC::Base
|
|
30
30
|
def initialize(key = nil)
|
|
31
|
-
super(::Blake3
|
|
31
|
+
super(::Digest::Blake3, Blake3::BLOCKLEN, Blake3::HASHLEN, key)
|
|
32
32
|
end
|
|
33
33
|
public_class_method :new, :digest, :hexdigest
|
|
34
34
|
end
|
data/lib/noise/pattern.rb
CHANGED
|
@@ -102,14 +102,16 @@ module Noise
|
|
|
102
102
|
class Pattern
|
|
103
103
|
attr_reader :name, :tokens, :modifiers, :psk_count, :fallback
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
# Noise::ProtocolName tells a pattern name from the modifiers written after it, so both
|
|
106
|
+
# arrive here already separated.
|
|
107
|
+
#
|
|
108
|
+
# @param [String] name the handshake pattern name on its own, for example 'XX'.
|
|
109
|
+
# @param [Array<Noise::Modifier::Psk, Noise::Modifier::Fallback>] modifiers the modifiers to
|
|
110
|
+
# apply to it. Call #apply_pattern_modifiers to have them rewrite the token list.
|
|
111
|
+
# @raise [Noise::Exceptions::ProtocolNameError] if no pattern goes by that name.
|
|
112
|
+
# @return [Noise::Pattern]
|
|
113
|
+
def self.create(name, modifiers = [])
|
|
114
|
+
pattern_class(name).new(modifiers)
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
def self.pattern_class(pattern)
|
data/lib/noise/protocol.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Noise
|
|
4
4
|
class Protocol
|
|
5
5
|
attr_accessor :cipher_fn, :hash_fn, :dh_fn, :hkdf_fn
|
|
6
|
-
attr_reader :name, :pattern
|
|
6
|
+
attr_reader :name, :pattern, :protocol_name
|
|
7
7
|
|
|
8
8
|
CIPHER = {
|
|
9
9
|
'AESGCM' => Noise::Functions::Cipher::AesGcm,
|
|
@@ -24,35 +24,59 @@ module Noise
|
|
|
24
24
|
'BLAKE3' => Noise::Functions::Hash::Blake3
|
|
25
25
|
}.freeze
|
|
26
26
|
|
|
27
|
+
# @param [String] name the protocol name, for example 'Noise_XX_25519_ChaChaPoly_SHA256'.
|
|
28
|
+
# @raise [Noise::Exceptions::ProtocolNameError] if the name is malformed, or names a pattern
|
|
29
|
+
# or a function this gem does not implement.
|
|
30
|
+
# @raise [Noise::Exceptions::UnsupportedModifierError] if it names a modifier this gem does
|
|
31
|
+
# not implement.
|
|
32
|
+
# @return [Noise::Protocol]
|
|
27
33
|
def self.create(name)
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
new(name, pattern_name, cipher_name, hash_name, dh_name)
|
|
34
|
+
new(Noise::ProtocolName.parse(name))
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@
|
|
40
|
-
@
|
|
37
|
+
# @param [Noise::ProtocolName] protocol_name the parsed name this protocol runs.
|
|
38
|
+
def initialize(protocol_name)
|
|
39
|
+
@protocol_name = protocol_name
|
|
40
|
+
@name = protocol_name.name
|
|
41
|
+
@pattern = Noise::Pattern.create(protocol_name.pattern_name, protocol_name.modifiers)
|
|
42
|
+
@hkdf_fn = Noise::Functions::Hash.create_hkdf_fn(protocol_name.hash_name)
|
|
41
43
|
@pattern.apply_pattern_modifiers
|
|
42
44
|
|
|
43
|
-
initialize_fn!
|
|
45
|
+
initialize_fn!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def psk?
|
|
49
|
+
@pattern.psk?
|
|
44
50
|
end
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Looks the three functions up by the names the protocol name gives them.
|
|
55
|
+
#
|
|
56
|
+
# @raise [Noise::Exceptions::ProtocolNameError] if any of the three is one this gem does not
|
|
57
|
+
# implement.
|
|
58
|
+
def initialize_fn!
|
|
59
|
+
@cipher_fn = CIPHER[@protocol_name.cipher_name]&.new
|
|
60
|
+
@hash_fn = HASH[@protocol_name.hash_name]&.new
|
|
61
|
+
@dh_fn = create_dh_fn(@protocol_name.dh_names)
|
|
50
62
|
raise Noise::Exceptions::ProtocolNameError, "Unsupported function in: #{@name}" unless
|
|
51
63
|
@cipher_fn && @hash_fn && @dh_fn
|
|
52
64
|
end
|
|
53
65
|
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
# A name may list more than one DH function, joined with '+', which is how a hybrid handshake
|
|
67
|
+
# is written. This gem runs a single DH function, so any name that lists more than one, or
|
|
68
|
+
# that leaves a member of the list empty, resolves to nothing and is reported as unsupported.
|
|
69
|
+
#
|
|
70
|
+
# The hybrid names the Noise extensions define also carry a modifier, hfs, which
|
|
71
|
+
# Noise::ProtocolName rejects first, so what reaches here today is a name that lists several
|
|
72
|
+
# DH functions and nothing else.
|
|
73
|
+
#
|
|
74
|
+
# @param [Array<String>] names the DH function names the protocol name lists.
|
|
75
|
+
# @return [Object, nil] the DH function, or nil if the name asks for one this gem lacks.
|
|
76
|
+
def create_dh_fn(names)
|
|
77
|
+
return nil unless names.size == 1
|
|
78
|
+
|
|
79
|
+
DH[names.first]&.new
|
|
56
80
|
end
|
|
57
81
|
end
|
|
58
82
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Noise
|
|
4
|
+
# A Noise protocol name, split into the parts that name a handshake pattern and the three
|
|
5
|
+
# functions the protocol runs on.
|
|
6
|
+
#
|
|
7
|
+
# A name has five parts joined with '_': the prefix 'Noise', the handshake pattern with its
|
|
8
|
+
# modifiers, the DH function, the cipher function and the hash function, for example
|
|
9
|
+
# 'Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s'.
|
|
10
|
+
#
|
|
11
|
+
# Two of those parts hold a list of their own. The pattern part is a pattern name followed by
|
|
12
|
+
# the modifiers applied to it, and the DH part is a list joined with '+', because a hybrid
|
|
13
|
+
# handshake names two DH functions at once ('Noise_NNhfs_25519+448_ChaChaPoly_BLAKE2s'). Both
|
|
14
|
+
# are split here, so that every rule about the shape of a name lives in this class alone.
|
|
15
|
+
#
|
|
16
|
+
# Which functions exist is Noise::Protocol's question, not this one's: a name that asks for a
|
|
17
|
+
# cipher, hash or DH function this gem does not implement still parses. Modifiers are the one
|
|
18
|
+
# exception, because a modifier has to be understood before it can be held as a value, so an
|
|
19
|
+
# unknown one is rejected here.
|
|
20
|
+
#
|
|
21
|
+
# The members are the names as written, not the functions themselves. Noise::Protocol resolves
|
|
22
|
+
# them, and calls its resolved functions cipher_fn, hash_fn and dh_fn.
|
|
23
|
+
ProtocolName = Data.define(:name, :pattern_name, :modifiers, :dh_names, :cipher_name,
|
|
24
|
+
:hash_name) do
|
|
25
|
+
# Parses a protocol name.
|
|
26
|
+
#
|
|
27
|
+
# @param [String] name for example 'Noise_XX_25519_ChaChaPoly_SHA256'.
|
|
28
|
+
# @raise [Noise::Exceptions::ProtocolNameError] if the name is not five parts with the 'Noise'
|
|
29
|
+
# prefix and a pattern part whose modifiers can be told from the pattern name.
|
|
30
|
+
# @raise [Noise::Exceptions::UnsupportedModifierError] if a modifier is written correctly but
|
|
31
|
+
# is not one this gem implements.
|
|
32
|
+
# @return [Noise::ProtocolName]
|
|
33
|
+
def self.parse(name)
|
|
34
|
+
# 'Noise', the pattern with its modifiers, the DH functions, the cipher and the hash.
|
|
35
|
+
parts = name.split('_')
|
|
36
|
+
malformed!(name) unless parts.size == 5
|
|
37
|
+
|
|
38
|
+
prefix, pattern_part, dh_part, cipher_name, hash_name = parts
|
|
39
|
+
malformed!(name) unless prefix == 'Noise'
|
|
40
|
+
|
|
41
|
+
# A pattern name is capitals, plus the digit 1 that deferred patterns use (X1K1). Whatever
|
|
42
|
+
# follows it is the modifiers.
|
|
43
|
+
matched = /\A([A-Z1]+)([^A-Z]*)\z/.match(pattern_part)
|
|
44
|
+
malformed!(name) unless matched
|
|
45
|
+
|
|
46
|
+
new(name: name, pattern_name: matched[1], modifiers: parse_modifiers(matched[2]),
|
|
47
|
+
dh_names: split_dh(dh_part), cipher_name: cipher_name, hash_name: hash_name)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param [String] part the modifiers as written, which is '' when there are none.
|
|
51
|
+
# @return [Array] the modifiers, in the order the name writes them.
|
|
52
|
+
def self.parse_modifiers(part)
|
|
53
|
+
part.split('+').map { |modifier| Modifier.parse(modifier) }.freeze
|
|
54
|
+
end
|
|
55
|
+
private_class_method :parse_modifiers
|
|
56
|
+
|
|
57
|
+
# Keeps the empty members a stray '+' leaves behind, so that '25519+' reads as two DH
|
|
58
|
+
# functions, the second of which has no name, rather than as the single function '25519'.
|
|
59
|
+
# Nothing resolves an empty name, so such a name is reported as unsupported.
|
|
60
|
+
#
|
|
61
|
+
# @param [String] part the DH functions as written.
|
|
62
|
+
# @return [Array<String>] one name per member.
|
|
63
|
+
def self.split_dh(part)
|
|
64
|
+
part.split('+', -1).freeze
|
|
65
|
+
end
|
|
66
|
+
private_class_method :split_dh
|
|
67
|
+
|
|
68
|
+
def self.malformed!(name)
|
|
69
|
+
raise Noise::Exceptions::ProtocolNameError, "Malformed protocol name: #{name}"
|
|
70
|
+
end
|
|
71
|
+
private_class_method :malformed!
|
|
72
|
+
|
|
73
|
+
# @return [String] the name this was parsed from.
|
|
74
|
+
def to_s
|
|
75
|
+
name
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|