ciri 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/ciri.gemspec +32 -0
- data/lib/ciri.rb +26 -0
- data/lib/ciri/crypto.rb +143 -0
- data/lib/ciri/devp2p/actor.rb +224 -0
- data/lib/ciri/devp2p/peer.rb +132 -0
- data/lib/ciri/devp2p/protocol.rb +44 -0
- data/lib/ciri/devp2p/protocol_io.rb +66 -0
- data/lib/ciri/devp2p/rlpx.rb +28 -0
- data/lib/ciri/devp2p/rlpx/connection.rb +179 -0
- data/lib/ciri/devp2p/rlpx/encryption_handshake.rb +143 -0
- data/lib/ciri/devp2p/rlpx/error.rb +34 -0
- data/lib/ciri/devp2p/rlpx/frame_io.rb +221 -0
- data/lib/ciri/devp2p/rlpx/message.rb +45 -0
- data/lib/ciri/devp2p/rlpx/node.rb +77 -0
- data/lib/ciri/devp2p/rlpx/protocol_handshake.rb +55 -0
- data/lib/ciri/devp2p/rlpx/protocol_messages.rb +69 -0
- data/lib/ciri/devp2p/rlpx/secrets.rb +49 -0
- data/lib/ciri/devp2p/server.rb +207 -0
- data/lib/ciri/key.rb +81 -0
- data/lib/ciri/rlp.rb +88 -0
- data/lib/ciri/rlp/decode.rb +81 -0
- data/lib/ciri/rlp/encode.rb +79 -0
- data/lib/ciri/rlp/serializable.rb +268 -0
- data/lib/ciri/utils.rb +75 -0
- data/lib/ciri/version.rb +5 -0
- metadata +179 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
|
4
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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
|
+
require_relative 'rlpx/node'
|
25
|
+
require_relative 'rlpx/message'
|
26
|
+
require_relative 'rlpx/frame_io'
|
27
|
+
require_relative 'rlpx/protocol_messages'
|
28
|
+
require_relative 'rlpx/encryption_handshake'
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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
|
+
require 'socket'
|
26
|
+
require 'forwardable'
|
27
|
+
require_relative 'frame_io'
|
28
|
+
require_relative 'protocol_messages'
|
29
|
+
require_relative 'error'
|
30
|
+
require_relative 'encryption_handshake'
|
31
|
+
|
32
|
+
module Ciri
|
33
|
+
module DevP2P
|
34
|
+
module RLPX
|
35
|
+
|
36
|
+
# RLPX::Connection implement RLPX protocol operations
|
37
|
+
# all operations end with bang(!)
|
38
|
+
class Connection
|
39
|
+
extend Forwardable
|
40
|
+
|
41
|
+
def_delegators :@frame_io, :read_msg, :write_msg, :send_data
|
42
|
+
|
43
|
+
class Error < RLPX::Error
|
44
|
+
end
|
45
|
+
|
46
|
+
class MessageOverflowError < Error
|
47
|
+
end
|
48
|
+
|
49
|
+
class UnexpectedMessageError < Error
|
50
|
+
end
|
51
|
+
|
52
|
+
class FormatError < Error
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(io)
|
56
|
+
set_timeout(io)
|
57
|
+
@io = io
|
58
|
+
@frame_io = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# Encryption handshake, exchange keys with node, must been invoked before other operations
|
62
|
+
def encryption_handshake!(private_key:, node_id: nil)
|
63
|
+
enc_handshake = EncryptionHandshake.new(private_key: private_key, remote_id: node_id)
|
64
|
+
secrets = node_id.nil? ? receiver_enc_handshake(enc_handshake) : initiator_enc_handshake(enc_handshake)
|
65
|
+
@frame_io = FrameIO.new(@io, secrets)
|
66
|
+
end
|
67
|
+
|
68
|
+
# protocol handshake
|
69
|
+
def protocol_handshake!(our_hs)
|
70
|
+
@frame_io.send_data(MESSAGES[:handshake], our_hs.rlp_encode!)
|
71
|
+
remote_hs = read_protocol_handshake
|
72
|
+
# enable snappy compress if remote peer support
|
73
|
+
@frame_io.snappy = remote_hs.version >= SNAPPY_PROTOCOL_VERSION
|
74
|
+
remote_hs
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
def receiver_enc_handshake(receiver)
|
79
|
+
auth_msg_binary, auth_packet = read_enc_handshake_msg(ENC_AUTH_MSG_LENGTH, receiver.private_key)
|
80
|
+
auth_msg = AuthMsgV4.rlp_decode(auth_msg_binary)
|
81
|
+
receiver.handle_auth_msg(auth_msg)
|
82
|
+
|
83
|
+
auth_ack_msg = receiver.auth_ack_msg
|
84
|
+
auth_ack_msg_plain_text = auth_ack_msg.rlp_encode!
|
85
|
+
auth_ack_packet = if auth_msg.got_plain
|
86
|
+
raise NotImplementedError.new('not support pre eip8 plain text seal')
|
87
|
+
else
|
88
|
+
seal_eip8(auth_ack_msg_plain_text, receiver)
|
89
|
+
end
|
90
|
+
@io.write(auth_ack_packet)
|
91
|
+
|
92
|
+
receiver.extract_secrets(auth_packet, auth_ack_packet, initiator: false)
|
93
|
+
end
|
94
|
+
|
95
|
+
def initiator_enc_handshake(initiator)
|
96
|
+
initiator_auth_msg = initiator.auth_msg
|
97
|
+
auth_msg_plain_text = initiator_auth_msg.rlp_encode!
|
98
|
+
# seal eip8
|
99
|
+
auth_packet = seal_eip8(auth_msg_plain_text, initiator)
|
100
|
+
@io.write(auth_packet)
|
101
|
+
|
102
|
+
auth_ack_mgs_binary, auth_ack_packet = read_enc_handshake_msg(ENC_AUTH_RESP_MSG_LENGTH, initiator.private_key)
|
103
|
+
auth_ack_msg = AuthRespV4.rlp_decode! auth_ack_mgs_binary
|
104
|
+
initiator.handle_auth_ack_msg(auth_ack_msg)
|
105
|
+
|
106
|
+
initiator.extract_secrets(auth_packet, auth_ack_packet, initiator: true)
|
107
|
+
end
|
108
|
+
|
109
|
+
def read_enc_handshake_msg(plain_size, private_key)
|
110
|
+
packet = @io.read(plain_size)
|
111
|
+
|
112
|
+
decrypt_binary_msg = begin
|
113
|
+
private_key.ecies_decrypt(packet)
|
114
|
+
rescue Crypto::ECIESDecryptionError => e
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# pre eip old plain format
|
119
|
+
return decrypt_binary_msg if decrypt_binary_msg
|
120
|
+
|
121
|
+
# try decode eip8 format
|
122
|
+
prefix = packet[0...2]
|
123
|
+
size = Ciri::Utils.big_endian_decode(prefix)
|
124
|
+
raise FormatError.new("EIP8 format message size #{size} less than plain_size #{plain_size}") if size < plain_size
|
125
|
+
|
126
|
+
# continue read remain bytes
|
127
|
+
packet << @io.read(size - plain_size + 2)
|
128
|
+
# decrypt message
|
129
|
+
[private_key.ecies_decrypt(packet[2..-1], prefix), packet]
|
130
|
+
end
|
131
|
+
|
132
|
+
def read_protocol_handshake
|
133
|
+
msg = @frame_io.read_msg
|
134
|
+
|
135
|
+
if msg.size > BASE_PROTOCOL_MAX_MSG_SIZE
|
136
|
+
raise MessageOverflowError.new("message size #{msg.size} is too big")
|
137
|
+
end
|
138
|
+
if msg.code == MESSAGES[:discovery]
|
139
|
+
payload = RLP.decode(msg.payload)
|
140
|
+
raise UnexpectedMessageError.new("expected handshake, get discovery, reason: #{payload}")
|
141
|
+
end
|
142
|
+
if msg.code != MESSAGES[:handshake]
|
143
|
+
raise UnexpectedMessageError.new("expected handshake, get #{msg.code}")
|
144
|
+
end
|
145
|
+
ProtocolHandshake.rlp_decode!(msg.payload)
|
146
|
+
end
|
147
|
+
|
148
|
+
def set_timeout(io)
|
149
|
+
timeout = HANDSHAKE_TIMEOUT
|
150
|
+
|
151
|
+
if io.is_a?(BasicSocket)
|
152
|
+
secs = Integer(timeout)
|
153
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
154
|
+
optval = [secs, usecs].pack("l_2")
|
155
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
156
|
+
io.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def seal_eip8(encoded_msg, handshake)
|
161
|
+
# padding encoded message, make message distinguished from pre eip8
|
162
|
+
encoded_msg += "\x00".b * rand(100..300)
|
163
|
+
prefix = encoded_prefix(encoded_msg.size + ECIES_OVERHEAD)
|
164
|
+
|
165
|
+
enc = handshake.remote_key.ecies_encrypt(encoded_msg, prefix)
|
166
|
+
prefix + enc
|
167
|
+
end
|
168
|
+
|
169
|
+
# encode 16 uint prefix
|
170
|
+
def encoded_prefix(n)
|
171
|
+
prefix = Utils.big_endian_encode(n)
|
172
|
+
# pad to 2 bytes
|
173
|
+
prefix.ljust(2, "\x00".b)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
|
4
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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 'digest/sha3'
|
27
|
+
require_relative 'secrets'
|
28
|
+
|
29
|
+
module Ciri
|
30
|
+
module DevP2P
|
31
|
+
module RLPX
|
32
|
+
|
33
|
+
SHA_LENGTH = 32
|
34
|
+
SIGNATURE_LENGTH = 65
|
35
|
+
PUBLIC_KEY_LENGTH = 64
|
36
|
+
ECIES_OVERHEAD = 65 + 16 + 32
|
37
|
+
AUTH_MSG_LENGTH = SIGNATURE_LENGTH + SHA_LENGTH + PUBLIC_KEY_LENGTH + SHA_LENGTH + 1
|
38
|
+
AUTH_RESP_MSG_LENGTH = PUBLIC_KEY_LENGTH + SHA_LENGTH + 1
|
39
|
+
|
40
|
+
HANDSHAKE_TIMEOUT = 5
|
41
|
+
|
42
|
+
ENC_AUTH_MSG_LENGTH = AUTH_MSG_LENGTH + ECIES_OVERHEAD
|
43
|
+
ENC_AUTH_RESP_MSG_LENGTH = AUTH_RESP_MSG_LENGTH + ECIES_OVERHEAD
|
44
|
+
|
45
|
+
# handle key exchange handshake
|
46
|
+
class EncryptionHandshake
|
47
|
+
attr_reader :private_key, :remote_key, :remote_random_key, :initiator_nonce, :receiver_nonce, :remote_id
|
48
|
+
|
49
|
+
def initialize(private_key:, remote_id:)
|
50
|
+
@private_key = private_key
|
51
|
+
@remote_id = remote_id
|
52
|
+
end
|
53
|
+
|
54
|
+
def remote_key
|
55
|
+
@remote_key || @remote_id.key
|
56
|
+
end
|
57
|
+
|
58
|
+
def random_key
|
59
|
+
@random_key ||= Ciri::Key.random
|
60
|
+
end
|
61
|
+
|
62
|
+
def auth_msg
|
63
|
+
# make nonce bytes
|
64
|
+
nonce = random_nonce(SHA_LENGTH)
|
65
|
+
@initiator_nonce = nonce
|
66
|
+
# remote first byte tag
|
67
|
+
token = dh_compute_key(private_key, remote_key)
|
68
|
+
raise StandardError.new("token size #{token.size} not correct") if token.size != nonce.size
|
69
|
+
# xor
|
70
|
+
signed = xor(token, nonce)
|
71
|
+
|
72
|
+
signature = random_key.ecdsa_signature(signed)
|
73
|
+
initiator_pubkey = private_key.raw_public_key[1..-1]
|
74
|
+
AuthMsgV4.new(signature: signature, initiator_pubkey: initiator_pubkey, nonce: nonce, version: 4)
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_auth_msg(msg)
|
78
|
+
@remote_key = Ciri::Key.new(raw_public_key: "\x04" + msg.initiator_pubkey)
|
79
|
+
@initiator_nonce = msg.nonce
|
80
|
+
|
81
|
+
token = dh_compute_key(private_key, @remote_key)
|
82
|
+
signed = xor(token, msg.nonce)
|
83
|
+
@remote_random_key = Ciri::Key.ecdsa_recover(signed, msg.signature)
|
84
|
+
end
|
85
|
+
|
86
|
+
def auth_ack_msg
|
87
|
+
# make nonce bytes
|
88
|
+
nonce = random_nonce(SHA_LENGTH)
|
89
|
+
@receiver_nonce = nonce
|
90
|
+
random_pubkey = random_key.raw_public_key[1..-1]
|
91
|
+
AuthRespV4.new(random_pubkey: random_pubkey, nonce: nonce, version: 4)
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_auth_ack_msg(msg)
|
95
|
+
# make nonce bytes
|
96
|
+
@receiver_nonce = msg.nonce
|
97
|
+
@remote_random_key = Ciri::Key.new(raw_public_key: "\x04" + msg.random_pubkey)
|
98
|
+
end
|
99
|
+
|
100
|
+
def extract_secrets(auth_packet, auth_ack_packet, initiator:)
|
101
|
+
secret = dh_compute_key(random_key, remote_random_key)
|
102
|
+
shared_secret = Ciri::Utils.sha3(secret, Ciri::Utils.sha3(receiver_nonce, initiator_nonce))
|
103
|
+
aes_secret = Ciri::Utils.sha3(secret, shared_secret)
|
104
|
+
mac = Ciri::Utils.sha3(secret, aes_secret)
|
105
|
+
secrets = Secrets.new(remote_id: remote_id, aes: aes_secret, mac: mac)
|
106
|
+
|
107
|
+
# initial secrets macs
|
108
|
+
mac1 = Digest::SHA3.new(256)
|
109
|
+
mac1.update xor(mac, receiver_nonce)
|
110
|
+
mac1.update auth_packet
|
111
|
+
|
112
|
+
mac2 = Digest::SHA3.new(256)
|
113
|
+
mac2.update xor(mac, initiator_nonce)
|
114
|
+
mac2.update auth_ack_packet
|
115
|
+
|
116
|
+
if initiator
|
117
|
+
secrets.egress_mac = mac1
|
118
|
+
secrets.ingress_mac = mac2
|
119
|
+
else
|
120
|
+
secrets.egress_mac = mac2
|
121
|
+
secrets.ingress_mac = mac1
|
122
|
+
end
|
123
|
+
secrets
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def dh_compute_key(private_key, public_key)
|
129
|
+
private_key.ec_key.dh_compute_key(public_key.ec_key.public_key)
|
130
|
+
end
|
131
|
+
|
132
|
+
def xor(b1, b2)
|
133
|
+
b1.each_byte.with_index.map {|b, i| b ^ b2[i].ord}.pack('c*')
|
134
|
+
end
|
135
|
+
|
136
|
+
def random_nonce(size)
|
137
|
+
size.times.map {rand(8)}.pack('c*')
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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 DevP2P
|
26
|
+
module RLPX
|
27
|
+
|
28
|
+
# RLPX basic error
|
29
|
+
class Error < StandardError
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2018, by Jiang Jinyang. <https://justjjy.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 'ciri/rlp/serializable'
|
26
|
+
require_relative 'error'
|
27
|
+
require_relative 'message'
|
28
|
+
|
29
|
+
require 'snappy'
|
30
|
+
|
31
|
+
module Ciri
|
32
|
+
module DevP2P
|
33
|
+
module RLPX
|
34
|
+
|
35
|
+
class FrameIO
|
36
|
+
|
37
|
+
# max message size, took 3 byte to store message size, equal to uint24 max size
|
38
|
+
MAX_MESSAGE_SIZE = (1 << 24) - 1
|
39
|
+
|
40
|
+
class Error < RLPX::Error
|
41
|
+
end
|
42
|
+
|
43
|
+
class OverflowError < Error
|
44
|
+
end
|
45
|
+
|
46
|
+
class InvalidError < Error
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_accessor :snappy
|
50
|
+
|
51
|
+
def initialize(io, secrets)
|
52
|
+
@io = io
|
53
|
+
@secrets = secrets
|
54
|
+
@snappy = false # snappy compress
|
55
|
+
|
56
|
+
mac_aes_version = secrets.mac.size * 8
|
57
|
+
@mac = OpenSSL::Cipher.new("AES#{mac_aes_version}")
|
58
|
+
@mac.encrypt
|
59
|
+
@mac.key = secrets.mac
|
60
|
+
|
61
|
+
# init encrypt/decrypt
|
62
|
+
aes_version = secrets.aes.size * 8
|
63
|
+
@encrypt = OpenSSL::Cipher::AES.new(aes_version, :CTR)
|
64
|
+
@decrypt = OpenSSL::Cipher::AES.new(aes_version, :CTR)
|
65
|
+
zero_iv = "\x00".b * @encrypt.iv_len
|
66
|
+
@encrypt.iv = zero_iv
|
67
|
+
@encrypt.key = secrets.aes
|
68
|
+
@decrypt.iv = zero_iv
|
69
|
+
@decrypt.key = secrets.aes
|
70
|
+
end
|
71
|
+
|
72
|
+
def send_data(code, data)
|
73
|
+
msg = Message.new(code: code, size: data.size, payload: data)
|
74
|
+
write_msg(msg)
|
75
|
+
end
|
76
|
+
|
77
|
+
def write_msg(msg)
|
78
|
+
pkg_type = RLP.encode_with_type msg.code, Integer, zero: "\x00"
|
79
|
+
|
80
|
+
# use snappy compress if enable
|
81
|
+
if snappy
|
82
|
+
if msg.size > MAX_MESSAGE_SIZE
|
83
|
+
raise OverflowError.new("Message size is overflow, msg size: #{msg.size}")
|
84
|
+
end
|
85
|
+
msg.payload = Snappy.deflate(msg.payload)
|
86
|
+
msg.size = msg.payload.size
|
87
|
+
end
|
88
|
+
|
89
|
+
# write header
|
90
|
+
head_buf = "\x00".b * 32
|
91
|
+
|
92
|
+
frame_size = pkg_type.size + msg.size
|
93
|
+
if frame_size > MAX_MESSAGE_SIZE
|
94
|
+
raise OverflowError.new("Message size is overflow, frame size: #{frame_size}")
|
95
|
+
end
|
96
|
+
|
97
|
+
write_frame_size(head_buf, frame_size)
|
98
|
+
|
99
|
+
# Can't find related RFC or RLPX Spec, below code is copy from geth
|
100
|
+
# write zero header, but I can't find spec or explanations of 'zero header'
|
101
|
+
head_buf[3..5] = [0xC2, 0x80, 0x80].pack('c*')
|
102
|
+
# encrypt first half
|
103
|
+
head_buf[0...16] = @encrypt.update(head_buf[0...16]) + @encrypt.final
|
104
|
+
# write header mac
|
105
|
+
head_buf[16...32] = update_mac(@secrets.egress_mac, head_buf[0...16])
|
106
|
+
@io.write head_buf
|
107
|
+
# write encrypt frame
|
108
|
+
write_frame(pkg_type)
|
109
|
+
write_frame(msg.payload)
|
110
|
+
# pad to n*16 bytes
|
111
|
+
if (need_padding = frame_size % 16) > 0
|
112
|
+
write_frame("\x00".b * (16 - need_padding))
|
113
|
+
end
|
114
|
+
finish_write_frame
|
115
|
+
end
|
116
|
+
|
117
|
+
def read_msg
|
118
|
+
# verify header mac
|
119
|
+
head_buf = read(32)
|
120
|
+
verify_mac = update_mac(@secrets.ingress_mac, head_buf[0...16])
|
121
|
+
unless Ciri::Utils.secret_compare(verify_mac, head_buf[16...32])
|
122
|
+
raise InvalidError.new('bad header mac')
|
123
|
+
end
|
124
|
+
|
125
|
+
# decrypt header
|
126
|
+
head_buf[0...16] = @decrypt.update(head_buf[0...16]) + @decrypt.final
|
127
|
+
|
128
|
+
# read frame
|
129
|
+
frame_size = read_frame_size head_buf
|
130
|
+
# frame size should padded to n*16 bytes
|
131
|
+
need_padding = frame_size % 16
|
132
|
+
padded_frame_size = need_padding > 0 ? frame_size + (16 - need_padding) : frame_size
|
133
|
+
frame_buf = read(padded_frame_size)
|
134
|
+
|
135
|
+
# verify frame mac
|
136
|
+
@secrets.ingress_mac.update(frame_buf)
|
137
|
+
frame_digest = @secrets.ingress_mac.digest
|
138
|
+
verify_mac = update_mac(@secrets.ingress_mac, frame_digest)
|
139
|
+
# clear head_buf 16...32 bytes(header mac), since we will not need it
|
140
|
+
frame_mac = head_buf[16...32] = read(16)
|
141
|
+
unless Ciri::Utils.secret_compare(verify_mac, frame_mac)
|
142
|
+
raise InvalidError.new('bad frame mac')
|
143
|
+
end
|
144
|
+
|
145
|
+
# decrypt frame
|
146
|
+
frame_content = @decrypt.update(frame_buf) + @decrypt.final
|
147
|
+
frame_content = frame_content[0...frame_size]
|
148
|
+
msg_code = RLP.decode_with_type frame_content[0], Integer
|
149
|
+
msg = Message.new(code: msg_code, size: frame_content.size - 1, payload: frame_content[1..-1])
|
150
|
+
|
151
|
+
# snappy decompress if enable
|
152
|
+
if snappy
|
153
|
+
msg.payload = Snappy.inflate(msg.payload)
|
154
|
+
msg.size = msg.payload.size
|
155
|
+
end
|
156
|
+
|
157
|
+
msg
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
def read(length)
|
162
|
+
if (buf = @io.read(length)).nil?
|
163
|
+
@io.close
|
164
|
+
raise EOFError.new('read EOF, connection closed')
|
165
|
+
end
|
166
|
+
buf
|
167
|
+
end
|
168
|
+
|
169
|
+
def write_frame_size(buf, frame_size)
|
170
|
+
# frame-size: 3-byte integer size of frame, big endian encoded (excludes padding)
|
171
|
+
bytes_of_frame_size = [
|
172
|
+
frame_size >> 16,
|
173
|
+
frame_size >> 8,
|
174
|
+
frame_size % 256
|
175
|
+
]
|
176
|
+
buf[0..2] = bytes_of_frame_size.pack('c*')
|
177
|
+
end
|
178
|
+
|
179
|
+
def read_frame_size(buf)
|
180
|
+
size_bytes = buf[0..2].each_byte.map(&:ord)
|
181
|
+
(size_bytes[0] << 16) + (size_bytes[1] << 8) + (size_bytes[2])
|
182
|
+
end
|
183
|
+
|
184
|
+
def update_mac(mac, seed)
|
185
|
+
# reset mac each time
|
186
|
+
@mac.reset
|
187
|
+
aes_buf = (@mac.update(mac.digest) + @mac.final)[0...@mac.block_size]
|
188
|
+
aes_buf = aes_buf.each_byte.with_index.map {|b, i| b ^ seed[i].ord}.pack('c*')
|
189
|
+
mac.update(aes_buf)
|
190
|
+
# return first 16 byte
|
191
|
+
mac.digest[0...16]
|
192
|
+
end
|
193
|
+
|
194
|
+
# write encrypt content to @io, and update @secrets.egress_mac
|
195
|
+
def write_frame(string_or_io)
|
196
|
+
if string_or_io.is_a?(IO)
|
197
|
+
while (s = string_or_io.read(4096))
|
198
|
+
write_frame_string(s)
|
199
|
+
end
|
200
|
+
else
|
201
|
+
write_frame_string(string_or_io)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def write_frame_string(s)
|
206
|
+
encrypt_content = @encrypt.update(s) + @encrypt.final
|
207
|
+
# update egress_mac
|
208
|
+
@secrets.egress_mac.update encrypt_content
|
209
|
+
@io.write encrypt_content
|
210
|
+
end
|
211
|
+
|
212
|
+
def finish_write_frame
|
213
|
+
# get frame digest
|
214
|
+
frame_digest = @secrets.egress_mac.digest
|
215
|
+
@io.write update_mac(@secrets.egress_mac, frame_digest)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|