ciri 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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