devp2p 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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +22 -0
  4. data/lib/devp2p.rb +57 -0
  5. data/lib/devp2p/app_helper.rb +85 -0
  6. data/lib/devp2p/base_app.rb +80 -0
  7. data/lib/devp2p/base_protocol.rb +136 -0
  8. data/lib/devp2p/base_service.rb +55 -0
  9. data/lib/devp2p/command.rb +82 -0
  10. data/lib/devp2p/configurable.rb +32 -0
  11. data/lib/devp2p/connection_monitor.rb +77 -0
  12. data/lib/devp2p/control.rb +32 -0
  13. data/lib/devp2p/crypto.rb +73 -0
  14. data/lib/devp2p/crypto/ecc_x.rb +133 -0
  15. data/lib/devp2p/crypto/ecies.rb +134 -0
  16. data/lib/devp2p/discovery.rb +118 -0
  17. data/lib/devp2p/discovery/address.rb +83 -0
  18. data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
  19. data/lib/devp2p/discovery/node.rb +32 -0
  20. data/lib/devp2p/discovery/protocol.rb +342 -0
  21. data/lib/devp2p/discovery/transport.rb +105 -0
  22. data/lib/devp2p/exception.rb +30 -0
  23. data/lib/devp2p/frame.rb +197 -0
  24. data/lib/devp2p/kademlia.rb +48 -0
  25. data/lib/devp2p/kademlia/k_bucket.rb +178 -0
  26. data/lib/devp2p/kademlia/node.rb +40 -0
  27. data/lib/devp2p/kademlia/protocol.rb +284 -0
  28. data/lib/devp2p/kademlia/routing_table.rb +131 -0
  29. data/lib/devp2p/kademlia/wire_interface.rb +30 -0
  30. data/lib/devp2p/multiplexed_session.rb +110 -0
  31. data/lib/devp2p/multiplexer.rb +358 -0
  32. data/lib/devp2p/p2p_protocol.rb +170 -0
  33. data/lib/devp2p/packet.rb +35 -0
  34. data/lib/devp2p/peer.rb +329 -0
  35. data/lib/devp2p/peer_errors.rb +35 -0
  36. data/lib/devp2p/peer_manager.rb +274 -0
  37. data/lib/devp2p/rlpx_session.rb +434 -0
  38. data/lib/devp2p/sync_queue.rb +76 -0
  39. data/lib/devp2p/utils.rb +106 -0
  40. data/lib/devp2p/version.rb +13 -0
  41. data/lib/devp2p/wired_service.rb +30 -0
  42. metadata +227 -0
@@ -0,0 +1,35 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ class PeerErrorsBase
6
+
7
+ def add(address, error, client_version='')
8
+ # do nothing
9
+ end
10
+
11
+ end
12
+
13
+ class PeerErrors < PeerErrorsBase
14
+
15
+ def initialize
16
+ @errors = Hash.new {|h, k| h[k] = [] } # node:['error']
17
+ @client_versions = {} # address: client_version
18
+
19
+ at_exit do
20
+ @errors.each do |k, v|
21
+ puts "#{k} #{@client_versions.fetch(k, '')}"
22
+ puts v.join("\t")
23
+ end
24
+ end
25
+ end
26
+
27
+ def add(address, error, client_version='')
28
+ @errors[address].push error
29
+ @client_versions[address] = client_version unless client_version.nil? || client_version.empty?
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
@@ -0,0 +1,274 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ module DEVp2p
4
+
5
+ ##
6
+ # connection strategy
7
+ # for service which requires peers
8
+ # while peers.size > min_num_peers
9
+ # gen random id
10
+ # resolve closest node address
11
+ # [ideally know their services]
12
+ # connect closest node
13
+ #
14
+ class PeerManager < WiredService
15
+ include Celluloid::IO
16
+ finalizer :cleanup
17
+
18
+ name 'peermanager'
19
+ required_services []
20
+
21
+ default_config(
22
+ p2p: {
23
+ bootstrap_nodes: [],
24
+ min_peers: 5,
25
+ max_peers: 10,
26
+ listen_port: 30303,
27
+ listen_host: '0.0.0.0'
28
+ },
29
+ log_disconnects: false,
30
+ node: {privkey_hex: ''}
31
+ )
32
+
33
+ def initialize(app)
34
+ super(app)
35
+
36
+ logger.info "PeerManager init"
37
+
38
+ @peers = []
39
+ @errors = @config[:log_disconnects] ? PeerErrors.new : PeerErrorsBase.new
40
+
41
+ @wire_protocol = P2PProtocol
42
+
43
+ # setup nodeid based on privkey
44
+ unless @config[:p2p].has_key?(:id)
45
+ @config[:node][:id] = Crypto.privtopub Utils.decode_hex(@config[:node][:privkey_hex])
46
+ end
47
+
48
+ @connect_timeout = 2.0
49
+ @connect_loop_delay = 0.1
50
+ @discovery_delay = 0.5
51
+
52
+ @host = @config[:p2p][:listen_host]
53
+ @port = @config[:p2p][:listen_port]
54
+ end
55
+
56
+ def start
57
+ logger.info "starting peermanager"
58
+
59
+ logger.info "starting tcp listener", host: @host, port: @port
60
+ @server = TCPServer.new @host, @port
61
+
62
+ super
63
+ after(0.01) { discovery_loop }
64
+ end
65
+
66
+ def stop
67
+ logger.info "stopping peermanager"
68
+
69
+ @server.close if @server
70
+ @peers.each(&:stop)
71
+
72
+ super
73
+ end
74
+
75
+ def _run
76
+ loop do
77
+ break if stopped?
78
+ async.handle_connection @server.accept
79
+ end
80
+ rescue IOError
81
+ logger.error "listening error: #{$!}"
82
+ puts $!
83
+ @stopped = true
84
+ end
85
+
86
+ def add(peer)
87
+ @peers.push peer
88
+ #link peer
89
+ end
90
+
91
+ def delete(peer)
92
+ @peers.delete peer
93
+ end
94
+
95
+ def on_hello_received(proto, version, client_version_string, capabilities, listen_port, remote_pubkey)
96
+ logger.debug 'hello_received', listen_port: listen_port, peer: proto.peer, num_peers: @peers.size
97
+
98
+ if @peers.size > @config[:p2p][:max_peers]
99
+ logger.debug "too many peers", max: @config[:p2p][:max_peers]
100
+ proto.send_disconnect proto.class::Disconnect::Reason[:too_many_peers]
101
+ return false
102
+ end
103
+ if @peers.select {|p| p != proto.peer }.include?(remote_pubkey)
104
+ logger.debug "connected to that node already"
105
+ proto.send_disconnect proto.class::Disconnect::Reason[:useless_peer]
106
+ return false
107
+ end
108
+
109
+ return true
110
+ end
111
+
112
+ def wired_services
113
+ app.services.values.select {|s| s.is_a?(WiredService) }
114
+ end
115
+
116
+ def broadcast(protocol, command_name, args=[], kwargs={}, num_peers=nil, exclude_peers=[])
117
+ logger.debug "broadcasting", protocol: protocol, command: command_name, num_peers: num_peers, exclude_peers: exclude_peers
118
+ raise ArgumentError, 'invalid num_peers' unless num_peers.nil? || num_peers > 0
119
+
120
+ peers_with_proto = @peers.select {|p| p.protocols.include?(protocol) && !exclude_peers.include?(p) }
121
+ if peers_with_proto.empty?
122
+ logger.debug "no peers with protocol found", protos: @peers.select {|p| p.protocols }
123
+ end
124
+
125
+ num_peers ||= peers_with_proto.size
126
+ peers_with_proto.sample([num_peers, peers_with_proto.size].min).each do |peer|
127
+ logger.debug "broadcasting to", proto: peer.protocols[protocol]
128
+
129
+ args.push kwargs
130
+ peer.protocols[protocol].send "send_#{command_name}", *args
131
+
132
+ peer.safe_to_read.wait
133
+ logger.debug "broadcasting done", ts: Time.now
134
+ end
135
+ end
136
+
137
+ ##
138
+ # Connect to address (a 2-tuple [host, port]) and return the socket object.
139
+ #
140
+ # Passing the optional timeout parameter will set the timeout.
141
+ #
142
+ def connect(host, port, remote_pubkey)
143
+ socket = create_connection host, port, @connect_timeout
144
+ logger.debug "connecting to", peer: socket.peeraddr
145
+
146
+ start_peer socket, remote_pubkey
147
+ true
148
+ rescue Errno::ETIMEDOUT
149
+ address = "#{host}:#{port}"
150
+ logger.debug "connection timeout", address: address, timeout: @connect_timeout
151
+ @errors.add address, 'connection timeout'
152
+ false
153
+ rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ECONNREFUSED
154
+ address = "#{host}:#{port}"
155
+ logger.debug "connection error #{$!}"
156
+ @errors.add address, "connection error #{$!}"
157
+ false
158
+ end
159
+
160
+ def num_peers
161
+ active = @peers.select {|p| !p.stopped? }
162
+
163
+ if @peers.size != active.size
164
+ logger.error "stopped peers in peers list", inlist: @peers.size, active: active.size
165
+ end
166
+
167
+ active.size
168
+ end
169
+
170
+ def add_error(*args)
171
+ @errors.add *args
172
+ end
173
+
174
+ private
175
+
176
+ def logger
177
+ @logger ||= Logger.new "#{@config[:p2p][:listen_port]}.p2p.peermgr"
178
+ end
179
+
180
+ def bootstrap(bootstrap_nodes=[])
181
+ bootstrap_nodes.each do |uri|
182
+ ip, port, pubkey = Utils.host_port_pubkey_from_uri uri
183
+ logger.info 'connecting bootstrap server', uri: uri
184
+
185
+ begin
186
+ connect ip, port, pubkey
187
+ rescue Errno::ECONNRESET, Errno::ECONNABORTED, Errno::ECONNREFUSED, Errno::ETIMEDOUT
188
+ logger.warn "connecting bootstrap server failed: #{$!}"
189
+ end
190
+ end
191
+ end
192
+
193
+ def handle_connection(socket)
194
+ _, port, host = socket.peeraddr
195
+ logger.debug "incoming connection", host: host, port: port
196
+
197
+ peer = start_peer socket
198
+ #Celluloid::Actor.join peer
199
+ rescue EOFError
200
+ logger.debug "connection disconnected", host: host, port: port
201
+ socket.close
202
+ end
203
+
204
+ # FIXME: TODO: timeout is ignored!
205
+ def create_connection(host, port, timeout)
206
+ Celluloid::IO::TCPSocket.new ::TCPSocket.new(host, port)
207
+ end
208
+
209
+ def start_peer(socket, remote_pubkey=nil)
210
+ peer = Peer.new Actor.current, socket, remote_pubkey
211
+ logger.debug "created new peer", peer: peer, fileno: socket.to_io.fileno
212
+
213
+ add peer
214
+ peer.start
215
+
216
+ logger.debug "peer started", peer: peer, fileno: socket.to_io.fileno
217
+ raise PeerError, 'connection closed' if socket.closed?
218
+
219
+ peer
220
+ end
221
+
222
+ def discovery_loop
223
+ logger.info "waiting for bootstrap"
224
+ sleep @discovery_delay
225
+
226
+ while !stopped?
227
+ num, min = num_peers, @config[:p2p][:min_peers]
228
+
229
+ begin
230
+ kademlia_proto = app.services.discovery.protocol.kademlia
231
+ rescue NoMethodError # some point hit nil
232
+ logger.error "Discovery service not available."
233
+ break
234
+ end
235
+
236
+ if num < min
237
+ logger.debug "missing peers", num_peers: num, min_peers: min, known: kademlia_proto.routing.size
238
+
239
+ nodeid = Kademlia.random_nodeid
240
+
241
+ kademlia_proto.find_node nodeid
242
+ sleep @discovery_delay
243
+
244
+ neighbours = kademlia_proto.routing.neighbours(nodeid, 2)
245
+ if neighbours.empty?
246
+ sleep @connect_loop_delay
247
+ next
248
+ end
249
+
250
+ node = neighbours.sample
251
+ logger.debug 'connecting random neighbour', node: node
252
+
253
+ local_pubkey = Crypto.privtopub Utils.decode_hex(@config[:node][:privkey_hex])
254
+ next if node.pubkey == local_pubkey
255
+ next if @peers.any? {|p| p.remote_pubkey == node.pubkey }
256
+
257
+ connect node.address.ip, node.address.tcp_port, node.pubkey
258
+ end
259
+
260
+ sleep @connect_loop_delay
261
+ end
262
+
263
+ # TODO: ???
264
+ cond = Celluloid::Condition.new
265
+ cond.wait
266
+ end
267
+
268
+ def cleanup
269
+ @server.close if @server && !@server.closed?
270
+ end
271
+
272
+ end
273
+
274
+ end
@@ -0,0 +1,434 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ require 'securerandom'
4
+ require 'digest/sha3'
5
+
6
+ module DEVp2p
7
+ class RLPxSession
8
+
9
+ SUPPORTED_RLPX_VERSION = 4
10
+
11
+ ENC_CIPHER = 'AES-256-CTR'
12
+ MAC_CIPHER = 'AES-256-ECB'
13
+
14
+ extend Configurable
15
+ add_config(
16
+ eip8_auth_sedes: RLP::Sedes::List.new(
17
+ elements: [
18
+ RLP::Sedes::Binary.new(min_length: 65, max_length: 65), # sig
19
+ RLP::Sedes::Binary.new(min_length: 64, max_length: 64), # pubkey
20
+ RLP::Sedes::Binary.new(min_length: 32, max_length: 32), # nonce
21
+ RLP::Sedes::BigEndianInt.new, # version
22
+ ],
23
+ strict: false
24
+ ),
25
+ eip8_ack_sedes: RLP::Sedes::List.new(
26
+ elements: [
27
+ RLP::Sedes::Binary.new(min_length: 64, max_length: 64), # ephemeral pubkey
28
+ RLP::Sedes::Binary.new(min_length: 32, max_length: 32), # nonce
29
+ RLP::Sedes::BigEndianInt.new # version
30
+ ],
31
+ strict: false
32
+ )
33
+ )
34
+
35
+ attr :ecc, :ephemeral_ecc,
36
+ :initiator_nonce, :responder_nonce,
37
+ :remote_version, :remote_pubkey, :remote_ephemeral_pubkey
38
+
39
+ def initialize(ecc, is_initiator=false, ephemeral_privkey=nil)
40
+ @ecc = ecc
41
+ @is_initiator = is_initiator
42
+ @ephemeral_ecc = Crypto::ECCx.new ephemeral_privkey
43
+
44
+ @ready = false
45
+ @got_eip8_auth, @got_eip8_ack = false, false
46
+ end
47
+
48
+ ### Frame Handling
49
+
50
+ def encrypt(header, frame)
51
+ raise RLPxSessionError, 'not ready' unless ready?
52
+ raise ArgumentError, 'invalid header length' unless header.size == 16
53
+ raise ArgumentError, 'invalid frame padding' unless frame.size % 16 == 0
54
+
55
+ header_ciphertext = aes_enc header
56
+ raise RLPxSessionError unless header_ciphertext.size == header.size
57
+ header_mac = egress_mac(Utils.sxor(mac_enc(egress_mac[0,16]), header_ciphertext))[0,16]
58
+
59
+ frame_ciphertext = aes_enc frame
60
+ raise RLPxSessionError unless frame_ciphertext.size == frame.size
61
+ fmac_seed = egress_mac frame_ciphertext
62
+ frame_mac = egress_mac(Utils.sxor(mac_enc(egress_mac[0,16]), fmac_seed[0,16]))[0,16]
63
+
64
+ header_ciphertext + header_mac + frame_ciphertext + frame_mac
65
+ end
66
+
67
+ def decrypt_header(data)
68
+ raise RLPxSessionError, 'not ready' unless ready?
69
+ raise ArgumentError, 'invalid data length' unless data.size == 32
70
+
71
+ header_ciphertext = data[0,16]
72
+ header_mac = data[16,16]
73
+
74
+ expected_header_mac = ingress_mac(Utils.sxor(mac_enc(ingress_mac[0,16]), header_ciphertext))[0,16]
75
+ raise AuthenticationError, 'invalid header mac' unless expected_header_mac == header_mac
76
+
77
+ aes_dec header_ciphertext
78
+ end
79
+
80
+ def decrypt_body(data, body_size)
81
+ raise RLPxSessionError, 'not ready' unless ready?
82
+
83
+ read_size = Utils.ceil16 body_size
84
+ raise FormatError, 'insufficient body length' unless data.size >= read_size + 16
85
+
86
+ frame_ciphertext = data[0, read_size]
87
+ frame_mac = data[read_size, 16]
88
+ raise RLPxSessionError, 'invalid frame mac length' unless frame_mac.size == 16
89
+
90
+ fmac_seed = ingress_mac frame_ciphertext
91
+ expected_frame_mac = ingress_mac(Utils.sxor(mac_enc(ingress_mac[0,16]), fmac_seed[0,16]))[0,16]
92
+ raise AuthenticationError, 'invalid frame mac' unless expected_frame_mac == frame_mac
93
+
94
+ aes_dec(frame_ciphertext)[0,body_size]
95
+ end
96
+
97
+ def decrypt(data)
98
+ header = decrypt_header data[0,32]
99
+ body_size = Frame.decode_body_size header
100
+
101
+ len = 32 + Utils.ceil16(body_size) + 16
102
+ raise FormatError, 'insufficient body length' unless data.size >= len
103
+
104
+ frame = decrypt_body data[32..-1], body_size
105
+ {header: header, frame: frame, bytes_read: len}
106
+ end
107
+
108
+ ### Handshake Auth Message Handling
109
+
110
+ ##
111
+ # 1. initiator generates ecdhe-random and nonce and creates auth
112
+ # 2. initiator connects to remote and sends auth
113
+ #
114
+ # New:
115
+ #
116
+ # E(remote-pubk,
117
+ # S(ephemeral-privk, ecdh-shared-secret ^ nonce) ||
118
+ # H(ephemeral-pubk) || pubk || nonce || 0x0
119
+ # )
120
+ #
121
+ # Known:
122
+ #
123
+ # E(remote-pubk,
124
+ # S(ephemeral-privk, token ^ nonce) ||
125
+ # H(ephemeral-pubk) || pubk || nonce || 0x1
126
+ # )
127
+ #
128
+ def create_auth_message(remote_pubkey, ephemeral_privkey=nil, nonce=nil)
129
+ raise RLPxSessionError, 'must be initiator' unless initiator?
130
+ raise InvalidKeyError, 'invalid remote pubkey' unless Crypto::ECCx.valid_key?(remote_pubkey)
131
+
132
+ @remote_pubkey = remote_pubkey
133
+
134
+ token = @ecc.get_ecdh_key remote_pubkey
135
+ flag = 0x0
136
+
137
+ @initiator_nonce = nonce || Crypto.keccak256(Utils.int_to_big_endian(SecureRandom.random_number(TT256)))
138
+ raise RLPxSessionError, 'invalid nonce length' unless @initiator_nonce.size == 32
139
+
140
+ token_xor_nonce = Utils.sxor token, @initiator_nonce
141
+ raise RLPxSessionError, 'invalid token xor nonce length' unless token_xor_nonce.size == 32
142
+
143
+ ephemeral_pubkey = @ephemeral_ecc.raw_pubkey
144
+ raise InvalidKeyError, 'invalid ephemeral pubkey' unless ephemeral_pubkey.size == 512 / 8 && Crypto::ECCx.valid_key?(ephemeral_pubkey)
145
+
146
+ sig = @ephemeral_ecc.sign token_xor_nonce
147
+ raise RLPxSessionError, 'invalid signature' unless sig.size == 65
148
+
149
+ auth_message = "#{sig}#{Crypto.keccak256(ephemeral_pubkey)}#{@ecc.raw_pubkey}#{@initiator_nonce}#{flag.chr}"
150
+ raise RLPxSessionError, 'invalid auth message length' unless auth_message.size == 194
151
+
152
+ auth_message
153
+ end
154
+
155
+ def encrypt_auth_message(auth_message, remote_pubkey=nil)
156
+ raise RLPxSessionError, 'must be initiator' unless initiator?
157
+
158
+ remote_pubkey ||= @remote_pubkey
159
+ @auth_init = @ecc.ecies_encrypt auth_message, remote_pubkey
160
+ raise RLPxSessionError, 'invalid encrypted auth message length' unless @auth_init.size == 307
161
+
162
+ @auth_init
163
+ end
164
+
165
+ ##
166
+ # 3. optionally, remote decrypts and verifies auth (checks that recovery of
167
+ # signature == H(ephemeral-pubk))
168
+ # 4. remote generates authAck from remote-ephemeral-pubk and nonce (authAck
169
+ # = authRecipient handshake)
170
+ #
171
+ # optional: remote derives secrets and preemptively sends
172
+ # protocol-handshake (steps 9,11,8,10)
173
+ #
174
+ def decode_authentication(ciphertext)
175
+ raise RLPxSessionError, 'must not be initiator' if initiator?
176
+ raise ArgumentError, 'invalid ciphertext length' unless ciphertext.size >= 307
177
+
178
+ result = nil
179
+ begin
180
+ result = decode_auth_plain ciphertext
181
+ rescue AuthenticationError
182
+ result = decode_auth_eip8 ciphertext
183
+ @got_eip8_auth = true
184
+ end
185
+ size, sig, initiator_pubkey, nonce, version = result
186
+
187
+ @auth_init = ciphertext[0, size]
188
+
189
+ token = @ecc.get_ecdh_key initiator_pubkey
190
+ @remote_ephemeral_pubkey = Crypto.ecdsa_recover(Utils.sxor(token, nonce), sig)
191
+ raise InvalidKeyError, 'invalid remote ephemeral pubkey' unless Crypto::ECCx.valid_key?(@remote_ephemeral_pubkey)
192
+
193
+ @initiator_nonce = nonce
194
+ @remote_pubkey = initiator_pubkey
195
+ @remote_version = version
196
+
197
+ ciphertext[size..-1]
198
+ end
199
+
200
+ ### Handshake ack message handling
201
+
202
+ ##
203
+ # authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x1) // token found
204
+ # authRecipient = E(remote-pubk, remote-ephemeral-pubk || nonce || 0x0) // token not found
205
+ #
206
+ # nonce, ephemeral_pubkey, version are local
207
+ #
208
+ def create_auth_ack_message(ephemeral_pubkey=nil, nonce=nil, version=SUPPORTED_RLPX_VERSION, eip8=false)
209
+ raise RLPxSessionError, 'must not be initiator' if initiator?
210
+
211
+ ephemeral_pubkey = ephemeral_pubkey || @ephemeral_ecc.raw_pubkey
212
+ @responder_nonce = nonce || Crypto.keccak256(Utils.int_to_big_endian(SecureRandom.random_number(TT256)))
213
+
214
+ if eip8 || @got_eip8_auth
215
+ msg = create_eip8_auth_ack_message ephemeral_pubkey, @responder_nonce, version
216
+ raise RLPxSessionError, 'invalid msg size' unless msg.size > 97
217
+ else
218
+ msg = "#{ephemeral_pubkey}#{@responder_nonce}\x00"
219
+ raise RLPxSessionError, 'invalid msg size' unless msg.size == 97
220
+ end
221
+
222
+ msg
223
+ end
224
+
225
+ def create_eip8_auth_ack_message(ephemeral_pubkey, nonce, version)
226
+ data = RLP.encode [ephemeral_pubkey, nonce, version], sedes: eip8_ack_sedes
227
+ pad = SecureRandom.random_bytes(SecureRandom.random_number(151)+100) # (100..150) random bytes
228
+ "#{data}#{pad}"
229
+ end
230
+
231
+ def encrypt_auth_ack_message(ack_message, eip8=false, remote_pubkey=nil)
232
+ raise RLPxSessionError, 'must not be initiator' if initiator?
233
+
234
+ remote_pubkey ||= @remote_pubkey
235
+
236
+ if eip8 || @got_eip8_auth
237
+ # The EIP-8 version has an authenticated length prefix
238
+ prefix = [ack_message.size + Crypto::ECIES::ENCRYPT_OVERHEAD_LENGTH].pack("S>")
239
+ @auth_ack = "#{prefix}#{@ecc.ecies_encrypt(ack_message, remote_pubkey, prefix)}"
240
+ else
241
+ @auth_ack = @ecc.ecies_encrypt ack_message, remote_pubkey
242
+ raise RLPxSessionError, 'invalid auth ack message length' unless @auth_ack.size == 210
243
+ end
244
+
245
+ @auth_ack
246
+ end
247
+
248
+ def decode_auth_ack_message(ciphertext)
249
+ raise RLPxSessionError, 'must be initiator' unless initiator?
250
+ raise ArgumentError, 'invalid ciphertext length' unless ciphertext.size >= 210
251
+
252
+ result = nil
253
+ begin
254
+ result = decode_ack_plain ciphertext
255
+ rescue AuthenticationError
256
+ result = decode_ack_eip8 ciphertext
257
+ @got_eip8_ack = true
258
+ end
259
+ size, ephemeral_pubkey, nonce, version = result
260
+
261
+ @auth_ack = ciphertext[0,size]
262
+ @remote_ephemeral_pubkey = ephemeral_pubkey[0,64]
263
+ @responder_nonce = nonce
264
+ @remote_version = version
265
+
266
+ raise InvalidKeyError, 'invalid remote ephemeral pubkey' unless Crypto::ECCx.valid_key?(@remote_ephemeral_pubkey)
267
+
268
+ ciphertext[size..-1]
269
+ end
270
+
271
+ ### Handshake Key Derivation
272
+
273
+ def setup_cipher
274
+ raise RLPxSessionError, 'missing responder nonce' unless @responder_nonce
275
+ raise RLPxSessionError, 'missing initiator_nonce' unless @initiator_nonce
276
+ raise RLPxSessionError, 'missing auth_init' unless @auth_init
277
+ raise RLPxSessionError, 'missing auth_ack' unless @auth_ack
278
+ raise RLPxSessionError, 'missing remote ephemeral pubkey' unless @remote_ephemeral_pubkey
279
+ raise InvalidKeyError, 'invalid remote ephemeral pubkey' unless Crypto::ECCx.valid_key?(@remote_ephemeral_pubkey)
280
+
281
+ # derive base secrets from ephemeral key agreement
282
+ # ecdhe-shared-secret = ecdh.agree(ephemeral-privkey, remote-ephemeral-pubk)
283
+ @ecdhe_shared_secret = @ephemeral_ecc.get_ecdh_key(@remote_ephemeral_pubkey)
284
+ @shared_secret = Crypto.keccak256("#{@ecdhe_shared_secret}#{Crypto.keccak256(@responder_nonce + @initiator_nonce)}")
285
+ @token = Crypto.keccak256 @shared_secret
286
+ @aes_secret = Crypto.keccak256 "#{@ecdhe_shared_secret}#{@shared_secret}"
287
+ @mac_secret = Crypto.keccak256 "#{@ecdhe_shared_secret}#{@aes_secret}"
288
+
289
+ mac1 = keccak256 "#{Utils.sxor(@mac_secret, @responder_nonce)}#{@auth_init}"
290
+ mac2 = keccak256 "#{Utils.sxor(@mac_secret, @initiator_nonce)}#{@auth_ack}"
291
+
292
+ if initiator?
293
+ @egress_mac, @ingress_mac = mac1, mac2
294
+ else
295
+ @egress_mac, @ingress_mac = mac2, mac1
296
+ end
297
+
298
+ iv = "\x00" * 16
299
+ @aes_enc = OpenSSL::Cipher.new(ENC_CIPHER).tap do |c|
300
+ c.encrypt
301
+ c.iv = iv
302
+ c.key = @aes_secret
303
+ end
304
+ @aes_dec = OpenSSL::Cipher.new(ENC_CIPHER).tap do |c|
305
+ c.decrypt
306
+ c.iv = iv
307
+ c.key = @aes_secret
308
+ end
309
+ @mac_enc = OpenSSL::Cipher.new(MAC_CIPHER).tap do |c|
310
+ c.encrypt
311
+ c.key = @mac_secret
312
+ end
313
+
314
+ @ready = true
315
+ end
316
+
317
+ ### Helpers
318
+
319
+ def ready?
320
+ @ready
321
+ end
322
+
323
+ def initiator?
324
+ @is_initiator
325
+ end
326
+
327
+ def mac_enc(data)
328
+ @mac_enc.update data
329
+ end
330
+
331
+ def aes_enc(data='')
332
+ @aes_enc.update data
333
+ end
334
+
335
+ def aes_dec(data='')
336
+ @aes_dec.update data
337
+ end
338
+
339
+ def egress_mac(data='')
340
+ @egress_mac.update data
341
+ return @egress_mac.digest
342
+ end
343
+
344
+ def ingress_mac(data='')
345
+ @ingress_mac.update data
346
+ return @ingress_mac.digest
347
+ end
348
+
349
+ private
350
+
351
+ def keccak256(x)
352
+ Digest::SHA3.new(256).tap {|d| d.update x }
353
+ end
354
+
355
+ ##
356
+ # decode legacy pre-EIP-8 auth message format
357
+ #
358
+ def decode_auth_plain(ciphertext)
359
+ message = begin
360
+ @ecc.ecies_decrypt ciphertext[0,307]
361
+ rescue
362
+ raise AuthenticationError, $!
363
+ end
364
+ raise RLPxSessionError, 'invalid message length' unless message.size == 194
365
+
366
+ sig = message[0,65]
367
+ pubkey = message[65+32,64]
368
+ raise InvalidKeyError, 'invalid initiator pubkey' unless Crypto::ECCx.valid_key?(pubkey)
369
+
370
+ nonce = message[65+32+64,32]
371
+ flag = message[(65+32+64+32)..-1].ord
372
+ raise RLPxSessionError, 'invalid flag' unless flag == 0
373
+
374
+ [307, sig, pubkey, nonce, 4]
375
+ end
376
+
377
+ ##
378
+ # decode EIP-8 auth message format
379
+ #
380
+ def decode_auth_eip8(ciphertext)
381
+ size = ciphertext[0,2].unpack('S>').first + 2
382
+ raise RLPxSessionError, 'invalid ciphertext size' unless ciphertext.size >= size
383
+
384
+ message = begin
385
+ @ecc.ecies_decrypt ciphertext[2...size], ciphertext[0,2]
386
+ rescue
387
+ raise AuthenticationError, $!
388
+ end
389
+
390
+ values = RLP.decode message, sedes: eip8_auth_sedes, strict: false
391
+ raise RLPxSessionError, 'invalid values size' unless values.size >= 4
392
+
393
+ [size] + values[0,4]
394
+ end
395
+
396
+ ##
397
+ # decode legacy pre-EIP-8 ack message format
398
+ #
399
+ def decode_ack_plain(ciphertext)
400
+ message = begin
401
+ @ecc.ecies_decrypt ciphertext[0,210]
402
+ rescue
403
+ raise AuthenticationError, $!
404
+ end
405
+ raise RLPxSessionError, 'invalid message length' unless message.size == 64+32+1
406
+
407
+ ephemeral_pubkey = message[0,64]
408
+ nonce = message[64,32]
409
+ known = message[-1].ord
410
+ raise RLPxSessionError, 'invalid known byte' unless known == 0
411
+
412
+ [210, ephemeral_pubkey, nonce, 4]
413
+ end
414
+
415
+ ##
416
+ # decode EIP-8 ack message format
417
+ #
418
+ def decode_ack_eip8(ciphertext)
419
+ size = ciphertext[0,2].unpack('S>').first + 2
420
+ raise RLPxSessionError, 'invalid ciphertext length' unless ciphertext.size == size
421
+
422
+ message = begin
423
+ @ecc.ecies_decrypt(ciphertext[2...size], ciphertext[0,2])
424
+ rescue
425
+ raise AuthenticationError, $!
426
+ end
427
+ values = RLP.decode message, sedes: eip8_ack_sedes, strict: false
428
+ raise RLPxSessionError, 'invalid values length' unless values.size >= 3
429
+
430
+ [size] + values[0,3]
431
+ end
432
+
433
+ end
434
+ end