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.
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