ciri-p2p 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 (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