block_io 1.2.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.appveyor.yml +26 -0
- data/.gitignore +1 -1
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/README.md +13 -7
- data/block_io.gemspec +9 -7
- data/examples/basic.rb +10 -12
- data/examples/dtrust.rb +36 -38
- data/examples/max_withdrawal.rb +29 -0
- data/examples/proxy.rb +36 -0
- data/examples/sweeper.rb +11 -6
- data/lib/block_io.rb +15 -411
- data/lib/block_io/client.rb +179 -0
- data/lib/block_io/constants.rb +10 -0
- data/lib/block_io/helper.rb +164 -0
- data/lib/block_io/key.rb +151 -0
- data/lib/block_io/version.rb +1 -1
- data/spec/client_spec.rb +223 -0
- data/spec/data/sign_and_finalize_dtrust_withdrawal_request.json +1 -0
- data/spec/data/sign_and_finalize_sweep_request.json +1 -0
- data/spec/data/sign_and_finalize_withdrawal_request.json +4 -0
- data/spec/data/sweep_from_address_response.json +1 -0
- data/spec/data/withdraw_from_dtrust_address_response.json +1 -0
- data/spec/data/withdraw_response.json +1227 -0
- data/spec/helper_spec.rb +44 -0
- data/spec/key_spec.rb +61 -0
- data/spec/rfc6979_spec.rb +59 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/withdraw_spec.rb +90 -0
- metadata +119 -35
- data/examples/change.rb +0 -117
@@ -0,0 +1,179 @@
|
|
1
|
+
module BlockIo
|
2
|
+
|
3
|
+
class Client
|
4
|
+
|
5
|
+
attr_reader :api_key, :version, :network
|
6
|
+
|
7
|
+
def initialize(args = {})
|
8
|
+
# api_key
|
9
|
+
# pin
|
10
|
+
# version
|
11
|
+
# hostname
|
12
|
+
# proxy
|
13
|
+
# pool_size
|
14
|
+
# keys
|
15
|
+
|
16
|
+
raise "Must provide an API Key." unless args.key?(:api_key) and args[:api_key].to_s.size > 0
|
17
|
+
|
18
|
+
@api_key = args[:api_key]
|
19
|
+
@encryption_key = Helper.pinToAesKey(args[:pin] || "") if args.key?(:pin)
|
20
|
+
@version = args[:version] || 2
|
21
|
+
@hostname = args[:hostname] || "block.io"
|
22
|
+
@proxy = args[:proxy] || {}
|
23
|
+
@keys = args[:keys] || []
|
24
|
+
@use_low_r = args[:use_low_r]
|
25
|
+
@raise_exception_on_error = args[:raise_exception_on_error] || false
|
26
|
+
|
27
|
+
raise Exception.new("Keys must be provided as an array.") unless @keys.is_a?(Array)
|
28
|
+
raise Exception.new("Keys must be BlockIo::Key objects.") unless @keys.all?{|key| key.is_a?(BlockIo::Key)}
|
29
|
+
|
30
|
+
# make a hash of the keys we've been given
|
31
|
+
@keys = @keys.inject({}){|h,v| h[v.public_key] = v; h}
|
32
|
+
|
33
|
+
raise Exception.new("Must specify hostname, port, username, password if using a proxy.") if @proxy.keys.size > 0 and [:hostname, :port, :username, :password].any?{|x| !@proxy.key?(x)}
|
34
|
+
|
35
|
+
@conn = ConnectionPool.new(:size => args[:pool_size] || 5) { http = HTTP.headers(:accept => "application/json", :user_agent => "gem:block_io:#{VERSION}");
|
36
|
+
http = http.via(args.dig(:proxy, :hostname), args.dig(:proxy, :port), args.dig(:proxy, :username), args.dig(:proxy, :password)) if @proxy.key?(:hostname);
|
37
|
+
http = http.persistent("https://#{@hostname}");
|
38
|
+
http }
|
39
|
+
|
40
|
+
# this will get populated after a successful API call
|
41
|
+
@network = nil
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(m, *args)
|
46
|
+
|
47
|
+
method_name = m.to_s
|
48
|
+
|
49
|
+
raise Exception.new("Must provide arguments as a Hash.") unless args.size <= 1 and args.all?{|x| x.is_a?(Hash)}
|
50
|
+
raise Exception.new("Parameter keys must be symbols. For instance: :label => 'default' instead of 'label' => 'default'") unless args[0].nil? or args[0].keys.all?{|x| x.is_a?(Symbol)}
|
51
|
+
raise Exception.new("Cannot pass PINs to any calls. PINs can only be set when initiating this library.") if !args[0].nil? and args[0].key?(:pin)
|
52
|
+
raise Exception.new("Do not specify API Keys here. Initiate a new BlockIo object instead if you need to use another API Key.") if !args[0].nil? and args[0].key?(:api_key)
|
53
|
+
|
54
|
+
if BlockIo::WITHDRAW_METHODS.key?(method_name) then
|
55
|
+
# it's a withdrawal call
|
56
|
+
withdraw(args[0], method_name)
|
57
|
+
elsif BlockIo::SWEEP_METHODS.key?(method_name) then
|
58
|
+
# we're sweeping from an address
|
59
|
+
sweep(args[0], method_name)
|
60
|
+
elsif BlockIo::FINALIZE_SIGNATURE_METHODS.key?(method_name) then
|
61
|
+
# we're finalize the transaction signatures
|
62
|
+
finalize_signature(args[0], method_name)
|
63
|
+
else
|
64
|
+
api_call({:method_name => method_name, :params => args[0] || {}})
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def withdraw(args = {}, method_name = "withdraw")
|
72
|
+
|
73
|
+
response = api_call({:method_name => method_name, :params => args})
|
74
|
+
|
75
|
+
if response["data"].key?("reference_id") then
|
76
|
+
# Block.io's asking us to provide client-side signatures
|
77
|
+
|
78
|
+
encrypted_passphrase = response["data"]["encrypted_passphrase"]
|
79
|
+
|
80
|
+
if !encrypted_passphrase.nil? and !@keys.key?(encrypted_passphrase["signer_public_key"]) then
|
81
|
+
# encrypted passphrase was provided, and we do not have the signer's key, so let's extract it first
|
82
|
+
|
83
|
+
raise Exception.new("PIN not set and no keys provided. Cannot execute withdrawal requests.") unless @encryption_key or @keys.size > 0
|
84
|
+
|
85
|
+
key = Helper.extractKey(encrypted_passphrase["passphrase"], @encryption_key, @use_low_r)
|
86
|
+
raise Exception.new("Public key mismatch for requested signer and ourselves. Invalid Secret PIN detected.") unless key.public_key.eql?(encrypted_passphrase["signer_public_key"])
|
87
|
+
|
88
|
+
# store this key for later use
|
89
|
+
@keys[key.public_key] = key
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
if @keys.size > 0 then
|
94
|
+
# if we have at least one key available, try to send signatures back
|
95
|
+
# if a dtrust withdrawal is used without any keys stored in the BlockIo::Client object, the output of this call will be the previous response from Block.io
|
96
|
+
|
97
|
+
# we just need reference_id and inputs
|
98
|
+
response["data"] = {"reference_id" => response["data"]["reference_id"], "inputs" => response["data"]["inputs"]}
|
99
|
+
|
100
|
+
# let's sign all the inputs we can
|
101
|
+
signatures_added = (@keys.size == 0 ? false : Helper.signData(response["data"]["inputs"], @keys))
|
102
|
+
|
103
|
+
# the response object is now signed, let's stringify it and finalize this withdrawal
|
104
|
+
response = finalize_signature({:signature_data => response["data"]}, "sign_and_finalize_withdrawal") if signatures_added
|
105
|
+
|
106
|
+
# if we provided all the required signatures, this transaction went through
|
107
|
+
# otherwise Block.io responded with data asking for more signatures and recorded the signature we provided above
|
108
|
+
# the latter will be the case for dTrust addresses
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
response
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
def sweep(args = {}, method_name = "sweep_from_address")
|
118
|
+
# sweep coins from a given address and key
|
119
|
+
|
120
|
+
raise Exception.new("No private_key provided.") unless args.key?(:private_key) and (args[:private_key] || "").size > 0
|
121
|
+
|
122
|
+
key = Key.from_wif(args[:private_key], @use_low_r)
|
123
|
+
sanitized_args = args.merge({:public_key => key.public_key})
|
124
|
+
sanitized_args.delete(:private_key)
|
125
|
+
|
126
|
+
response = api_call({:method_name => method_name, :params => sanitized_args})
|
127
|
+
|
128
|
+
if response["data"].key?("reference_id") then
|
129
|
+
# Block.io's asking us to provide client-side signatures
|
130
|
+
|
131
|
+
# we just need the reference_id and inputs
|
132
|
+
response["data"] = {"reference_id" => response["data"]["reference_id"], "inputs" => response["data"]["inputs"]}
|
133
|
+
|
134
|
+
# let's sign all the inputs we can
|
135
|
+
signatures_added = Helper.signData(response["data"]["inputs"], [key])
|
136
|
+
|
137
|
+
# the response object is now signed, let's stringify it and finalize this transaction
|
138
|
+
response = finalize_signature({:signature_data => response["data"]}, "sign_and_finalize_sweep") if signatures_added
|
139
|
+
|
140
|
+
# if we provided all the required signatures, this transaction went through
|
141
|
+
end
|
142
|
+
|
143
|
+
response
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
def finalize_signature(args = {}, method_name = "sign_and_finalize_withdrawal")
|
148
|
+
|
149
|
+
raise Exception.new("Object must have reference_id and inputs keys.") unless args.key?(:signature_data) and args[:signature_data].key?("inputs") and args[:signature_data].key?("reference_id")
|
150
|
+
|
151
|
+
signatures = {"reference_id" => args[:signature_data]["reference_id"], "inputs" => args[:signature_data]["inputs"]}
|
152
|
+
|
153
|
+
response = api_call({:method_name => method_name, :params => {:signature_data => Oj.dump(signatures)}})
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
def api_call(args)
|
158
|
+
|
159
|
+
raise Exception.new("No connections left to perform API call. Please re-initialize BlockIo::Client with :pool_size greater than #{@conn.size}.") unless @conn.available > 0
|
160
|
+
|
161
|
+
response = @conn.with {|http| http.post("/api/v#{@version}/#{args[:method_name]}", :json => args[:params].merge({:api_key => @api_key}))}
|
162
|
+
|
163
|
+
begin
|
164
|
+
body = Oj.safe_load(response.to_s)
|
165
|
+
rescue
|
166
|
+
body = {"status" => "fail", "data" => {"error_message" => "Unknown error occurred. Please report this to support@block.io. Status #{response.code}."}}
|
167
|
+
end
|
168
|
+
|
169
|
+
raise Exception.new("#{body["data"]["error_message"]}") if !body["status"].eql?("success") and @raise_exception_on_error
|
170
|
+
|
171
|
+
@network ||= body["data"]["network"] if body["data"].key?("network")
|
172
|
+
|
173
|
+
body
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module BlockIo
|
2
|
+
|
3
|
+
WITHDRAW_METHODS = ["withdraw", "withdraw_from_address", "withdraw_from_addresses", "withdraw_from_label", "withdraw_from_labels",
|
4
|
+
"withdraw_from_dtrust_address", "withdraw_from_dtrust_addresses", "withdraw_from_dtrust_label", "withdraw_from_dtrust_labels"].inject({}){|h,v| h[v] = true; h}.freeze
|
5
|
+
|
6
|
+
SWEEP_METHODS = ["sweep_from_address"].inject({}){|h,v| h[v] = true; h}.freeze
|
7
|
+
|
8
|
+
FINALIZE_SIGNATURE_METHODS = ["sign_and_finalize_withdrawal", "sign_and_finalize_sweep"].inject({}){|h,v| h[v] = true; h}.freeze
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
module BlockIo
|
2
|
+
|
3
|
+
class Helper
|
4
|
+
|
5
|
+
def self.signData(inputs, keys)
|
6
|
+
# sign the given data with the given keys
|
7
|
+
|
8
|
+
raise Exception.new("Keys object must be a hash or array containing the appropriate keys.") unless keys.size >= 1
|
9
|
+
|
10
|
+
signatures_added = false
|
11
|
+
|
12
|
+
# create a dictionary of keys we have
|
13
|
+
# saves the next loop from being O(n^3)
|
14
|
+
hkeys = (keys.is_a?(Hash) ? keys : keys.inject({}){|h,v| h[v.public_key] = v; h})
|
15
|
+
odata = []
|
16
|
+
|
17
|
+
# saves the next loop from being O(n^2)
|
18
|
+
inputs.each{|input| odata << input["data_to_sign"]; odata << input["signatures_needed"]; odata.push(*input["signers"])}
|
19
|
+
|
20
|
+
data_to_sign = nil
|
21
|
+
signatures_needed = nil
|
22
|
+
|
23
|
+
while !(cdata = odata.shift).nil? do
|
24
|
+
# O(n)
|
25
|
+
|
26
|
+
if cdata.is_a?(String) then
|
27
|
+
# this is data to sign
|
28
|
+
|
29
|
+
# make a copy of this
|
30
|
+
data_to_sign = '' << cdata
|
31
|
+
|
32
|
+
# number of signatures needed
|
33
|
+
signatures_needed = 0 + odata.shift
|
34
|
+
|
35
|
+
else
|
36
|
+
# add signatures if necessary
|
37
|
+
# dTrust required signatures may be lower than number of keys provided
|
38
|
+
|
39
|
+
if hkeys.key?(cdata["signer_public_key"]) and signatures_needed > 0 and cdata["signed_data"].nil? then
|
40
|
+
cdata["signed_data"] = hkeys[cdata["signer_public_key"]].sign(data_to_sign)
|
41
|
+
signatures_needed -= 1
|
42
|
+
signatures_added ||= true
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
signatures_added
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.extractKey(encrypted_data, b64_enc_key, use_low_r = true)
|
53
|
+
# passphrase is in plain text
|
54
|
+
# encrypted_data is in base64, as it was stored on Block.io
|
55
|
+
# returns the private key extracted from the given encrypted data
|
56
|
+
|
57
|
+
decrypted = self.decrypt(encrypted_data, b64_enc_key)
|
58
|
+
|
59
|
+
Key.from_passphrase(decrypted, use_low_r)
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.sha256(value)
|
64
|
+
# returns the hex of the hash of the given value
|
65
|
+
OpenSSL::Digest::SHA256.digest(value).unpack("H*")[0]
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.pinToAesKey(secret_pin, iterations = 2048)
|
69
|
+
# converts the pincode string to PBKDF2
|
70
|
+
# returns a base64 version of PBKDF2 pincode
|
71
|
+
salt = ""
|
72
|
+
|
73
|
+
part1 = OpenSSL::PKCS5.pbkdf2_hmac(
|
74
|
+
secret_pin,
|
75
|
+
"",
|
76
|
+
1024,
|
77
|
+
128/8,
|
78
|
+
OpenSSL::Digest::SHA256.new
|
79
|
+
).unpack("H*")[0]
|
80
|
+
|
81
|
+
part2 = OpenSSL::PKCS5.pbkdf2_hmac(
|
82
|
+
part1,
|
83
|
+
"",
|
84
|
+
1024,
|
85
|
+
256/8,
|
86
|
+
OpenSSL::Digest::SHA256.new
|
87
|
+
) # binary
|
88
|
+
|
89
|
+
[part2].pack("m0") # the base64 encryption key
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.low_r?(r)
|
94
|
+
# https://github.com/bitcoin/bitcoin/blob/v0.20.0/src/key.cpp#L207
|
95
|
+
h = r.scan(/../)
|
96
|
+
h[3].to_i(16) == 32 and h[4].to_i(16) < 0x80
|
97
|
+
end
|
98
|
+
|
99
|
+
# Decrypts a block of data (encrypted_data) given an encryption key
|
100
|
+
def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB")
|
101
|
+
|
102
|
+
response = nil
|
103
|
+
|
104
|
+
begin
|
105
|
+
aes = OpenSSL::Cipher.new(cipher_type)
|
106
|
+
aes.decrypt
|
107
|
+
aes.key = b64_enc_key.unpack("m0")[0]
|
108
|
+
aes.iv = iv unless iv.nil?
|
109
|
+
response = aes.update(encrypted_data.unpack("m0")[0]) << aes.final
|
110
|
+
rescue Exception => e
|
111
|
+
# decryption failed, must be an invalid Secret PIN
|
112
|
+
raise Exception.new("Invalid Secret PIN provided.")
|
113
|
+
end
|
114
|
+
|
115
|
+
response
|
116
|
+
end
|
117
|
+
|
118
|
+
# Encrypts a block of data given an encryption key
|
119
|
+
def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB")
|
120
|
+
aes = OpenSSL::Cipher.new(cipher_type)
|
121
|
+
aes.encrypt
|
122
|
+
aes.key = b64_enc_key.unpack("m0")[0]
|
123
|
+
aes.iv = iv unless iv.nil?
|
124
|
+
[aes.update(data) << aes.final].pack("m0")
|
125
|
+
end
|
126
|
+
|
127
|
+
# courtesy bitcoin-ruby
|
128
|
+
|
129
|
+
def self.int_to_base58(int_val, leading_zero_bytes=0)
|
130
|
+
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
131
|
+
base58_val, base = "", alpha.size
|
132
|
+
while int_val > 0
|
133
|
+
int_val, remainder = int_val.divmod(base)
|
134
|
+
base58_val = alpha[remainder] << base58_val
|
135
|
+
end
|
136
|
+
base58_val
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.base58_to_int(base58_val)
|
140
|
+
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
141
|
+
int_val, base = 0, alpha.size
|
142
|
+
base58_val.reverse.each_char.with_index do |char,index|
|
143
|
+
raise ArgumentError, "Value not a valid Base58 String." unless char_index = alpha.index(char)
|
144
|
+
int_val += char_index*(base**index)
|
145
|
+
end
|
146
|
+
int_val
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.encode_base58(hex)
|
150
|
+
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : "").size / 2
|
151
|
+
("1"*leading_zero_bytes) << Helper.int_to_base58( hex.to_i(16) )
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.decode_base58(base58_val)
|
155
|
+
s = Helper.base58_to_int(base58_val).to_s(16)
|
156
|
+
s = (s.bytesize.odd? ? ("0" << s) : s)
|
157
|
+
s = "" if s == "00"
|
158
|
+
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
|
159
|
+
s = ("00"*leading_zero_bytes) << s if leading_zero_bytes > 0
|
160
|
+
s
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
data/lib/block_io/key.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
module BlockIo
|
2
|
+
|
3
|
+
class Key
|
4
|
+
|
5
|
+
def initialize(privkey = nil, use_low_r = true, compressed = true)
|
6
|
+
# the privkey must be in hex if at all provided
|
7
|
+
|
8
|
+
@group = ECDSA::Group::Secp256k1
|
9
|
+
@private_key = (privkey.nil? ? (1 + SecureRandom.random_number(@group.order - 1)) : privkey.to_i(16))
|
10
|
+
@public_key = @group.generator.multiply_by_scalar(@private_key)
|
11
|
+
@compressed = compressed
|
12
|
+
@use_low_r = use_low_r
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def private_key
|
17
|
+
# returns private key in hex form
|
18
|
+
@private_key.to_s(16)
|
19
|
+
end
|
20
|
+
|
21
|
+
def public_key
|
22
|
+
# returns the compressed form of the public key to save network fees (shorter scripts)
|
23
|
+
# hex form
|
24
|
+
ECDSA::Format::PointOctetString.encode(@public_key, compression: @compressed).unpack("H*")[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def sign(data)
|
28
|
+
# sign the given hexadecimal string
|
29
|
+
|
30
|
+
counter = 0
|
31
|
+
signature = nil
|
32
|
+
|
33
|
+
loop do
|
34
|
+
|
35
|
+
# first this we get K, it's without extra entropy
|
36
|
+
# second time onwards, with extra entropy
|
37
|
+
nonce = Key.deterministicGenerateK([data].pack("H*"), @private_key, counter) # RFC6979
|
38
|
+
signature = ECDSA.sign(@group, @private_key, data.to_i(16), nonce)
|
39
|
+
|
40
|
+
r, s = signature.components
|
41
|
+
|
42
|
+
# BIP0062 -- use lower S values only
|
43
|
+
over_two = @group.order >> 1 # half of what it was
|
44
|
+
s = @group.order - s if (s > over_two)
|
45
|
+
|
46
|
+
signature = ECDSA::Signature.new(r, s)
|
47
|
+
|
48
|
+
# DER encode this, and return it in hex form
|
49
|
+
signature = ECDSA::Format::SignatureDerString.encode(signature).unpack("H*")[0]
|
50
|
+
|
51
|
+
break if !@use_low_r or Helper.low_r?(signature)
|
52
|
+
|
53
|
+
counter += 1
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
signature
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_signature?(signature, data)
|
62
|
+
ECDSA.valid_signature?(@public_key, [data].pack("H*"), ECDSA::Format::SignatureDerString.decode([signature].pack("H*")))
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.from_passphrase(passphrase, use_low_r = true)
|
66
|
+
# ATTENTION: use BlockIo::Key.new to generate new private keys. Using passphrases is not recommended due to lack of / low entropy.
|
67
|
+
# create a private/public key pair from a given passphrase
|
68
|
+
# use a long, random passphrase. your security depends on the passphrase's entropy.
|
69
|
+
|
70
|
+
raise Exception.new("Must provide passphrase at least 8 characters long.") if passphrase.nil? or passphrase.length < 8
|
71
|
+
|
72
|
+
hashed_key = Helper.sha256([passphrase].pack("H*")) # must pass bytes to sha256
|
73
|
+
|
74
|
+
# modding is for backward compatibility with legacy bitcoinjs
|
75
|
+
Key.new((hashed_key.to_i(16) % ECDSA::Group::Secp256k1.order).to_s(16), use_low_r)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.from_wif(wif, use_low_r = true)
|
79
|
+
# returns a new key extracted from the Wallet Import Format provided
|
80
|
+
# TODO check against checksum
|
81
|
+
|
82
|
+
hexkey = Helper.decode_base58(wif)
|
83
|
+
actual_key = hexkey[2...66]
|
84
|
+
|
85
|
+
compressed = hexkey[2..hexkey.length].length-8 > 64 and hexkey[2..hexkey.length][64...66] == "01"
|
86
|
+
|
87
|
+
Key.new(actual_key, use_low_r, compressed)
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def self.isPositive(i)
|
94
|
+
sig = "!+-"[i <=> 0]
|
95
|
+
sig.eql?("+")
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.deterministicGenerateK(data, privkey, extra_entropy = nil, group = ECDSA::Group::Secp256k1)
|
99
|
+
# returns a deterministic K -- RFC6979
|
100
|
+
|
101
|
+
hash = data.bytes.to_a
|
102
|
+
|
103
|
+
x = [privkey.to_s(16)].pack("H*").bytes.to_a
|
104
|
+
|
105
|
+
k = [0] * 32
|
106
|
+
v = [1] * 32
|
107
|
+
|
108
|
+
e = (extra_entropy.to_i <= 0 ? [] : [extra_entropy.to_s(16).rjust(64,"0").scan(/../).reverse.join].pack("H*").bytes.to_a)
|
109
|
+
|
110
|
+
# step D
|
111
|
+
k_data = [v, [0], x, hash, e]
|
112
|
+
k_data.flatten!
|
113
|
+
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), k_data.pack("C*")).bytes.to_a
|
114
|
+
|
115
|
+
# step E
|
116
|
+
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
117
|
+
|
118
|
+
# step F
|
119
|
+
k_data = [v, [1], x, hash, e]
|
120
|
+
k_data.flatten!
|
121
|
+
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), k_data.pack("C*")).bytes.to_a
|
122
|
+
|
123
|
+
# step G
|
124
|
+
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
125
|
+
|
126
|
+
# step H2b (Step H1/H2a ignored)
|
127
|
+
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
128
|
+
|
129
|
+
h2b = v.pack("C*").unpack("H*")[0]
|
130
|
+
tNum = h2b.to_i(16)
|
131
|
+
|
132
|
+
# step H3
|
133
|
+
while (!isPositive(tNum) or tNum >= group.order) do
|
134
|
+
# k = crypto.HmacSHA256(Buffer.concat([v, new Buffer([0])]), k)
|
135
|
+
k_data = [v, [0]]
|
136
|
+
k_data.flatten!
|
137
|
+
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), k_data.pack("C*")).bytes.to_a
|
138
|
+
|
139
|
+
# v = crypto.HmacSHA256(v, k)
|
140
|
+
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
141
|
+
|
142
|
+
# T = BigInteger.fromBuffer(v)
|
143
|
+
tNum = v.pack("C*").unpack("H*")[0].to_i(16)
|
144
|
+
end
|
145
|
+
|
146
|
+
tNum
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|