devp2p 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,82 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
class Command
|
6
|
+
|
7
|
+
extend Configurable
|
8
|
+
add_config(
|
9
|
+
cmd_id: 0,
|
10
|
+
structure: {}, # {arg_name: RLP::Sedes.type}
|
11
|
+
decode_strict: true
|
12
|
+
)
|
13
|
+
|
14
|
+
class <<self
|
15
|
+
def encode_payload(data)
|
16
|
+
if data.is_a?(Hash)
|
17
|
+
raise ArgumentError, 'structure must be hash of arg names and sedes' unless structure.instance_of?(Hash)
|
18
|
+
data = structure.keys.map {|k| data[k] }
|
19
|
+
end
|
20
|
+
|
21
|
+
case structure
|
22
|
+
when RLP::Sedes::CountableList
|
23
|
+
RLP.encode data, structure
|
24
|
+
when Hash
|
25
|
+
raise ArgumentError, 'structure and data length mismatch' unless data.size == structure.size
|
26
|
+
RLP.encode data, sedes: RLP::Sedes::List.new(elements: sedes)
|
27
|
+
else
|
28
|
+
raise InvalidCommandStructure
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def decode_payload(rlp_data)
|
33
|
+
case structure
|
34
|
+
when RLP::Sedes::CountableList
|
35
|
+
decoder = structure
|
36
|
+
when Hash
|
37
|
+
decoder = RLP::Sedes::List.new(elements: sedes, strict: decode_strict)
|
38
|
+
else
|
39
|
+
raise InvalidCommandStructure
|
40
|
+
end
|
41
|
+
|
42
|
+
data = RLP.decode rlp_data, sedes: decoder
|
43
|
+
data = structure.keys.zip(data).to_h if structure.is_a?(Hash)
|
44
|
+
data
|
45
|
+
rescue
|
46
|
+
puts "error in decode: #{$!}"
|
47
|
+
puts "rlp:"
|
48
|
+
puts RLP.decode(rlp_data)
|
49
|
+
raise $!
|
50
|
+
end
|
51
|
+
|
52
|
+
def sedes
|
53
|
+
@sedes ||= structure.values
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr :receive_callbacks
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
raise InvalidCommandStructure unless [Hash, RLP::Sedes::CountableList].any? {|c| structure.is_a?(c) }
|
61
|
+
@receive_callbacks = []
|
62
|
+
end
|
63
|
+
|
64
|
+
# optionally implement create
|
65
|
+
def create(proto, *args)
|
66
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
67
|
+
raise ArgumentError, "proto #{proto} must be protocol" unless proto.is_a?(BaseProtocol)
|
68
|
+
raise ArgumentError, "command structure mismatch" if !options.empty? && structure.instance_of?(RLP::Sedes::CountableList)
|
69
|
+
options.empty? ? args : options
|
70
|
+
end
|
71
|
+
|
72
|
+
# optionally implement receive
|
73
|
+
def receive(proto, data)
|
74
|
+
if structure.instance_of?(RLP::Sedes::CountableList)
|
75
|
+
receive_callbacks.each {|cb| cb.call(proto, data) }
|
76
|
+
else
|
77
|
+
receive_callbacks.each {|cb| cb.call(proto, **data) }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Configurable
|
5
|
+
|
6
|
+
def add_config(configs)
|
7
|
+
raise ArgumentError, 'self must be a class' unless self.class == Class
|
8
|
+
|
9
|
+
configs.each do |name, default|
|
10
|
+
singleton_class.send(:define_method, name) do |*args|
|
11
|
+
iv = "@#{name}"
|
12
|
+
if args.empty?
|
13
|
+
if instance_variable_defined?(iv)
|
14
|
+
instance_variable_get(iv)
|
15
|
+
else
|
16
|
+
v = superclass.respond_to?(:add_config) && superclass.respond_to?(name) ?
|
17
|
+
superclass.public_send(name) : default
|
18
|
+
instance_variable_set(iv, v)
|
19
|
+
end
|
20
|
+
else
|
21
|
+
instance_variable_set(iv, args.first)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
define_method(name) do |*args|
|
26
|
+
self.class.public_send name, *args
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
|
5
|
+
##
|
6
|
+
# monitors the connection by sending pings and checking pongs
|
7
|
+
#
|
8
|
+
class ConnectionMonitor
|
9
|
+
include Celluloid
|
10
|
+
|
11
|
+
def initialize(proto)
|
12
|
+
@proto = proto
|
13
|
+
|
14
|
+
logger.debug "init"
|
15
|
+
raise ArgumentError, 'protocol must be P2PProtocol' unless proto.is_a?(P2PProtocol)
|
16
|
+
|
17
|
+
@samples = []
|
18
|
+
@last_response = @last_request = Time.now
|
19
|
+
|
20
|
+
@ping_interval = 15
|
21
|
+
@response_delay_threshold = 120
|
22
|
+
@max_samples = 1000
|
23
|
+
|
24
|
+
track_response = ->(proto, **data) {
|
25
|
+
@last_response = Time.now
|
26
|
+
@samples.unshift(@last_response - @last_request)
|
27
|
+
@samples.pop if @samples.size > @max_samples
|
28
|
+
}
|
29
|
+
@proto.receive_pong_callbacks.push(track_response)
|
30
|
+
|
31
|
+
monitor = Actor.current
|
32
|
+
@proto.receive_hello_callbacks.push(->(p, **kwargs) { monitor.start })
|
33
|
+
end
|
34
|
+
|
35
|
+
def latency(num_samples=@max_samples)
|
36
|
+
num_samples = [num_samples, @samples.size].min
|
37
|
+
return 1 unless num_samples > 0
|
38
|
+
(0...num_samples).map {|i| @samples[i] }.reduce(0, &:+)
|
39
|
+
end
|
40
|
+
|
41
|
+
def run
|
42
|
+
logger.debug 'started', monitor: Actor.current
|
43
|
+
loop do
|
44
|
+
logger.debug 'pinging', monitor: Actor.current
|
45
|
+
@proto.send_ping
|
46
|
+
|
47
|
+
now = @last_request = Time.now
|
48
|
+
sleep @ping_interval
|
49
|
+
logger.debug('latency', peer: @proto, latency: ("%.3f" % latency))
|
50
|
+
|
51
|
+
if now - @last_response > @response_delay_threshold
|
52
|
+
logger.debug "unresponsive_peer", monitor: Actor.current
|
53
|
+
@proto.peer.report_error 'not responding to ping'
|
54
|
+
@proto.stop
|
55
|
+
terminate
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def start
|
61
|
+
async.run
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop
|
65
|
+
logger.debug 'stopped', monitor: Actor.current
|
66
|
+
terminate
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def logger
|
72
|
+
@logger ||= Logger.new("#{@proto.peer.config[:p2p][:listen_port]}.p2p.ctxmonitor.#{object_id}")
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module DEVp2p
|
2
|
+
module Control
|
3
|
+
|
4
|
+
def initialize_control
|
5
|
+
@stopped = false
|
6
|
+
@killed = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
_run
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
@stopped = false
|
15
|
+
async.run unless killed?
|
16
|
+
end
|
17
|
+
|
18
|
+
def stop
|
19
|
+
@stopped = true
|
20
|
+
@killed = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def stopped?
|
24
|
+
@stopped
|
25
|
+
end
|
26
|
+
|
27
|
+
def killed?
|
28
|
+
@killed
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
require 'secp256k1' # bitcoin-secp256k1
|
4
|
+
require 'digest/sha3'
|
5
|
+
|
6
|
+
require 'devp2p/crypto/ecies'
|
7
|
+
require 'devp2p/crypto/ecc_x'
|
8
|
+
|
9
|
+
module DEVp2p
|
10
|
+
module Crypto
|
11
|
+
|
12
|
+
extend self
|
13
|
+
|
14
|
+
def mk_privkey(seed)
|
15
|
+
Crypto.keccak256 seed
|
16
|
+
end
|
17
|
+
|
18
|
+
def privtopub(privkey)
|
19
|
+
priv = Secp256k1::PrivateKey.new privkey: privkey, raw: true
|
20
|
+
|
21
|
+
pub = priv.pubkey.serialize(compressed: false)
|
22
|
+
raise InvalidKeyError, 'invalid pubkey' unless pub.size == 65 && pub[0] == "\x04"
|
23
|
+
|
24
|
+
pub[1,64]
|
25
|
+
end
|
26
|
+
|
27
|
+
def keccak256(x)
|
28
|
+
Digest::SHA3.new(256).digest(x)
|
29
|
+
end
|
30
|
+
|
31
|
+
def hmac_sha256(key, msg)
|
32
|
+
OpenSSL::HMAC.digest 'sha256', key, msg
|
33
|
+
end
|
34
|
+
|
35
|
+
def ecdsa_sign(msghash, privkey)
|
36
|
+
raise ArgumentError, 'msghash length must be 32' unless msghash.size == 32
|
37
|
+
|
38
|
+
priv = Secp256k1::PrivateKey.new privkey: privkey, raw: true
|
39
|
+
sig = priv.ecdsa_recoverable_serialize priv.ecdsa_sign_recoverable(msghash, raw: true)
|
40
|
+
"#{sig[0]}#{sig[1].chr}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def ecdsa_recover(msghash, sig)
|
44
|
+
raise ArgumentError, 'msghash length must be 32' unless msghash.size == 32
|
45
|
+
raise ArgumentError, 'signature length must be 65' unless sig.size == 65
|
46
|
+
|
47
|
+
pub = Secp256k1::PublicKey.new flags: Secp256k1::ALL_FLAGS
|
48
|
+
recsig = pub.ecdsa_recoverable_deserialize sig[0,64], sig[64].ord
|
49
|
+
pub.public_key = pub.ecdsa_recover msghash, recsig, raw: true
|
50
|
+
pub.serialize(compressed: false)[1..-1]
|
51
|
+
end
|
52
|
+
|
53
|
+
def ecdsa_verify(pubkey, sig, msg)
|
54
|
+
raise ArgumentError, 'invalid signature length' unless sig.size == 65
|
55
|
+
raise ArgumentError, 'invalid pubkey length' unless pubkey.size == 64
|
56
|
+
|
57
|
+
pub = Secp256k1::PublicKey.new pubkey: "\x04#{pubkey}", raw: true
|
58
|
+
raw_sig = pub.ecdsa_recoverable_convert pub.ecdsa_recoverable_deserialize(sig[0,64], sig[64].ord)
|
59
|
+
|
60
|
+
pub.ecdsa_verify msg, raw_sig, raw: true
|
61
|
+
end
|
62
|
+
alias verify ecdsa_verify
|
63
|
+
|
64
|
+
##
|
65
|
+
# Encrypt data with ECIES method using the public key of the recipient.
|
66
|
+
#
|
67
|
+
def encrypt(data, raw_pubkey)
|
68
|
+
raise ArgumentError, "invalid pubkey of length #{raw_pubkey.size}" unless raw_pubkey.size == 64
|
69
|
+
Crypto::ECIES.encrypt data, raw_pubkey
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module DEVp2p
|
6
|
+
module Crypto
|
7
|
+
class ECCx
|
8
|
+
|
9
|
+
CURVE = 'secp256k1'.freeze
|
10
|
+
|
11
|
+
class <<self
|
12
|
+
def valid_key?(raw_pubkey, raw_privkey=nil)
|
13
|
+
return false unless raw_pubkey.size == 64
|
14
|
+
|
15
|
+
group = OpenSSL::PKey::EC::Group.new CURVE
|
16
|
+
bn = OpenSSL::BN.new Utils.encode_hex("\x04#{raw_pubkey}"), 16
|
17
|
+
point = OpenSSL::PKey::EC::Point.new group, bn
|
18
|
+
|
19
|
+
key = OpenSSL::PKey::EC.new(CURVE)
|
20
|
+
key.public_key = point
|
21
|
+
key.private_key = OpenSSL::BN.new Utils.big_endian_to_int(raw_privkey) if raw_privkey
|
22
|
+
key.check_key
|
23
|
+
|
24
|
+
true
|
25
|
+
rescue
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_key
|
30
|
+
curve = OpenSSL::PKey::EC.new(CURVE)
|
31
|
+
curve.generate_key
|
32
|
+
|
33
|
+
raw_privkey = Utils.zpad Utils.int_to_big_endian(curve.private_key.to_i), 32
|
34
|
+
raw_pubkey = Utils.int_to_big_endian(curve.public_key.to_bn.to_i)
|
35
|
+
raise InvalidKeyError, 'invalid pubkey' unless raw_pubkey.size == 65 && raw_pubkey[0] == "\x04"
|
36
|
+
|
37
|
+
[raw_privkey, raw_pubkey[1,64]]
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Compute public key with the local private key and returns a 256bits
|
42
|
+
# shared key
|
43
|
+
#
|
44
|
+
def get_ecdh_key(curve, raw_pubkey)
|
45
|
+
pubkey = raw_pubkey_to_openssl_pubkey raw_pubkey
|
46
|
+
curve.dh_compute_key pubkey
|
47
|
+
end
|
48
|
+
|
49
|
+
def raw_pubkey_to_openssl_pubkey(raw_pubkey)
|
50
|
+
return unless raw_pubkey
|
51
|
+
|
52
|
+
bn = OpenSSL::BN.new Utils.encode_hex("\x04#{raw_pubkey}"), 16
|
53
|
+
group = OpenSSL::PKey::EC::Group.new CURVE
|
54
|
+
OpenSSL::PKey::EC::Point.new group, bn
|
55
|
+
end
|
56
|
+
|
57
|
+
def raw_privkey_to_openssl_privkey(raw_privkey)
|
58
|
+
return unless raw_privkey
|
59
|
+
|
60
|
+
OpenSSL::BN.new Utils.big_endian_to_int(raw_privkey)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr :raw_pubkey
|
65
|
+
|
66
|
+
def initialize(raw_privkey=nil, raw_pubkey=nil)
|
67
|
+
if raw_privkey && raw_pubkey
|
68
|
+
raise ArgumentError, 'must not provide pubkey with privkey'
|
69
|
+
elsif raw_privkey
|
70
|
+
raw_pubkey = Crypto.privtopub raw_privkey
|
71
|
+
elsif raw_pubkey
|
72
|
+
raise ArgumentError, 'invalid pubkey length' unless raw_pubkey.size == 64
|
73
|
+
else
|
74
|
+
raw_privkey, raw_pubkey = self.class.generate_key
|
75
|
+
end
|
76
|
+
|
77
|
+
if self.class.valid_key?(raw_pubkey, raw_privkey)
|
78
|
+
@raw_pubkey, @raw_privkey = raw_pubkey, raw_privkey
|
79
|
+
@pubkey_x, @pubkey_y = decode_pubkey raw_pubkey
|
80
|
+
set_curve
|
81
|
+
else
|
82
|
+
@raw_pubkey, @raw_privkey = nil, nil
|
83
|
+
@pubkey_x, @pubkey_y = nil, nil
|
84
|
+
raise InvalidKeyError, "bad ECC keys"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def sign(data)
|
89
|
+
sig = Crypto.ecdsa_sign data, @raw_privkey
|
90
|
+
raise InvalidSignatureError unless sig.size == 65
|
91
|
+
sig
|
92
|
+
end
|
93
|
+
|
94
|
+
def verify(sig, msg)
|
95
|
+
raise ArgumentError, 'invalid signature length' unless sig.size == 65
|
96
|
+
Crypto.ecdsa_verify @raw_pubkey, sig, msg
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_ecdh_key(raw_pubkey)
|
100
|
+
self.class.get_ecdh_key curve, raw_pubkey
|
101
|
+
end
|
102
|
+
|
103
|
+
def ecies_encrypt(*args)
|
104
|
+
ECIES.encrypt *args
|
105
|
+
end
|
106
|
+
alias encrypt ecies_encrypt
|
107
|
+
|
108
|
+
def ecies_decrypt(*args)
|
109
|
+
ECIES.decrypt curve, *args
|
110
|
+
end
|
111
|
+
alias decrypt ecies_decrypt
|
112
|
+
|
113
|
+
def curve
|
114
|
+
@curve ||= OpenSSL::PKey::EC.new(CURVE)
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def decode_pubkey(raw_pubkey)
|
120
|
+
return [nil, nil] unless raw_pubkey
|
121
|
+
|
122
|
+
raise ArgumentError, 'invalid pubkey length' unless raw_pubkey.size == 64
|
123
|
+
[raw_pubkey[0,32], raw_pubkey[32,32]]
|
124
|
+
end
|
125
|
+
|
126
|
+
def set_curve
|
127
|
+
curve.public_key = self.class.raw_pubkey_to_openssl_pubkey(@raw_pubkey)
|
128
|
+
curve.private_key = self.class.raw_privkey_to_openssl_privkey(@raw_privkey)
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# -*- encoding : ascii-8bit -*-
|
2
|
+
|
3
|
+
module DEVp2p
|
4
|
+
module Crypto
|
5
|
+
class ECIES
|
6
|
+
|
7
|
+
CIPHER = 'AES-128-CTR'.freeze
|
8
|
+
CIPHER_BLOCK_SIZE = 16 # 128 / 8
|
9
|
+
|
10
|
+
ENCRYPT_OVERHEAD_LENGTH = 113
|
11
|
+
|
12
|
+
class <<self
|
13
|
+
|
14
|
+
##
|
15
|
+
# ECIES Encrypt, where P = recipient publie key, is:
|
16
|
+
#
|
17
|
+
# 1. generate r = random value
|
18
|
+
# 2. generate shared-secret = kdf( ecdhAgree(r, P) )
|
19
|
+
# 3. generate R = rG [ same op as generating a public key ]
|
20
|
+
# 4. send 0x04 || R || AsymmetricEncrypt(shared-secret, plaintext) || tag
|
21
|
+
#
|
22
|
+
def encrypt(data, remote_pubkey, shared_mac_data='')
|
23
|
+
# 1. generate r = random value
|
24
|
+
ephem = ECCx.new
|
25
|
+
|
26
|
+
# 2. generate shared-secret = kdf( ecdhAgree(r, P) )
|
27
|
+
key_material = ephem.get_ecdh_key remote_pubkey
|
28
|
+
raise InvalidKeyError unless key_material.size == 32
|
29
|
+
|
30
|
+
key = kdf key_material, 32
|
31
|
+
raise InvalidKeyError unless key.size == 32
|
32
|
+
key_enc, key_mac = key[0,16], key[16,16]
|
33
|
+
|
34
|
+
key_mac = Digest::SHA256.digest(key_mac)
|
35
|
+
raise InvalidKeyError unless key_mac.size == 32
|
36
|
+
|
37
|
+
# 3. generate R = rG
|
38
|
+
ephem_pubkey = ephem.raw_pubkey
|
39
|
+
|
40
|
+
ctx = OpenSSL::Cipher.new(CIPHER)
|
41
|
+
ctx.encrypt
|
42
|
+
ctx.key = key_enc
|
43
|
+
iv = ctx.random_iv
|
44
|
+
ctx.iv = iv
|
45
|
+
|
46
|
+
ciphertext = ctx.update(data) + ctx.final
|
47
|
+
raise EncryptionError unless ciphertext.size == data.size
|
48
|
+
|
49
|
+
# 4. send 0x04 || R || AsymmetricEncrypt(shared-secret, plaintext) || tag
|
50
|
+
tag = Crypto.hmac_sha256 key_mac, "#{iv}#{ciphertext}#{shared_mac_data}"
|
51
|
+
raise InvalidMACError unless tag.size == 32
|
52
|
+
msg = "\x04#{ephem_pubkey}#{iv}#{ciphertext}#{tag}"
|
53
|
+
|
54
|
+
raise EncryptionError unless msg.size == ENCRYPT_OVERHEAD_LENGTH + data.size
|
55
|
+
msg
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Decrypt data with ECIES method using the local private key
|
60
|
+
#
|
61
|
+
# ECIES Decrypt (performed by recipient):
|
62
|
+
#
|
63
|
+
# 1. generate shared-secret = kdf( ecdhAgree(myPrivKey, msg[1,64]) )
|
64
|
+
# 2. verify tag
|
65
|
+
# 3. decrypt
|
66
|
+
#
|
67
|
+
# ecdhAgree(r, recipientPublic) == ecdhAgree(recipientPrivate, R)
|
68
|
+
# where R = r*G, recipientPublic = recipientPrivate * G
|
69
|
+
#
|
70
|
+
def decrypt(curve, data, shared_mac_data='')
|
71
|
+
raise DecryptionError, 'wrong ecies header' unless data[0] == "\x04"
|
72
|
+
|
73
|
+
# 1. generate shared-secret = kdf( ecdhAgree(myPrivKey, msg[1,64]) )
|
74
|
+
shared = data[1,64] # ephem_pubkey
|
75
|
+
raise DecryptionError, 'invalid shared secret' unless ECCx.valid_key?(shared)
|
76
|
+
|
77
|
+
key_material = ECCx.get_ecdh_key curve, shared
|
78
|
+
raise InvalidKeyError unless key_material.size == 32
|
79
|
+
|
80
|
+
key = kdf key_material, 32
|
81
|
+
raise InvalidKeyError unless key.size == 32
|
82
|
+
key_enc, key_mac = key[0,16], key[16,16]
|
83
|
+
|
84
|
+
key_mac = Digest::SHA256.digest(key_mac)
|
85
|
+
raise InvalidKeyError unless key_mac.size == 32
|
86
|
+
|
87
|
+
tag = data[-32..-1]
|
88
|
+
raise InvalidMACError unless tag.size == 32
|
89
|
+
|
90
|
+
# 2. verify tag
|
91
|
+
raise DecryptionError, 'Fail to verify data' unless Crypto.hmac_sha256(key_mac, "#{data[65...-32]}#{shared_mac_data}") == tag
|
92
|
+
|
93
|
+
# 3. decrypt
|
94
|
+
iv = data[65,CIPHER_BLOCK_SIZE]
|
95
|
+
ciphertext = data[(65+CIPHER_BLOCK_SIZE)...-32]
|
96
|
+
raise DecryptionError unless 1 + shared.size + iv.size + ciphertext.size + tag.size == data.size
|
97
|
+
|
98
|
+
ctx = OpenSSL::Cipher.new CIPHER
|
99
|
+
ctx.decrypt
|
100
|
+
ctx.key = key_enc
|
101
|
+
ctx.iv = iv
|
102
|
+
|
103
|
+
ctx.update(ciphertext) + ctx.final
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# interop w/go ecies implementation
|
108
|
+
#
|
109
|
+
# for sha3, blocksize is 136 bytes
|
110
|
+
# for sha256, blocksize is 64 bytes
|
111
|
+
#
|
112
|
+
# NIST SP 800-56a Concatenation Key Derivation Function (section 5.8.1)
|
113
|
+
#
|
114
|
+
def kdf(key_material, key_len)
|
115
|
+
s1 = ""
|
116
|
+
key = ""
|
117
|
+
hash_blocksize = 64
|
118
|
+
reps = ((key_len + 7) * 8) / (hash_blocksize * 8)
|
119
|
+
counter = 0
|
120
|
+
while counter <= reps
|
121
|
+
counter += 1
|
122
|
+
ctx = Digest::SHA256.new
|
123
|
+
ctx.update [counter].pack('I>')
|
124
|
+
ctx.update key_material
|
125
|
+
ctx.update s1
|
126
|
+
key += ctx.digest
|
127
|
+
end
|
128
|
+
key[0,key_len]
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|