ciri 0.0.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.
@@ -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