block_io 2.0.0 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/{.appveyor.yml → .appveyor.yml-disabled} +2 -2
  3. data/.gitignore +1 -0
  4. data/.travis.yml +1 -1
  5. data/LICENSE +1 -1
  6. data/README.md +18 -14
  7. data/block_io.gemspec +7 -7
  8. data/examples/basic.rb +29 -5
  9. data/examples/dtrust.rb +43 -24
  10. data/examples/sweeper.rb +21 -16
  11. data/lib/block_io/api_exception.rb +11 -0
  12. data/lib/block_io/chainparams/BTC.yml +8 -0
  13. data/lib/block_io/chainparams/BTCTEST.yml +8 -0
  14. data/lib/block_io/chainparams/DOGE.yml +8 -0
  15. data/lib/block_io/chainparams/DOGETEST.yml +8 -0
  16. data/lib/block_io/chainparams/LTC.yml +8 -0
  17. data/lib/block_io/chainparams/LTCTEST.yml +8 -0
  18. data/lib/block_io/client.rb +145 -80
  19. data/lib/block_io/extended_bitcoinrb.rb +132 -0
  20. data/lib/block_io/helper.rb +211 -53
  21. data/lib/block_io/key.rb +11 -124
  22. data/lib/block_io/version.rb +1 -1
  23. data/lib/block_io.rb +3 -2
  24. data/spec/client_misc_spec.rb +76 -0
  25. data/spec/client_spec.rb +23 -178
  26. data/spec/dtrust_spec.rb +167 -0
  27. data/spec/helper_spec.rb +117 -7
  28. data/spec/key_spec.rb +50 -19
  29. data/spec/larger_transaction_spec.rb +371 -0
  30. data/spec/sweep_spec.rb +115 -0
  31. data/spec/test-cases/.gitignore +2 -0
  32. data/spec/test-cases/LICENSE +21 -0
  33. data/spec/test-cases/README.md +2 -0
  34. data/spec/test-cases/json/create_and_sign_transaction_response.json +61 -0
  35. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1261 -0
  36. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1266 -0
  37. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1271 -0
  38. data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +3816 -0
  39. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_3of5_195inputs.json +2931 -0
  40. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_4of5_195inputs.json +5 -0
  41. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_251inputs.json +3771 -0
  42. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_252inputs.json +3786 -0
  43. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_253inputs.json +3801 -0
  44. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_251inputs.json +5 -0
  45. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_252inputs.json +5 -0
  46. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_253inputs.json +5 -0
  47. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_251inputs.json +3771 -0
  48. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_252inputs.json +3786 -0
  49. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_253inputs.json +3801 -0
  50. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_251inputs.json +5 -0
  51. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_252inputs.json +5 -0
  52. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_253inputs.json +5 -0
  53. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_3_of_5_keys.json +21 -0
  54. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_4_of_5_keys.json +5 -0
  55. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_3_of_5_keys.json +21 -0
  56. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_4_of_5_keys.json +5 -0
  57. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3_of_5_keys.json +36 -0
  58. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_251outputs.json +591 -0
  59. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_252outputs.json +576 -0
  60. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_253outputs.json +531 -0
  61. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4_of_5_keys.json +5 -0
  62. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_251outputs.json +5 -0
  63. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_252outputs.json +5 -0
  64. data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_253outputs.json +5 -0
  65. data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2pkh.json +5 -0
  66. data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh.json +5 -0
  67. data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh_over_p2sh.json +5 -0
  68. data/spec/test-cases/json/create_and_sign_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +21 -0
  69. data/spec/test-cases/json/create_and_sign_transaction_response_witness_v1_output.json +11 -0
  70. data/spec/test-cases/json/get_balance_response.json +8 -0
  71. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_3of5_195inputs.json +1397 -0
  72. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_4of5_195inputs.json +1397 -0
  73. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_251inputs.json +1795 -0
  74. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_252inputs.json +1802 -0
  75. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_253inputs.json +1809 -0
  76. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_251inputs.json +1789 -0
  77. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_252inputs.json +1802 -0
  78. data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_253inputs.json +1809 -0
  79. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_251inputs.json +1795 -0
  80. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_252inputs.json +1802 -0
  81. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_253inputs.json +1809 -0
  82. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_251inputs.json +1795 -0
  83. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_252inputs.json +1802 -0
  84. data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_253inputs.json +1809 -0
  85. data/spec/test-cases/json/prepare_dtrust_transaction_response_p2sh.json +45 -0
  86. data/spec/test-cases/json/prepare_dtrust_transaction_response_p2wsh_over_p2sh.json +45 -0
  87. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0.json +52 -0
  88. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_251outputs.json +1805 -0
  89. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_252outputs.json +1804 -0
  90. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_253outputs.json +1789 -0
  91. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_251outputs.json +1805 -0
  92. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_252outputs.json +1804 -0
  93. data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_253outputs.json +1789 -0
  94. data/spec/test-cases/json/prepare_sweep_transaction_response_p2pkh.json +35 -0
  95. data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh.json +35 -0
  96. data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh_over_p2sh.json +35 -0
  97. data/spec/test-cases/json/prepare_transaction_response.json +164 -0
  98. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1796 -0
  99. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1803 -0
  100. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1810 -0
  101. data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +5367 -0
  102. data/spec/test-cases/json/prepare_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +76 -0
  103. data/spec/test-cases/json/prepare_transaction_response_witness_v1_output.json +64 -0
  104. data/spec/test-cases/json/summarize_prepared_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +6 -0
  105. metadata +203 -57
  106. data/examples/max_withdrawal.rb +0 -29
  107. data/lib/block_io/constants.rb +0 -10
  108. data/spec/data/sign_and_finalize_dtrust_withdrawal_request.json +0 -1
  109. data/spec/data/sign_and_finalize_sweep_request.json +0 -1
  110. data/spec/data/sign_and_finalize_withdrawal_request.json +0 -4
  111. data/spec/data/sweep_from_address_response.json +0 -1
  112. data/spec/data/withdraw_from_dtrust_address_response.json +0 -1
  113. data/spec/data/withdraw_response.json +0 -1227
  114. data/spec/rfc6979_spec.rb +0 -59
  115. data/spec/withdraw_spec.rb +0 -90
@@ -2,61 +2,215 @@ module BlockIo
2
2
 
3
3
  class Helper
4
4
 
5
- def self.signData(inputs, keys)
6
- # sign the given data with the given keys
5
+ LEGACY_DECRYPTION_ALGORITHM = {
6
+ :pbkdf2_salt => "",
7
+ :pbkdf2_iterations => 2048,
8
+ :pbkdf2_hash_function => "SHA256",
9
+ :pbkdf2_phase1_key_length => 16,
10
+ :pbkdf2_phase2_key_length => 32,
11
+ :aes_iv => nil,
12
+ :aes_cipher => "AES-256-ECB",
13
+ :aes_auth_tag => nil,
14
+ :aes_auth_data => nil
15
+ }
16
+
17
+ def self.allSignaturesPresent?(tx, inputs, signatures, input_address_data)
18
+ # returns true if transaction has all signatures present
19
+
20
+ all_signatures_present = false
7
21
 
8
- raise Exception.new("Keys object must be a hash or array containing the appropriate keys.") unless keys.size >= 1
22
+ inputs.each do |input|
23
+ # check if each input has its required signatures
24
+
25
+ spending_address = input['spending_address']
26
+ current_input_address_data = input_address_data.detect{|x| x['address'] == spending_address}
27
+ required_signatures = current_input_address_data['required_signatures']
28
+ public_keys = current_input_address_data['public_keys']
9
29
 
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 = []
30
+ signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
31
+
32
+ # break the loop if all signatures are not present for this input
33
+ all_signatures_present = (signatures_present.keys.size >= required_signatures)
34
+ break unless all_signatures_present
35
+
36
+ end
37
+
38
+ all_signatures_present
16
39
 
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"])}
40
+ end
41
+
42
+ def self.isSegwitAddressType?(address_type)
19
43
 
20
- data_to_sign = nil
21
- signatures_needed = nil
44
+ case address_type
45
+ when /^P2WPKH(-over-P2SH)?$/
46
+ true
47
+ when /^P2WSH(-over-P2SH)?$/
48
+ true
49
+ when /^WITNESS_V(\d)$/
50
+ true
51
+ else
52
+ false
53
+ end
54
+
55
+ end
56
+
57
+ def self.finalizeTransaction(tx, inputs, signatures, input_address_data)
58
+ # append signatures to the transaction and return its hexadecimal representation
22
59
 
23
- while !(cdata = odata.shift).nil? do
24
- # O(n)
60
+ inputs.each do |input|
61
+ # for each input
62
+
63
+ signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
64
+ address_data = input_address_data.detect{|x| x['address'] == input['spending_address']} # contains public keys (ordered) and the address type
65
+ input_index = input['input_index']
66
+ is_segwit = isSegwitAddressType?(address_data['address_type'])
67
+ script_stack = (is_segwit ? tx.in[input_index].script_witness.stack : tx.in[input_index].script_sig)
25
68
 
26
- if cdata.is_a?(String) then
27
- # this is data to sign
69
+ if ['P2PKH', 'P2WPKH', 'P2WPKH-over-P2SH'].include?(address_data['address_type']) then
70
+ # P2PKH will use script_sig as script_stack
71
+ # P2WPKH input, or P2WPKH-over-P2SH input will use script_witness.stack as script_stack
28
72
 
29
- # make a copy of this
30
- data_to_sign = '' << cdata
73
+ current_public_key = address_data['public_keys'][0]
74
+ current_signature = signatures_present[current_public_key]
31
75
 
32
- # number of signatures needed
33
- signatures_needed = 0 + odata.shift
76
+ # no blank push necessary for P2PKH, P2WPKH, P2WPKH-over-P2SH
77
+ script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
78
+ script_stack << [current_public_key].pack("H*")
34
79
 
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
80
+ # P2WPKH-over-P2SH required script_sig still
81
+ tx.in[input_index].script_sig << (
82
+ Bitcoin::Script.to_p2wpkh(
83
+ Bitcoin::Key.new(:pubkey => current_public_key, :key_type => Bitcoin::Key::TYPES[:compressed]).hash160 # hash160 of the compressed pubkey
84
+ ).to_payload
85
+ ) if address_data['address_type'] == "P2WPKH-over-P2SH"
86
+
87
+ elsif ['P2SH', 'WITNESS_V0', 'P2WSH-over-P2SH'].include?(address_data['address_type']) then
88
+ # P2SH will use script_sig as script_stack
89
+ # P2WSH or P2WSH-over-P2SH input will use script_witness.stack as script_stack
90
+
91
+ script = Bitcoin::Script.to_p2sh_multisig_script(address_data['required_signatures'], address_data['public_keys'])
92
+
93
+ script_stack << '' # blank push for scripthash always
94
+
95
+ signatures_added = 0
96
+
97
+ address_data['public_keys'].each do |public_key|
98
+ next unless signatures_present.key?(public_key)
99
+
100
+ # append signatures, no sighash needed, in correct order of public keys
101
+ current_signature = signatures_present[public_key]
102
+ script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
103
+
104
+ signatures_added += 1
105
+
106
+ # required signatures added? break loop and move on
107
+ break if signatures_added == address_data['required_signatures']
43
108
  end
109
+
110
+ script_stack << script.last.to_payload
111
+
112
+ # P2WSH-over-P2SH needs script_sig populated still
113
+ tx.in[input_index].script_sig << Bitcoin::Script.to_p2wsh(script.last).to_payload if address_data['address_type'] == "P2WSH-over-P2SH"
44
114
 
115
+ else
116
+ raise "Unrecognized input address: #{address_data['address_type']}"
45
117
  end
118
+
119
+ end
46
120
 
121
+ tx.to_hex
122
+
123
+ end
124
+
125
+ def self.getSigHashForInput(tx, input_index, input_data, input_address_data)
126
+ # returns the sighash for the given input in bytes
127
+
128
+ address_type = input_address_data["address_type"]
129
+ input_value = (BigDecimal(input_data['input_value']) * BigDecimal(100000000)).to_i # in sats
130
+ sighash = nil
131
+
132
+ if address_type == "P2SH" then
133
+ # P2SH addresses
134
+
135
+ script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
136
+ sighash = tx.sighash_for_input(input_index, script.last)
137
+
138
+ elsif address_type == "P2WSH-over-P2SH" or address_type == "WITNESS_V0" then
139
+ # P2WSH-over-P2SH addresses
140
+ # WITNESS_V0 addresses
141
+
142
+ script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
143
+ sighash = tx.sighash_for_input(input_index, script.last, amount: input_value, sig_version: :witness_v0)
144
+
145
+ elsif address_type == "P2WPKH-over-P2SH" or address_type == "P2WPKH" then
146
+ # P2WPKH-over-P2SH addresses
147
+ # P2WPKH addresses
148
+
149
+ pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
150
+ script = Bitcoin::Script.to_p2wpkh(pub_key.hash160)
151
+ sighash = tx.sighash_for_input(input_index, script, amount: input_value, sig_version: :witness_v0)
152
+
153
+ elsif address_type == "P2PKH" then
154
+ # P2PKH addresses
155
+
156
+ pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
157
+ script = Bitcoin::Script.to_p2pkh(pub_key.hash160)
158
+ sighash = tx.sighash_for_input(input_index, script)
159
+
160
+ else
161
+ raise "Unrecognize address type: #{address_type}"
47
162
  end
48
163
 
49
- signatures_added
164
+ sighash
165
+
50
166
  end
51
167
 
52
- def self.extractKey(encrypted_data, b64_enc_key, use_low_r = true)
168
+ def self.getDecryptionAlgorithm(user_key_algorithm = nil)
169
+ # mainly used so existing unit tests do not break
170
+
171
+ algorithm = ({}).merge(LEGACY_DECRYPTION_ALGORITHM)
172
+
173
+ if !user_key_algorithm.nil? then
174
+ algorithm[:pbkdf2_salt] = user_key_algorithm['pbkdf2_salt']
175
+ algorithm[:pbkdf2_iterations] = user_key_algorithm['pbkdf2_iterations']
176
+ algorithm[:pbkdf2_hash_function] = user_key_algorithm['pbkdf2_hash_function']
177
+ algorithm[:pbkdf2_phase1_key_length] = user_key_algorithm['pbkdf2_phase1_key_length']
178
+ algorithm[:pbkdf2_phase2_key_length] = user_key_algorithm['pbkdf2_phase2_key_length']
179
+ algorithm[:aes_iv] = user_key_algorithm['aes_iv']
180
+ algorithm[:aes_cipher] = user_key_algorithm['aes_cipher']
181
+ algorithm[:aes_auth_tag] = user_key_algorithm['aes_auth_tag']
182
+ algorithm[:aes_auth_data] = user_key_algorithm['aes_auth_data']
183
+ end
184
+
185
+ algorithm
186
+
187
+ end
188
+
189
+ def self.dynamicExtractKey(user_key, pin)
190
+ # user_key object contains the encrypted user key and decryption algorithm
191
+
192
+ algorithm = self.getDecryptionAlgorithm(user_key['algorithm'])
193
+
194
+ aes_key = self.pinToAesKey(pin, algorithm[:pbkdf2_iterations],
195
+ algorithm[:pbkdf2_salt],
196
+ algorithm[:pbkdf2_hash_function],
197
+ algorithm[:pbkdf2_phase1_key_length],
198
+ algorithm[:pbkdf2_phase2_key_length])
199
+
200
+ decrypted = self.decrypt(user_key['encrypted_passphrase'], aes_key, algorithm[:aes_iv], algorithm[:aes_cipher], algorithm[:aes_auth_tag], algorithm[:aes_auth_data])
201
+
202
+ Key.from_passphrase(decrypted)
203
+
204
+ end
205
+
206
+ def self.extractKey(encrypted_data, b64_enc_key)
53
207
  # passphrase is in plain text
54
208
  # encrypted_data is in base64, as it was stored on Block.io
55
209
  # returns the private key extracted from the given encrypted data
56
210
 
57
211
  decrypted = self.decrypt(encrypted_data, b64_enc_key)
58
212
 
59
- Key.from_passphrase(decrypted, use_low_r)
213
+ Key.from_passphrase(decrypted)
60
214
 
61
215
  end
62
216
 
@@ -65,24 +219,25 @@ module BlockIo
65
219
  OpenSSL::Digest::SHA256.digest(value).unpack("H*")[0]
66
220
  end
67
221
 
68
- def self.pinToAesKey(secret_pin, iterations = 2048)
222
+ def self.pinToAesKey(secret_pin, iterations = 2048, salt = "", hash_function = "SHA256", pbkdf2_phase1_key_length = 16, pbkdf2_phase2_key_length = 32)
69
223
  # converts the pincode string to PBKDF2
70
224
  # returns a base64 version of PBKDF2 pincode
71
- salt = ""
72
225
 
226
+ raise Exception.new("Unknown hash function specified. Are you using current version of this library?") unless hash_function == "SHA256"
227
+
73
228
  part1 = OpenSSL::PKCS5.pbkdf2_hmac(
74
229
  secret_pin,
75
- "",
76
- 1024,
77
- 128/8,
230
+ salt,
231
+ iterations/2,
232
+ pbkdf2_phase1_key_length,
78
233
  OpenSSL::Digest::SHA256.new
79
234
  ).unpack("H*")[0]
80
235
 
81
236
  part2 = OpenSSL::PKCS5.pbkdf2_hmac(
82
237
  part1,
83
- "",
84
- 1024,
85
- 256/8,
238
+ salt,
239
+ iterations/2,
240
+ pbkdf2_phase2_key_length,
86
241
  OpenSSL::Digest::SHA256.new
87
242
  ) # binary
88
243
 
@@ -90,22 +245,20 @@ module BlockIo
90
245
 
91
246
  end
92
247
 
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
248
  # 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")
249
+ def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_tag = nil, auth_data = nil)
250
+
251
+ raise Exception.new("Auth tag must be 16 bytes exactly.") unless auth_tag.nil? or auth_tag.size == 32
101
252
 
102
253
  response = nil
103
254
 
104
255
  begin
105
- aes = OpenSSL::Cipher.new(cipher_type)
256
+ aes = OpenSSL::Cipher.new(cipher_type.downcase)
106
257
  aes.decrypt
107
258
  aes.key = b64_enc_key.unpack("m0")[0]
108
- aes.iv = iv unless iv.nil?
259
+ aes.iv = [iv].pack("H*") unless iv.nil?
260
+ aes.auth_tag = [auth_tag].pack("H*") unless auth_tag.nil?
261
+ aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
109
262
  response = aes.update(encrypted_data.unpack("m0")[0]) << aes.final
110
263
  rescue Exception => e
111
264
  # decryption failed, must be an invalid Secret PIN
@@ -116,12 +269,17 @@ module BlockIo
116
269
  end
117
270
 
118
271
  # 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)
272
+ def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_data = nil)
273
+ aes = OpenSSL::Cipher.new(cipher_type.downcase)
121
274
  aes.encrypt
122
275
  aes.key = b64_enc_key.unpack("m0")[0]
123
- aes.iv = iv unless iv.nil?
124
- [aes.update(data) << aes.final].pack("m0")
276
+ aes.iv = [iv].pack("H*") unless iv.nil?
277
+ aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
278
+ result = [aes.update(data) << aes.final].pack("m0")
279
+ auth_tag = (cipher_type.end_with?("-GCM") ? aes.auth_tag.unpack("H*")[0] : nil)
280
+
281
+ {:aes_auth_tag => auth_tag, :aes_cipher_text => result, :aes_iv => iv, :aes_cipher => cipher_type, :aes_auth_data => auth_data}
282
+
125
283
  end
126
284
 
127
285
  # courtesy bitcoin-ruby
data/lib/block_io/key.rb CHANGED
@@ -2,67 +2,18 @@ module BlockIo
2
2
 
3
3
  class Key
4
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]
5
+ def self.generate
6
+ # returns a new key
7
+ Bitcoin::Key.generate(Bitcoin::Key::TYPES[:compressed]) # compressed
25
8
  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
9
 
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*")))
10
+ def self.from_private_key_hex(priv_key_hex)
11
+ # returns Bitcoin::Key (compressed)
12
+ # quirky behavior from bitcoinrb 0.7.0: use IntegerOctetString.encode on private key (integer) first
13
+ Bitcoin::Key.new(:priv_key => ECDSA::Format::IntegerOctetString.encode(priv_key_hex.to_i(16), 32).bth, :key_type => Bitcoin::Key::TYPES[:compressed])
63
14
  end
64
15
 
65
- def self.from_passphrase(passphrase, use_low_r = true)
16
+ def self.from_passphrase(passphrase)
66
17
  # ATTENTION: use BlockIo::Key.new to generate new private keys. Using passphrases is not recommended due to lack of / low entropy.
67
18
  # create a private/public key pair from a given passphrase
68
19
  # use a long, random passphrase. your security depends on the passphrase's entropy.
@@ -72,78 +23,14 @@ module BlockIo
72
23
  hashed_key = Helper.sha256([passphrase].pack("H*")) # must pass bytes to sha256
73
24
 
74
25
  # 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)
26
+ BlockIo::Key.from_private_key_hex((hashed_key.to_i(16) % ECDSA::Group::Secp256k1.order).to_s(16))
76
27
  end
77
28
 
78
- def self.from_wif(wif, use_low_r = true)
29
+ def self.from_wif(wif)
79
30
  # 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
31
 
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
32
+ Bitcoin::Key.from_wif(wif)
145
33
 
146
- tNum
147
34
  end
148
35
 
149
36
  end
@@ -1,3 +1,3 @@
1
1
  module BlockIo
2
- VERSION = "2.0.0"
2
+ VERSION = "3.0.3"
3
3
  end
data/lib/block_io.rb CHANGED
@@ -1,15 +1,16 @@
1
1
  require "http"
2
2
  require "oj"
3
- require "ecdsa"
3
+ require "bitcoin"
4
4
  require "openssl"
5
5
  require "securerandom"
6
6
  require "connection_pool"
7
7
 
8
8
  require_relative "block_io/version"
9
- require_relative "block_io/constants"
10
9
  require_relative "block_io/helper"
11
10
  require_relative "block_io/key"
12
11
  require_relative "block_io/client"
12
+ require_relative "block_io/api_exception"
13
+ require_relative "block_io/extended_bitcoinrb"
13
14
 
14
15
  module BlockIo
15
16
 
@@ -0,0 +1,76 @@
1
+ require 'securerandom'
2
+
3
+ describe "Client" do
4
+
5
+ before(:each) do
6
+ @api_key = "0000-0000-0000-0000"
7
+ @req_params = {:to_address => "QTLcyTFrH7T6kqUsi1VV2mJVXmX3AmwUNH", :amounts => "0.248"}
8
+ @headers = {
9
+ 'Accept' => 'application/json',
10
+ 'Connection' => 'Keep-Alive',
11
+ 'Content-Type' => 'application/json; charset=UTF-8',
12
+ 'Host' => 'block.io',
13
+ 'User-Agent' => "gem:block_io:#{BlockIo::VERSION}"
14
+ }
15
+
16
+ @prepare_transaction_response = File.new("spec/test-cases/json/prepare_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json").read
17
+ @stub1 = stub_request(:post, "https://block.io/api/v2/prepare_transaction").
18
+ with(
19
+ body: @req_params.merge({:api_key => @api_key}).to_json,
20
+ headers: @headers).
21
+ to_return(status: 200, body: @prepare_transaction_response, headers: {})
22
+
23
+ @create_and_sign_transaction_response = File.new("spec/test-cases/json/create_and_sign_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json").read
24
+ @summarize_prepared_transaction_response = File.new("spec/test-cases/json/summarize_prepared_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json").read
25
+
26
+ @insecure_pin_valid = "d1650160bd8d2bb32bebd139d0063eb6063ffa2f9e4501ad" # still insecure, don't use this!
27
+ @insecure_pin_invalid = "blockiotestpininsecure"
28
+ end
29
+
30
+ context "summarize_prepare_transaction" do
31
+
32
+ before(:each) do
33
+
34
+ @blockio = BlockIo::Client.new(:api_key => @api_key, :pin => @insecure_pin_valid)
35
+
36
+ end
37
+
38
+ it "success" do
39
+
40
+ @blockio.prepare_transaction(@req_params)
41
+
42
+ expect(@stub1).to have_been_requested.times(1)
43
+
44
+ expect(@blockio.summarize_prepared_transaction(Oj.safe_load(@prepare_transaction_response))).to eq(Oj.safe_load(@summarize_prepared_transaction_response))
45
+
46
+ expect(@blockio.create_and_sign_transaction(Oj.safe_load(@prepare_transaction_response))).to eq(Oj.safe_load(@create_and_sign_transaction_response))
47
+
48
+ end
49
+
50
+ end
51
+
52
+ context "create_and_sign_transaction_with_invalid_expected_unsigned_txid" do
53
+
54
+ before(:each) do
55
+
56
+ @blockio = BlockIo::Client.new(:api_key => @api_key, :pin => @insecure_pin_valid)
57
+
58
+ end
59
+
60
+ it "fails" do
61
+
62
+ @blockio.prepare_transaction(@req_params)
63
+
64
+ expect(@stub1).to have_been_requested.times(1)
65
+
66
+ @bad_response = Oj.safe_load(@prepare_transaction_response)
67
+ @bad_response['data']['expected_unsigned_txid'] = SecureRandom.hex(32)
68
+
69
+ expect{@blockio.create_and_sign_transaction(@bad_response)}.to raise_error(Exception, "Expected unsigned transaction ID mismatch. Please report this error to support@block.io.")
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+