ciri-p2p 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +15 -0
  5. data/.vscode/launch.json +90 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +65 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +45 -0
  11. data/Rakefile +6 -0
  12. data/bin/bundle +105 -0
  13. data/bin/console +14 -0
  14. data/bin/htmldiff +29 -0
  15. data/bin/ldiff +29 -0
  16. data/bin/rake +29 -0
  17. data/bin/rspec +29 -0
  18. data/bin/setup +8 -0
  19. data/ciri-p2p.gemspec +37 -0
  20. data/lib/ciri/p2p.rb +7 -0
  21. data/lib/ciri/p2p/address.rb +51 -0
  22. data/lib/ciri/p2p/dial_scheduler.rb +73 -0
  23. data/lib/ciri/p2p/dialer.rb +55 -0
  24. data/lib/ciri/p2p/discovery/protocol.rb +237 -0
  25. data/lib/ciri/p2p/discovery/service.rb +255 -0
  26. data/lib/ciri/p2p/errors.rb +36 -0
  27. data/lib/ciri/p2p/kad.rb +301 -0
  28. data/lib/ciri/p2p/network_state.rb +223 -0
  29. data/lib/ciri/p2p/node.rb +96 -0
  30. data/lib/ciri/p2p/peer.rb +151 -0
  31. data/lib/ciri/p2p/peer_store.rb +183 -0
  32. data/lib/ciri/p2p/protocol.rb +62 -0
  33. data/lib/ciri/p2p/protocol_context.rb +54 -0
  34. data/lib/ciri/p2p/protocol_io.rb +65 -0
  35. data/lib/ciri/p2p/rlpx.rb +29 -0
  36. data/lib/ciri/p2p/rlpx/connection.rb +182 -0
  37. data/lib/ciri/p2p/rlpx/encryption_handshake.rb +143 -0
  38. data/lib/ciri/p2p/rlpx/errors.rb +34 -0
  39. data/lib/ciri/p2p/rlpx/frame_io.rb +229 -0
  40. data/lib/ciri/p2p/rlpx/message.rb +45 -0
  41. data/lib/ciri/p2p/rlpx/protocol_handshake.rb +56 -0
  42. data/lib/ciri/p2p/rlpx/protocol_messages.rb +71 -0
  43. data/lib/ciri/p2p/rlpx/secrets.rb +49 -0
  44. data/lib/ciri/p2p/server.rb +159 -0
  45. data/lib/ciri/p2p/version.rb +5 -0
  46. metadata +229 -0
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+ #
3
+
4
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+
25
+ require 'ciri/key'
26
+ require_relative 'secrets'
27
+
28
+ module Ciri
29
+ module P2P
30
+ module RLPX
31
+
32
+ SHA_LENGTH = 32
33
+ SIGNATURE_LENGTH = 65
34
+ PUBLIC_KEY_LENGTH = 64
35
+ ECIES_OVERHEAD = 65 + 16 + 32
36
+ AUTH_MSG_LENGTH = SIGNATURE_LENGTH + SHA_LENGTH + PUBLIC_KEY_LENGTH + SHA_LENGTH + 1
37
+ AUTH_RESP_MSG_LENGTH = PUBLIC_KEY_LENGTH + SHA_LENGTH + 1
38
+
39
+ HANDSHAKE_TIMEOUT = 5
40
+
41
+ ENC_AUTH_MSG_LENGTH = AUTH_MSG_LENGTH + ECIES_OVERHEAD
42
+ ENC_AUTH_RESP_MSG_LENGTH = AUTH_RESP_MSG_LENGTH + ECIES_OVERHEAD
43
+
44
+ # handle key exchange handshake
45
+ class EncryptionHandshake
46
+ attr_reader :private_key, :remote_key, :remote_random_key, :initiator_nonce, :receiver_nonce, :remote_id
47
+
48
+ def initialize(private_key:, remote_id:)
49
+ @private_key = private_key
50
+ @remote_id = remote_id
51
+ end
52
+
53
+ def remote_key
54
+ @remote_key || @remote_id.key
55
+ end
56
+
57
+ def random_key
58
+ @random_key ||= Ciri::Key.random
59
+ end
60
+
61
+ def auth_msg
62
+ # make nonce bytes
63
+ nonce = random_nonce(SHA_LENGTH)
64
+ @initiator_nonce = nonce
65
+ # remote first byte tag
66
+ token = dh_compute_key(private_key, remote_key)
67
+ raise StandardError.new("token size #{token.size} not correct") if token.size != nonce.size
68
+ # xor
69
+ signed = xor(token, nonce)
70
+
71
+ signature = random_key.ecdsa_signature(signed).signature
72
+ initiator_pubkey = private_key.raw_public_key[1..-1]
73
+ AuthMsgV4.new(signature: signature, initiator_pubkey: initiator_pubkey, nonce: nonce, version: 4)
74
+ end
75
+
76
+ def handle_auth_msg(msg)
77
+ @remote_key = Ciri::Key.new(raw_public_key: "\x04" + msg.initiator_pubkey)
78
+ @initiator_nonce = msg.nonce
79
+
80
+ token = dh_compute_key(private_key, @remote_key)
81
+ signed = xor(token, msg.nonce)
82
+ @remote_random_key = Ciri::Key.ecdsa_recover(signed, msg.signature)
83
+ end
84
+
85
+ def auth_ack_msg
86
+ # make nonce bytes
87
+ nonce = random_nonce(SHA_LENGTH)
88
+ @receiver_nonce = nonce
89
+ random_pubkey = random_key.raw_public_key[1..-1]
90
+ AuthRespV4.new(random_pubkey: random_pubkey, nonce: nonce, version: 4)
91
+ end
92
+
93
+ def handle_auth_ack_msg(msg)
94
+ # make nonce bytes
95
+ @receiver_nonce = msg.nonce
96
+ @remote_random_key = Ciri::Key.new(raw_public_key: "\x04" + msg.random_pubkey)
97
+ end
98
+
99
+ def extract_secrets(auth_packet, auth_ack_packet, initiator:)
100
+ secret = dh_compute_key(random_key, remote_random_key)
101
+ shared_secret = Ciri::Utils.keccak(secret, Ciri::Utils.keccak(receiver_nonce, initiator_nonce))
102
+ aes_secret = Ciri::Utils.keccak(secret, shared_secret)
103
+ mac = Ciri::Utils.keccak(secret, aes_secret)
104
+ secrets = Secrets.new(remote_id: remote_id, aes: aes_secret, mac: mac)
105
+
106
+ # initial secrets macs
107
+ mac1 = Digest::SHA3.new(256)
108
+ mac1.update xor(mac, receiver_nonce)
109
+ mac1.update auth_packet
110
+
111
+ mac2 = Digest::SHA3.new(256)
112
+ mac2.update xor(mac, initiator_nonce)
113
+ mac2.update auth_ack_packet
114
+
115
+ if initiator
116
+ secrets.egress_mac = mac1
117
+ secrets.ingress_mac = mac2
118
+ else
119
+ secrets.egress_mac = mac2
120
+ secrets.ingress_mac = mac1
121
+ end
122
+ secrets
123
+ end
124
+
125
+ private
126
+
127
+ def dh_compute_key(private_key, public_key)
128
+ private_key.ec_key.dh_compute_key(public_key.ec_key.public_key)
129
+ end
130
+
131
+ def xor(b1, b2)
132
+ b1.each_byte.with_index.map {|b, i| b ^ b2[i].ord}.pack('c*')
133
+ end
134
+
135
+ def random_nonce(size)
136
+ size.times.map {rand(8)}.pack('c*')
137
+ end
138
+
139
+ end
140
+ end
141
+ end
142
+ end
143
+
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+
24
+ module Ciri
25
+ module P2P
26
+ module RLPX
27
+
28
+ # RLPX basic error
29
+ class Error < StandardError
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+
24
+ require 'stringio'
25
+ require 'forwardable'
26
+ require 'ciri/core_ext'
27
+ require 'ciri/rlp/serializable'
28
+ require_relative 'errors'
29
+ require_relative 'message'
30
+
31
+ require 'snappy'
32
+
33
+ using Ciri::CoreExt
34
+
35
+ module Ciri
36
+ module P2P
37
+ module RLPX
38
+
39
+ class FrameIO
40
+ extend Forwardable
41
+ def_delegators :@io, :closed?, :close, :flush
42
+
43
+ # max message size, took 3 byte to store message size, equal to uint24 max size
44
+ MAX_MESSAGE_SIZE = (1 << 24) - 1
45
+
46
+ class Error < RLPX::Error
47
+ end
48
+
49
+ class OverflowError < Error
50
+ end
51
+
52
+ class InvalidError < Error
53
+ end
54
+
55
+ attr_accessor :snappy
56
+
57
+ def initialize(io, secrets)
58
+ @io = io
59
+ @secrets = secrets
60
+ @snappy = false # snappy compress
61
+
62
+ mac_aes_version = secrets.mac.size * 8
63
+ @mac = OpenSSL::Cipher.new("AES#{mac_aes_version}")
64
+ @mac.encrypt
65
+ @mac.key = secrets.mac
66
+
67
+ # init encrypt/decrypt
68
+ aes_version = secrets.aes.size * 8
69
+ @encrypt = OpenSSL::Cipher::AES.new(aes_version, :CTR)
70
+ @decrypt = OpenSSL::Cipher::AES.new(aes_version, :CTR)
71
+ zero_iv = "\x00".b * @encrypt.iv_len
72
+ @encrypt.iv = zero_iv
73
+ @encrypt.key = secrets.aes
74
+ @decrypt.iv = zero_iv
75
+ @decrypt.key = secrets.aes
76
+ end
77
+
78
+ def send_data(code, data)
79
+ msg = Message.new(code: code, size: data.size, payload: data)
80
+ write_msg(msg)
81
+ end
82
+
83
+ def write_msg(msg)
84
+ pkg_type = RLP.encode_with_type msg.code, Integer, zero: "\x00"
85
+
86
+ # use snappy compress if enable
87
+ if snappy
88
+ if msg.size > MAX_MESSAGE_SIZE
89
+ raise OverflowError.new("Message size is overflow, msg size: #{msg.size}")
90
+ end
91
+ msg.payload = Snappy.deflate(msg.payload)
92
+ msg.size = msg.payload.size
93
+ end
94
+
95
+ # write header
96
+ head_buf = "\x00".b * 32
97
+
98
+ frame_size = pkg_type.size + msg.size
99
+ if frame_size > MAX_MESSAGE_SIZE
100
+ raise OverflowError.new("Message size is overflow, frame size: #{frame_size}")
101
+ end
102
+
103
+ write_frame_size(head_buf, frame_size)
104
+
105
+ # Can't find related RFC or RLPX Spec, below code is copy from geth
106
+ # write zero header, but I can't find spec or explanations of 'zero header'
107
+ head_buf[3..5] = [0xC2, 0x80, 0x80].pack('c*')
108
+ # encrypt first half
109
+ head_buf[0...16] = @encrypt.update(head_buf[0...16]) + @encrypt.final
110
+ # write header mac
111
+ head_buf[16...32] = update_mac(@secrets.egress_mac, head_buf[0...16])
112
+ @io.write head_buf
113
+ # write encrypt frame
114
+ write_frame(pkg_type)
115
+ write_frame(msg.payload)
116
+ # pad to n*16 bytes
117
+ if (need_padding = frame_size % 16) > 0
118
+ write_frame("\x00".b * (16 - need_padding))
119
+ end
120
+ finish_write_frame
121
+ # because we use Async::IO::Stream as IO object, we must invoke flush to make sure data is send
122
+ flush
123
+ end
124
+
125
+ def read_msg
126
+ # verify header mac
127
+ head_buf = read(32)
128
+ verify_mac = update_mac(@secrets.ingress_mac, head_buf[0...16])
129
+ unless Ciri::Utils.secret_compare(verify_mac, head_buf[16...32])
130
+ raise InvalidError.new('bad header mac')
131
+ end
132
+
133
+ # decrypt header
134
+ head_buf[0...16] = @decrypt.update(head_buf[0...16]) + @decrypt.final
135
+
136
+ # read frame
137
+ frame_size = read_frame_size head_buf
138
+ # frame size should padded to n*16 bytes
139
+ need_padding = frame_size % 16
140
+ padded_frame_size = need_padding > 0 ? frame_size + (16 - need_padding) : frame_size
141
+ frame_buf = read(padded_frame_size)
142
+
143
+ # verify frame mac
144
+ @secrets.ingress_mac.update(frame_buf)
145
+ frame_digest = @secrets.ingress_mac.digest
146
+ verify_mac = update_mac(@secrets.ingress_mac, frame_digest)
147
+ # clear head_buf 16...32 bytes(header mac), since we will not need it
148
+ frame_mac = head_buf[16...32] = read(16)
149
+ unless Ciri::Utils.secret_compare(verify_mac, frame_mac)
150
+ raise InvalidError.new('bad frame mac')
151
+ end
152
+
153
+ # decrypt frame
154
+ frame_content = @decrypt.update(frame_buf) + @decrypt.final
155
+ frame_content = frame_content[0...frame_size]
156
+ msg_code = RLP.decode_with_type frame_content[0], Integer
157
+ msg = Message.new(code: msg_code, size: frame_content.size - 1, payload: frame_content[1..-1])
158
+
159
+ # snappy decompress if enable
160
+ if snappy
161
+ msg.payload = Snappy.inflate(msg.payload)
162
+ msg.size = msg.payload.size
163
+ end
164
+
165
+ msg
166
+ end
167
+
168
+ private
169
+ def read(length)
170
+ if (buf = @io.read(length)).nil?
171
+ @io.close
172
+ raise EOFError.new('read EOF, connection closed')
173
+ end
174
+ buf
175
+ end
176
+
177
+ def write_frame_size(buf, frame_size)
178
+ # frame-size: 3-byte integer size of frame, big endian encoded (excludes padding)
179
+ bytes_of_frame_size = [
180
+ frame_size >> 16,
181
+ frame_size >> 8,
182
+ frame_size % 256
183
+ ]
184
+ buf[0..2] = bytes_of_frame_size.pack('c*')
185
+ end
186
+
187
+ def read_frame_size(buf)
188
+ size_bytes = buf[0..2].each_byte.map(&:ord)
189
+ (size_bytes[0] << 16) + (size_bytes[1] << 8) + (size_bytes[2])
190
+ end
191
+
192
+ def update_mac(mac, seed)
193
+ # reset mac each time
194
+ @mac.reset
195
+ aes_buf = (@mac.update(mac.digest) + @mac.final)[0...@mac.block_size]
196
+ aes_buf = aes_buf.each_byte.with_index.map {|b, i| b ^ seed[i].ord}.pack('c*')
197
+ mac.update(aes_buf)
198
+ # return first 16 byte
199
+ mac.digest[0...16]
200
+ end
201
+
202
+ # write encrypt content to @io, and update @secrets.egress_mac
203
+ def write_frame(string_or_io)
204
+ if string_or_io.is_a?(IO)
205
+ while (s = string_or_io.read(4096))
206
+ write_frame_string(s)
207
+ end
208
+ else
209
+ write_frame_string(string_or_io)
210
+ end
211
+ end
212
+
213
+ def write_frame_string(s)
214
+ encrypt_content = @encrypt.update(s) + @encrypt.final
215
+ # update egress_mac
216
+ @secrets.egress_mac.update encrypt_content
217
+ @io.write encrypt_content
218
+ end
219
+
220
+ def finish_write_frame
221
+ # get frame digest
222
+ frame_digest = @secrets.egress_mac.digest
223
+ @io.write update_mac(@secrets.egress_mac, frame_digest)
224
+ end
225
+ end
226
+
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018 by Jiang Jinyang <jjyruby@gmail.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+
24
+ require 'ciri/rlp'
25
+
26
+ module Ciri
27
+ module P2P
28
+ module RLPX
29
+
30
+ # RLPX message
31
+ class Message
32
+ include Ciri::RLP::Serializable
33
+
34
+ attr_accessor :received_at
35
+
36
+ schema(
37
+ code: Integer,
38
+ size: Integer,
39
+ payload: RLP::Bytes
40
+ )
41
+ end
42
+
43
+ end
44
+ end
45
+ end