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