noise-ruby 0.1.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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +15 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/README.md +39 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/noise.rb +19 -0
- data/lib/noise/connection.rb +96 -0
- data/lib/noise/exceptions.rb +10 -0
- data/lib/noise/exceptions/max_nonce_error.rb +8 -0
- data/lib/noise/exceptions/noise_handshake_error.rb +8 -0
- data/lib/noise/exceptions/noise_validation_error.rb +8 -0
- data/lib/noise/exceptions/protocol_name_error.rb +8 -0
- data/lib/noise/functions.rb +9 -0
- data/lib/noise/functions/cipher.rb +10 -0
- data/lib/noise/functions/cipher/aes_gcm.rb +21 -0
- data/lib/noise/functions/cipher/cha_cha_poly.rb +23 -0
- data/lib/noise/functions/dh.rb +11 -0
- data/lib/noise/functions/dh/dh25519.rb +34 -0
- data/lib/noise/functions/dh/dh448.rb +25 -0
- data/lib/noise/functions/dh/secp256k1.rb +28 -0
- data/lib/noise/functions/hash.rb +32 -0
- data/lib/noise/functions/hash/blake2b.rb +23 -0
- data/lib/noise/functions/hash/blake2s.rb +23 -0
- data/lib/noise/functions/hash/sha256.rb +23 -0
- data/lib/noise/functions/hash/sha512.rb +23 -0
- data/lib/noise/pattern.rb +223 -0
- data/lib/noise/protocol.rb +107 -0
- data/lib/noise/state.rb +9 -0
- data/lib/noise/state/cipher_state.rb +54 -0
- data/lib/noise/state/handshake_state.rb +141 -0
- data/lib/noise/state/symmetric_state.rb +86 -0
- data/lib/noise/utils/hash.rb +9 -0
- data/lib/noise/utils/string.rb +10 -0
- data/lib/noise/version.rb +5 -0
- data/noise.gemspec +29 -0
- metadata +168 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noise
|
4
|
+
class Protocol
|
5
|
+
attr_accessor :prologue, :initiator, :cipher_state_encrypt, :cipher_state_decrypt
|
6
|
+
attr_reader :name, :cipher_fn, :hash_fn, :dh_fn, :hkdf_fn, :pattern
|
7
|
+
attr_reader :handshake_state, :keypairs, :keypair_fn
|
8
|
+
attr_reader :handshake_hash
|
9
|
+
attr_accessor :cipher_state_handshake
|
10
|
+
|
11
|
+
CIPHER = {
|
12
|
+
'AESGCM': Noise::Functions::Cipher::AesGcm,
|
13
|
+
'ChaChaPoly': Noise::Functions::Cipher::ChaChaPoly
|
14
|
+
}.stringify_keys.freeze
|
15
|
+
|
16
|
+
DH = {
|
17
|
+
'25519': Noise::Functions::DH::DH25519,
|
18
|
+
'448': Noise::Functions::DH::DH448
|
19
|
+
}.stringify_keys.freeze
|
20
|
+
|
21
|
+
HASH = {
|
22
|
+
'BLAKE2b': Noise::Functions::Hash::Blake2b,
|
23
|
+
'BLAKE2s': Noise::Functions::Hash::Blake2s,
|
24
|
+
'SHA256': Noise::Functions::Hash::Sha256,
|
25
|
+
'SHA512': Noise::Functions::Hash::Sha512
|
26
|
+
}.stringify_keys.freeze
|
27
|
+
|
28
|
+
def self.create(name)
|
29
|
+
prefix, pattern_name, dh_name, cipher_name, hash_name = name.split('_')
|
30
|
+
raise Noise::Exceptions::ProtocolNameError if prefix != 'Noise'
|
31
|
+
new(name, pattern_name, cipher_name, hash_name, dh_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(name, pattern_name, cipher_name, hash_name, dh_name)
|
35
|
+
@name = name
|
36
|
+
@pattern = Noise::Pattern.create(pattern_name[0..1])
|
37
|
+
@keypairs = { s: nil, e: nil, rs: nil, re: nil }
|
38
|
+
@cipher_fn = CIPHER[cipher_name]&.new
|
39
|
+
@hash_fn = HASH[hash_name]&.new
|
40
|
+
@dh_fn = DH[dh_name]&.new
|
41
|
+
@hkdf_fn = Noise::Functions::Hash.create_hkdf_fn(hash_name)
|
42
|
+
raise Noise::Exceptions::ProtocolNameError unless @cipher_fn && @hash_fn && @dh_fn
|
43
|
+
end
|
44
|
+
|
45
|
+
def handshake_done
|
46
|
+
if @pattern.one_way
|
47
|
+
if @initiator
|
48
|
+
@cipher_state_decrypt = nil
|
49
|
+
else
|
50
|
+
@cipher_state_encrypt = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
@handshake_hash = @symmetric_state.handshake_hash
|
54
|
+
@handshake_state = nil
|
55
|
+
@symmetric_state = nil
|
56
|
+
@cipher_state_handshake = nil
|
57
|
+
@prologue = nil
|
58
|
+
@initiator = nil
|
59
|
+
@dh_fn = nil
|
60
|
+
@hash_fn = nil
|
61
|
+
@keypair_fn = nil
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate
|
66
|
+
# TODO : support PSK
|
67
|
+
# if @psk_handshake
|
68
|
+
# if @psks.inclueds? {|psk| psk.size != 32}
|
69
|
+
# raise NoisePSKError
|
70
|
+
# else
|
71
|
+
# raise NoisePSKError
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
|
75
|
+
# You need to set role with NoiseConnection.set_as_initiator
|
76
|
+
# or NoiseConnection.set_as_responder
|
77
|
+
raise Noise::Exceptions::NoiseValidationError if @initiator.nil?
|
78
|
+
|
79
|
+
# 'Keypair {} has to be set for chosen handshake pattern'.format(keypair)
|
80
|
+
# require 'pp'
|
81
|
+
# pp @pattern
|
82
|
+
# pp @initiator
|
83
|
+
# pp @pattern.required_keypairs(@initiator)
|
84
|
+
# pp @keypairs
|
85
|
+
raise Noise::Exceptions::NoiseValidationError if @pattern.required_keypairs(@initiator).any? { |keypair| !@keypairs[keypair] }
|
86
|
+
|
87
|
+
if @keypairs[:e] || @keypairs[:re]
|
88
|
+
# warnings
|
89
|
+
# One of ephemeral keypairs is already set.
|
90
|
+
# This is OK for testing, but should NEVER happen in production!
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialise_handshake_state
|
95
|
+
@handshake_state = Noise::State::HandshakeState.new(
|
96
|
+
self,
|
97
|
+
@initiator,
|
98
|
+
@prologue,
|
99
|
+
@keypairs[:s],
|
100
|
+
@keypairs[:e],
|
101
|
+
@keypairs[:rs],
|
102
|
+
@keypairs[:re]
|
103
|
+
)
|
104
|
+
@symmetric_state = @handshake_state.symmetric_state
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/noise/state.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noise
|
4
|
+
module State
|
5
|
+
# A CipherState can encrypt and decrypt data based on its k and n variables:
|
6
|
+
#
|
7
|
+
# - k: A cipher key of 32 bytes (which may be empty). Empty is a special value which indicates k has not yet been
|
8
|
+
# initialized.
|
9
|
+
# - n: An 8-byte (64-bit) unsigned integer nonce.
|
10
|
+
#
|
11
|
+
class CipherState
|
12
|
+
MAX_NONCE = 2**64 - 1
|
13
|
+
|
14
|
+
attr_reader :k, :n
|
15
|
+
|
16
|
+
def initialize(cipher: AesGcm.new)
|
17
|
+
@cipher = cipher
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_key(key)
|
21
|
+
@k = key
|
22
|
+
@n = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def key?
|
26
|
+
!@k.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def nonce=(nonce)
|
30
|
+
@n = nonce
|
31
|
+
end
|
32
|
+
|
33
|
+
def encrypt_with_ad(ad, plaintext)
|
34
|
+
return plaintext unless key?
|
35
|
+
raise Noise::Exceptions::MaxNonceError if @n == MAX_NONCE
|
36
|
+
ciphertext = @cipher.encrypt(@k, @n, ad, plaintext)
|
37
|
+
@n += 1
|
38
|
+
ciphertext
|
39
|
+
end
|
40
|
+
|
41
|
+
def decrypt_with_ad(ad, ciphertext)
|
42
|
+
return ciphertext unless key?
|
43
|
+
raise Noise::Exceptions::MaxNonceError if @n == MAX_NONCE
|
44
|
+
plaintext = @cipher.decrypt(@k, @n, ad, ciphertext)
|
45
|
+
@n += 1
|
46
|
+
plaintext
|
47
|
+
end
|
48
|
+
|
49
|
+
def rekey
|
50
|
+
@k = @cipher.rekey(@k)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noise
|
4
|
+
module State
|
5
|
+
# A HandshakeState object contains a SymmetricState plus the following variables, any of which may be empty. Empty
|
6
|
+
# is a special value which indicates the variable has not yet been initialized.
|
7
|
+
#
|
8
|
+
# s: The local static key pair
|
9
|
+
# e: The local ephemeral key pair
|
10
|
+
# rs: The remote party's static public key
|
11
|
+
# re: The remote party's ephemeral public key
|
12
|
+
#
|
13
|
+
class HandshakeState
|
14
|
+
|
15
|
+
attr_reader :message_patterns, :symmetric_state
|
16
|
+
|
17
|
+
def initialize(protocol, initiator, prologue, s, e, rs, re)
|
18
|
+
# @protocol = handshake_pattern.to_protocol
|
19
|
+
@protocol = protocol
|
20
|
+
@symmetric_state = SymmetricState.new
|
21
|
+
@symmetric_state.initialize_symmetric(@protocol)
|
22
|
+
@symmetric_state.mix_hash(prologue)
|
23
|
+
@initiator = initiator
|
24
|
+
@s = [s].flatten
|
25
|
+
@e = [e].flatten
|
26
|
+
@rs = [rs].flatten
|
27
|
+
@re = [re].flatten
|
28
|
+
|
29
|
+
# TODO : Calls MixHash() once for each public key listed in the pre-messages from handshake_pattern, with the
|
30
|
+
# specified public key as input (see Section 7 for an explanation of pre-messages). If both initiator and
|
31
|
+
# responder have pre-messages, the initiator's public keys are hashed first.
|
32
|
+
get_local_keypair = ->(token) { instance_variable_get('@' + token) }
|
33
|
+
get_remote_keypair = ->(token) { instance_variable_get('@r' + token) }
|
34
|
+
|
35
|
+
if initiator
|
36
|
+
initiator_keypair_getter = get_local_keypair
|
37
|
+
responder_keypair_getter = get_remote_keypair
|
38
|
+
else
|
39
|
+
initiator_keypair_getter = get_remote_keypair
|
40
|
+
responder_keypair_getter = get_local_keypair
|
41
|
+
end
|
42
|
+
|
43
|
+
@protocol.pattern.initiator_pre_messages&.map do |message|
|
44
|
+
keypair = initiator_keypair_getter.call(message)
|
45
|
+
@symmetric_state.mix_hash(keypair[1])
|
46
|
+
end
|
47
|
+
|
48
|
+
@protocol.pattern.responder_pre_messages&.map do |message|
|
49
|
+
keypair = responder_keypair_getter.call(message)
|
50
|
+
@symmetric_state.mix_hash(keypair[1])
|
51
|
+
end
|
52
|
+
# Sets message_patterns to the message patterns from handshake_pattern
|
53
|
+
@message_patterns = @protocol.pattern.tokens.dup
|
54
|
+
end
|
55
|
+
|
56
|
+
def write_message(payload, message_buffer)
|
57
|
+
pattern = @message_patterns.shift
|
58
|
+
dh_fn = @protocol.dh_fn
|
59
|
+
|
60
|
+
pattern.each do |token|
|
61
|
+
case token
|
62
|
+
when 'e'
|
63
|
+
@e = dh_fn.generate_keypair if @e.compact.empty?
|
64
|
+
message_buffer << @e[1]
|
65
|
+
@symmetric_state.mix_hash(@e[1])
|
66
|
+
next
|
67
|
+
when 's'
|
68
|
+
message_buffer << @symmetric_state.encrypt_and_hash(@s[1])
|
69
|
+
next
|
70
|
+
when 'ee'
|
71
|
+
@symmetric_state.mix_key(dh_fn.dh(@e[0], @re[1]))
|
72
|
+
next
|
73
|
+
when 'es'
|
74
|
+
if @initiator
|
75
|
+
@symmetric_state.mix_key(dh_fn.dh(@e[0], @rs[1]))
|
76
|
+
else
|
77
|
+
@symmetric_state.mix_key(dh_fn.dh(@s[0], @re[1]))
|
78
|
+
end
|
79
|
+
next
|
80
|
+
when 'se'
|
81
|
+
if @initiator
|
82
|
+
@symmetric_state.mix_key(dh_fn.dh(@s[0], @re[1]))
|
83
|
+
else
|
84
|
+
@symmetric_state.mix_key(dh_fn.dh(@e[0], @rs[1]))
|
85
|
+
end
|
86
|
+
next
|
87
|
+
when 'ss'
|
88
|
+
@symmetric_state.mix_key(dh_fn.dh(@s[0], @rs[1]))
|
89
|
+
next
|
90
|
+
end
|
91
|
+
end
|
92
|
+
message_buffer << @symmetric_state.encrypt_and_hash(payload)
|
93
|
+
@symmetric_state.split if @message_patterns.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def read_message(message, payload_buffer)
|
97
|
+
pattern = @message_patterns.shift
|
98
|
+
dh_fn = @protocol.dh_fn
|
99
|
+
len = dh_fn.dhlen
|
100
|
+
pattern.each do |token|
|
101
|
+
case token
|
102
|
+
when 'e'
|
103
|
+
@re = @protocol.dh_fn.class.from_public(message[0...len]) if @re.compact.empty?
|
104
|
+
message = message[len..-1]
|
105
|
+
@symmetric_state.mix_hash(@re[1])
|
106
|
+
next
|
107
|
+
when 's'
|
108
|
+
offset = @protocol.cipher_state_handshake.key? ? 16 : 0
|
109
|
+
temp = message[0...len + offset]
|
110
|
+
message = message[(len + offset)..-1]
|
111
|
+
@rs = @protocol.dh_fn.class.from_public(@symmetric_state.decrypt_and_hash(temp))
|
112
|
+
# @protocol.keypair.load(@symmetric_state.decrypt_and_hash(temp))
|
113
|
+
next
|
114
|
+
when 'ee'
|
115
|
+
@symmetric_state.mix_key(dh_fn.dh(@e[0], @re[1]))
|
116
|
+
next
|
117
|
+
when 'es'
|
118
|
+
if @initiator
|
119
|
+
@symmetric_state.mix_key(dh_fn.dh(@e[0], @rs[1]))
|
120
|
+
else
|
121
|
+
@symmetric_state.mix_key(dh_fn.dh(@s[0], @re[1]))
|
122
|
+
end
|
123
|
+
next
|
124
|
+
when 'se'
|
125
|
+
if @initiator
|
126
|
+
@symmetric_state.mix_key(dh_fn.dh(@s[0], @re[1]))
|
127
|
+
else
|
128
|
+
@symmetric_state.mix_key(dh_fn.dh(@e[0], @rs[1]))
|
129
|
+
end
|
130
|
+
next
|
131
|
+
when 'ss'
|
132
|
+
@symmetric_state.mix_key(dh_fn.dh(@s[0], @rs[1]))
|
133
|
+
next
|
134
|
+
end
|
135
|
+
end
|
136
|
+
payload_buffer << @symmetric_state.decrypt_and_hash(message)
|
137
|
+
@symmetric_state.split if @message_patterns.empty?
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Noise
|
4
|
+
module State
|
5
|
+
# A SymmetricState object contains a CipherState plus the following variables:
|
6
|
+
#
|
7
|
+
# - ck: A chaining key of HASHLEN bytes.
|
8
|
+
# - h: A hash output of HASHLEN bytes.
|
9
|
+
#
|
10
|
+
class SymmetricState
|
11
|
+
attr_reader :h, :ck
|
12
|
+
attr_reader :cipher_state
|
13
|
+
|
14
|
+
def initialize_symmetric(protocol)
|
15
|
+
@protocol = protocol
|
16
|
+
@ck = @h =
|
17
|
+
if @protocol.name.length <= @protocol.hash_fn.hashlen
|
18
|
+
@protocol.name.ljust(@protocol.hash_fn.hashlen, "\x00")
|
19
|
+
else
|
20
|
+
@protocol.hash_fn.hash(@protocol.name)
|
21
|
+
end
|
22
|
+
|
23
|
+
@cipher_state = CipherState.new(cipher: @protocol.cipher_fn)
|
24
|
+
@cipher_state.initialize_key(nil)
|
25
|
+
@protocol.cipher_state_handshake = @cipher_state
|
26
|
+
end
|
27
|
+
|
28
|
+
def mix_key(input_key_meterial)
|
29
|
+
@ck, temp_k = @protocol.hkdf_fn.call(@ck, input_key_meterial, 2)
|
30
|
+
temp_k = truncate(temp_k)
|
31
|
+
@cipher_state.initialize_key(temp_k)
|
32
|
+
end
|
33
|
+
|
34
|
+
# data [String] binary string
|
35
|
+
def mix_hash(data)
|
36
|
+
@h = @protocol.hash_fn.hash(@h + data)
|
37
|
+
end
|
38
|
+
|
39
|
+
def mix_key_and_hash(input_key_meterial)
|
40
|
+
@ck, temp_h, temp_k = @protocol.hkdf_fn.call(@ck, input_key_meterial, 3)
|
41
|
+
mix_hash(temp_h)
|
42
|
+
temp_k = truncate(temp_k)
|
43
|
+
@cipher_state.initialize_key(temp_k)
|
44
|
+
end
|
45
|
+
|
46
|
+
def handshake_hash
|
47
|
+
@h
|
48
|
+
end
|
49
|
+
|
50
|
+
def encrypt_and_hash(plaintext)
|
51
|
+
ciphertext = @cipher_state.encrypt_with_ad(@h, plaintext)
|
52
|
+
mix_hash(ciphertext)
|
53
|
+
ciphertext
|
54
|
+
end
|
55
|
+
|
56
|
+
def decrypt_and_hash(ciphertext)
|
57
|
+
plaintext = @cipher_state.decrypt_with_ad(@h, ciphertext)
|
58
|
+
mix_hash(ciphertext)
|
59
|
+
plaintext
|
60
|
+
end
|
61
|
+
|
62
|
+
def split
|
63
|
+
temp_k1, temp_k2 = @protocol.hkdf_fn.call(@ck, '', 2)
|
64
|
+
temp_k1 = truncate(temp_k1)
|
65
|
+
temp_k2 = truncate(temp_k2)
|
66
|
+
c1 = CipherState.new(cipher: @protocol.cipher_fn)
|
67
|
+
c2 = CipherState.new(cipher: @protocol.cipher_fn)
|
68
|
+
c1.initialize_key(temp_k1)
|
69
|
+
c2.initialize_key(temp_k2)
|
70
|
+
if @protocol.initiator
|
71
|
+
@protocol.cipher_state_encrypt = c1
|
72
|
+
@protocol.cipher_state_decrypt = c2
|
73
|
+
else
|
74
|
+
@protocol.cipher_state_encrypt = c2
|
75
|
+
@protocol.cipher_state_decrypt = c1
|
76
|
+
end
|
77
|
+
@protocol.handshake_done
|
78
|
+
[c1, c2]
|
79
|
+
end
|
80
|
+
|
81
|
+
def truncate(temp_k)
|
82
|
+
@protocol.hash_fn.hashlen == 64 ? temp_k[0, 32] : temp_k
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/noise.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'noise/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'noise-ruby'
|
7
|
+
spec.version = Noise::VERSION
|
8
|
+
spec.authors = ['Hajime Yamaguchi']
|
9
|
+
spec.email = ['gen.yamaguchi0@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'A Ruby implementation of the Noise Protocol framework'
|
12
|
+
spec.description = 'A Ruby implementation of the Noise Protocol framework(http://noiseprotocol.org/).'
|
13
|
+
spec.homepage = 'https://github.com/Yamaguchi/noise'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'ecdsa'
|
27
|
+
spec.add_runtime_dependency 'rbnacl'
|
28
|
+
spec.add_runtime_dependency 'rb-pure25519'
|
29
|
+
end
|