noise-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +15 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +5 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +6 -0
  9. data/README.md +39 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/noise.rb +19 -0
  14. data/lib/noise/connection.rb +96 -0
  15. data/lib/noise/exceptions.rb +10 -0
  16. data/lib/noise/exceptions/max_nonce_error.rb +8 -0
  17. data/lib/noise/exceptions/noise_handshake_error.rb +8 -0
  18. data/lib/noise/exceptions/noise_validation_error.rb +8 -0
  19. data/lib/noise/exceptions/protocol_name_error.rb +8 -0
  20. data/lib/noise/functions.rb +9 -0
  21. data/lib/noise/functions/cipher.rb +10 -0
  22. data/lib/noise/functions/cipher/aes_gcm.rb +21 -0
  23. data/lib/noise/functions/cipher/cha_cha_poly.rb +23 -0
  24. data/lib/noise/functions/dh.rb +11 -0
  25. data/lib/noise/functions/dh/dh25519.rb +34 -0
  26. data/lib/noise/functions/dh/dh448.rb +25 -0
  27. data/lib/noise/functions/dh/secp256k1.rb +28 -0
  28. data/lib/noise/functions/hash.rb +32 -0
  29. data/lib/noise/functions/hash/blake2b.rb +23 -0
  30. data/lib/noise/functions/hash/blake2s.rb +23 -0
  31. data/lib/noise/functions/hash/sha256.rb +23 -0
  32. data/lib/noise/functions/hash/sha512.rb +23 -0
  33. data/lib/noise/pattern.rb +223 -0
  34. data/lib/noise/protocol.rb +107 -0
  35. data/lib/noise/state.rb +9 -0
  36. data/lib/noise/state/cipher_state.rb +54 -0
  37. data/lib/noise/state/handshake_state.rb +141 -0
  38. data/lib/noise/state/symmetric_state.rb +86 -0
  39. data/lib/noise/utils/hash.rb +9 -0
  40. data/lib/noise/utils/string.rb +10 -0
  41. data/lib/noise/version.rb +5 -0
  42. data/noise.gemspec +29 -0
  43. 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ module State
5
+ autoload :CipherState, 'noise/state/cipher_state'
6
+ autoload :HandshakeState, 'noise/state/handshake_state'
7
+ autoload :SymmetricState, 'noise/state/symmetric_state'
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Hash
4
+ def stringify_keys
5
+ keys.each_with_object({}) do |key, h|
6
+ h[key.to_s] = self[key]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ def htb
5
+ [self].pack("H*")
6
+ end
7
+ def bth
8
+ self.unpack("H*").first
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Noise
4
+ VERSION = '0.1.0'
5
+ end
@@ -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