block_io 1.0.5 → 2.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.
- checksums.yaml +5 -5
- data/.appveyor.yml +26 -0
- data/.gitignore +4 -1
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/README.md +17 -7
- data/block_io.gemspec +9 -8
- data/examples/basic.rb +17 -13
- data/examples/dtrust.rb +37 -38
- data/examples/max_withdrawal.rb +29 -0
- data/examples/proxy.rb +36 -0
- data/examples/sweeper.rb +29 -0
- data/lib/block_io.rb +15 -302
- 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 +103 -38
@@ -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
|