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