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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +22 -0
- data/lib/devp2p.rb +57 -0
- data/lib/devp2p/app_helper.rb +85 -0
- data/lib/devp2p/base_app.rb +80 -0
- data/lib/devp2p/base_protocol.rb +136 -0
- data/lib/devp2p/base_service.rb +55 -0
- data/lib/devp2p/command.rb +82 -0
- data/lib/devp2p/configurable.rb +32 -0
- data/lib/devp2p/connection_monitor.rb +77 -0
- data/lib/devp2p/control.rb +32 -0
- data/lib/devp2p/crypto.rb +73 -0
- data/lib/devp2p/crypto/ecc_x.rb +133 -0
- data/lib/devp2p/crypto/ecies.rb +134 -0
- data/lib/devp2p/discovery.rb +118 -0
- data/lib/devp2p/discovery/address.rb +83 -0
- data/lib/devp2p/discovery/kademlia_protocol_adapter.rb +11 -0
- data/lib/devp2p/discovery/node.rb +32 -0
- data/lib/devp2p/discovery/protocol.rb +342 -0
- data/lib/devp2p/discovery/transport.rb +105 -0
- data/lib/devp2p/exception.rb +30 -0
- data/lib/devp2p/frame.rb +197 -0
- data/lib/devp2p/kademlia.rb +48 -0
- data/lib/devp2p/kademlia/k_bucket.rb +178 -0
- data/lib/devp2p/kademlia/node.rb +40 -0
- data/lib/devp2p/kademlia/protocol.rb +284 -0
- data/lib/devp2p/kademlia/routing_table.rb +131 -0
- data/lib/devp2p/kademlia/wire_interface.rb +30 -0
- data/lib/devp2p/multiplexed_session.rb +110 -0
- data/lib/devp2p/multiplexer.rb +358 -0
- data/lib/devp2p/p2p_protocol.rb +170 -0
- data/lib/devp2p/packet.rb +35 -0
- data/lib/devp2p/peer.rb +329 -0
- data/lib/devp2p/peer_errors.rb +35 -0
- data/lib/devp2p/peer_manager.rb +274 -0
- data/lib/devp2p/rlpx_session.rb +434 -0
- data/lib/devp2p/sync_queue.rb +76 -0
- data/lib/devp2p/utils.rb +106 -0
- data/lib/devp2p/version.rb +13 -0
- data/lib/devp2p/wired_service.rb +30 -0
- 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
|