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,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
|