noise-ruby 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|