block_io 2.0.0 → 3.0.3

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