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