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.
- checksums.yaml +4 -4
- data/{.appveyor.yml → .appveyor.yml-disabled} +2 -2
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/LICENSE +1 -1
- data/README.md +18 -14
- data/block_io.gemspec +7 -7
- data/examples/basic.rb +29 -5
- data/examples/dtrust.rb +43 -24
- data/examples/sweeper.rb +21 -16
- data/lib/block_io/api_exception.rb +11 -0
- data/lib/block_io/chainparams/BTC.yml +8 -0
- data/lib/block_io/chainparams/BTCTEST.yml +8 -0
- data/lib/block_io/chainparams/DOGE.yml +8 -0
- data/lib/block_io/chainparams/DOGETEST.yml +8 -0
- data/lib/block_io/chainparams/LTC.yml +8 -0
- data/lib/block_io/chainparams/LTCTEST.yml +8 -0
- data/lib/block_io/client.rb +145 -80
- data/lib/block_io/extended_bitcoinrb.rb +132 -0
- data/lib/block_io/helper.rb +211 -53
- data/lib/block_io/key.rb +11 -124
- data/lib/block_io/version.rb +1 -1
- data/lib/block_io.rb +3 -2
- data/spec/client_misc_spec.rb +76 -0
- data/spec/client_spec.rb +23 -178
- data/spec/dtrust_spec.rb +167 -0
- data/spec/helper_spec.rb +117 -7
- data/spec/key_spec.rb +50 -19
- data/spec/larger_transaction_spec.rb +371 -0
- data/spec/sweep_spec.rb +115 -0
- data/spec/test-cases/.gitignore +2 -0
- data/spec/test-cases/LICENSE +21 -0
- data/spec/test-cases/README.md +2 -0
- data/spec/test-cases/json/create_and_sign_transaction_response.json +61 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1261 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1266 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1271 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +3816 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_3of5_195inputs.json +2931 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_4of5_195inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_251inputs.json +3771 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_252inputs.json +3786 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_253inputs.json +3801 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_251inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_252inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_253inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_251inputs.json +3771 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_252inputs.json +3786 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_253inputs.json +3801 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_251inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_252inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_253inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_3_of_5_keys.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_3_of_5_keys.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3_of_5_keys.json +36 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_251outputs.json +591 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_252outputs.json +576 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_253outputs.json +531 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_251outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_252outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_253outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2pkh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh_over_p2sh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_witness_v1_output.json +11 -0
- data/spec/test-cases/json/get_balance_response.json +8 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_3of5_195inputs.json +1397 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_4of5_195inputs.json +1397 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_251inputs.json +1789 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_p2sh.json +45 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_p2wsh_over_p2sh.json +45 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0.json +52 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_251outputs.json +1805 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_252outputs.json +1804 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_253outputs.json +1789 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_251outputs.json +1805 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_252outputs.json +1804 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_253outputs.json +1789 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2pkh.json +35 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh.json +35 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh_over_p2sh.json +35 -0
- data/spec/test-cases/json/prepare_transaction_response.json +164 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1796 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1803 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1810 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +5367 -0
- data/spec/test-cases/json/prepare_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +76 -0
- data/spec/test-cases/json/prepare_transaction_response_witness_v1_output.json +64 -0
- data/spec/test-cases/json/summarize_prepared_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +6 -0
- metadata +203 -57
- data/examples/max_withdrawal.rb +0 -29
- data/lib/block_io/constants.rb +0 -10
- data/spec/data/sign_and_finalize_dtrust_withdrawal_request.json +0 -1
- data/spec/data/sign_and_finalize_sweep_request.json +0 -1
- data/spec/data/sign_and_finalize_withdrawal_request.json +0 -4
- data/spec/data/sweep_from_address_response.json +0 -1
- data/spec/data/withdraw_from_dtrust_address_response.json +0 -1
- data/spec/data/withdraw_response.json +0 -1227
- data/spec/rfc6979_spec.rb +0 -59
- data/spec/withdraw_spec.rb +0 -90
data/lib/block_io/helper.rb
CHANGED
@@ -2,61 +2,215 @@ module BlockIo
|
|
2
2
|
|
3
3
|
class Helper
|
4
4
|
|
5
|
-
|
6
|
-
|
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
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.isSegwitAddressType?(address_type)
|
19
43
|
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
#
|
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
|
27
|
-
#
|
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
|
-
|
30
|
-
|
73
|
+
current_public_key = address_data['public_keys'][0]
|
74
|
+
current_signature = signatures_present[current_public_key]
|
31
75
|
|
32
|
-
#
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
164
|
+
sighash
|
165
|
+
|
50
166
|
end
|
51
167
|
|
52
|
-
def self.
|
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
|
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
|
-
|
77
|
-
|
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
|
-
|
85
|
-
|
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
|
-
|
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
|
6
|
-
#
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
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.
|
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
|
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
|
-
|
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
|
data/lib/block_io/version.rb
CHANGED
data/lib/block_io.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
require "http"
|
2
2
|
require "oj"
|
3
|
-
require "
|
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
|
+
|