bitcoinrb 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc2fd227e7a25b6fa3bdd88ef6035e7b7f4a76bb1ef501285ccd0f7f5d961c4c
4
- data.tar.gz: 0f2557375f3918a5416983dde696cb922be71cf7a7c762c2be2305b6e9edb3d3
3
+ metadata.gz: 360a1a8533df9b68228d18279909e7bc5f45d4d5679caa1a02cda75e0a9140d0
4
+ data.tar.gz: ff86fcec32b3fe1394eea043080aaedafee61e724ec689cde5efdd7b9ca5c307
5
5
  SHA512:
6
- metadata.gz: 9cd9ed6bfca773abf5d25228be6b2a88ef7a17c163e3ddd5bb7de9c6365261d65e51fa8c3e9a6f87304a5bfc48036f87e7b0fb99d8b910c368001f7c5aa78d81
7
- data.tar.gz: bec0d5c4ab556984e2fb9fa4da85008c92fb392d4510e41901701626658f8356f20aa5b74d1837ed90c1a35efe86b52f6ea5713b90f4c2caf2112b47a42a1706
6
+ metadata.gz: 9aeb17cc9dc42ce33c4c31ed1f6424569d5c0f4f20627015c930d11e5ba2bf9c15f43cd20a666257caba689ffc0010b08b5855c13a3cf61a724c05fe8bcd383d
7
+ data.tar.gz: 14848d9c37a69ec5b0d2f993ce92c3cad3bdfc6275151789ff768ad5989d101f9c5545f1dc668d372f847b574fdc60d10e5f90b81b377b23ac20bfb3705b0a3a
data/bitcoinrb.gemspec CHANGED
@@ -27,8 +27,8 @@ Gem::Specification.new do |spec|
27
27
  spec.add_runtime_dependency 'bip-schnorr', '>= 0.7.0'
28
28
  spec.add_runtime_dependency 'base32', '>= 0.3.4'
29
29
  spec.add_runtime_dependency 'base64', '~> 0.2.0'
30
- spec.add_runtime_dependency 'secp256k1rb', '0.1.1'
30
+ spec.add_runtime_dependency 'secp256k1rb', '0.8.0'
31
31
  spec.add_runtime_dependency 'logger'
32
- spec.add_runtime_dependency 'merkle', '0.3.0'
32
+ spec.add_runtime_dependency 'merkle', '1.0.0'
33
33
  spec.add_runtime_dependency 'dnsruby', '1.73.0'
34
34
  end
@@ -8,6 +8,10 @@ module Bitcoin
8
8
  ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
9
9
  SIZE = ALPHABET.size
10
10
 
11
+ # Upper bound for #decode. Decoding is quadratic in the length of the input, and the
12
+ # longest value Bitcoin encodes with Base58 is an extended key, at 111 characters.
13
+ MAX_LENGTH = 256
14
+
11
15
  # encode hex value to base58 string.
12
16
  def encode(hex)
13
17
  leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
@@ -21,11 +25,19 @@ module Bitcoin
21
25
  end
22
26
 
23
27
  # decode base58 string to hex value.
28
+ # @param [String] base58_val Base58 string.
29
+ # @return [String] Decoded value with hex format.
30
+ # @raise [ArgumentError] If +base58_val+ is longer than MAX_LENGTH or holds a character
31
+ # which is not in the alphabet.
24
32
  def decode(base58_val)
33
+ if base58_val.length > MAX_LENGTH
34
+ raise ArgumentError, "Base58 string must not be longer than #{MAX_LENGTH} characters."
35
+ end
25
36
  int_val = 0
26
- base58_val.reverse.split(//).each_with_index do |char,index|
27
- raise ArgumentError, 'Value passed not a valid Base58 String.' if (char_index = ALPHABET.index(char)).nil?
28
- int_val += char_index * (SIZE ** index)
37
+ base58_val.each_char do |char|
38
+ char_index = ALPHABET.index(char)
39
+ raise ArgumentError, 'Value passed not a valid Base58 String.' if char_index.nil?
40
+ int_val = int_val * SIZE + char_index
29
41
  end
30
42
  s = int_val.to_even_length_hex
31
43
  s = '' if s == '00'
@@ -91,13 +91,16 @@ module Bitcoin
91
91
  ECDSA::Format::IntegerOctetString.encode(result, 32).bth
92
92
  end
93
93
 
94
+ # Number of cases xswiftec_inv selects between. It reads c as 3 bits, c & 1, c & 2 and c & 4.
95
+ CASE_COUNT = 8
96
+
94
97
  # Given a field element X on the curve, find (u, t) that encode them.
95
98
  # @param [String] x coordinate with hex format.
96
99
  # @return [String] ElligatorSwift public key with hex format.
97
100
  def xelligatorswift(x)
98
101
  loop do
99
102
  u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
100
- c = SecureRandom.random_number(9)
103
+ c = SecureRandom.random_number(CASE_COUNT)
101
104
  t = xswiftec_inv(x, u, c)
102
105
  unless t.nil?
103
106
  return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) +
data/lib/bitcoin/block.rb CHANGED
@@ -28,7 +28,7 @@ module Bitcoin
28
28
  header = BlockHeader.new(
29
29
  version,
30
30
  '00' * 32,
31
- Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: [coinbase.txid]).compute_root.rhex,
31
+ Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: [coinbase.txid]).compute_root.rhex,
32
32
  time,
33
33
  bits,
34
34
  nonce
@@ -72,7 +72,7 @@ module Bitcoin
72
72
 
73
73
  # calculate merkle root from tx list.
74
74
  def calculate_merkle_root
75
- tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: transactions.map(&:tx_hash))
75
+ tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: transactions.map(&:tx_hash))
76
76
  tree.compute_root
77
77
  end
78
78
 
@@ -86,7 +86,7 @@ module Bitcoin
86
86
  witness_hashes = [COINBASE_WTXID]
87
87
  witness_hashes += (transactions[1..-1].map(&:witness_hash))
88
88
  reserved_value = transactions[0].inputs[0].script_witness.stack.map(&:bth).join
89
- tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin, leaves: witness_hashes)
89
+ tree = Merkle::BinaryTree.new(config: Merkle::Config.bitcoin(element_encoding: :hex), leaves: witness_hashes)
90
90
  root_hash = tree.compute_root
91
91
  Bitcoin.double_sha256([root_hash + reserved_value].pack('H*')).bth
92
92
  end
data/lib/bitcoin/key.rb CHANGED
@@ -170,7 +170,7 @@ module Bitcoin
170
170
  else
171
171
  false
172
172
  end
173
- rescue Exception
173
+ rescue StandardError
174
174
  false
175
175
  end
176
176
  end
@@ -32,11 +32,17 @@ module Bitcoin
32
32
  magic = buf.read(4)
33
33
  raise ArgumentError, 'Invalid magic.' unless magic == Bitcoin.chain_params.magic_head.htb
34
34
  command = buf.read(12).delete("\x00")
35
+ # The length is announced by the peer, so it is checked before it is used to read.
35
36
  length = buf.read(4).unpack1('V')
37
+ if length > MAX_PROTOCOL_MESSAGE_LENGTH
38
+ raise ArgumentError, "Payload must not be longer than #{MAX_PROTOCOL_MESSAGE_LENGTH} bytes."
39
+ end
36
40
  checksum = buf.read(4)
41
+ # read returns "" for a length of 0 and nil once the buffer is exhausted.
37
42
  payload = buf.read(length)
43
+ raise ArgumentError, 'Payload is shorter than the announced length.' unless payload&.bytesize == length
38
44
  raise ArgumentError, 'Checksum do not match.' unless checksum == Bitcoin.double_sha256(payload)[0...4]
39
- Bitcoin::Message.decode(command, payload&.bth)
45
+ Bitcoin::Message.decode(command, payload.bth)
40
46
  end
41
47
 
42
48
  end
@@ -22,8 +22,18 @@ module Bitcoin
22
22
  def self.parse_from_payload(payload)
23
23
  buf = StringIO.new(payload)
24
24
  filter_count = Bitcoin.unpack_var_int_from_io(buf)
25
+ # A filterload message is received from an untrusted peer, so the limits which bound
26
+ # the work BloomFilter#add and #contains? perform have to be enforced here.
27
+ if filter_count > Bitcoin::BloomFilter::MAX_BLOOM_FILTER_SIZE
28
+ raise Bitcoin::Message::Error,
29
+ "filter size must be less than or equal to #{Bitcoin::BloomFilter::MAX_BLOOM_FILTER_SIZE}."
30
+ end
25
31
  filter = buf.read(filter_count).unpack('C*')
26
32
  func_count = buf.read(4).unpack1('V')
33
+ if func_count > Bitcoin::BloomFilter::MAX_HASH_FUNCS
34
+ raise Bitcoin::Message::Error,
35
+ "hash funcs must be less than or equal to #{Bitcoin::BloomFilter::MAX_HASH_FUNCS}."
36
+ end
27
37
  tweak = buf.read(4).unpack1('V')
28
38
  flag = buf.read(1).unpack1('C')
29
39
  FilterLoad.new(Bitcoin::BloomFilter.new(filter, func_count, tweak), flag)
@@ -28,6 +28,12 @@ module Bitcoin
28
28
  flag_count = Bitcoin.unpack_var_int_from_io(buf)
29
29
  # A sequence of bits packed eight in a byte with the least significant bit first.
30
30
  m.flags = buf.read(flag_count).bth
31
+ # Reject a tree an untrusted peer could use to exhaust memory or CPU in #partial_tree.
32
+ begin
33
+ Bitcoin::PartialTree.validate!(m.tx_count, m.hashes, Bitcoin.byte_to_bit(m.flags.htb))
34
+ rescue ArgumentError => e
35
+ raise Bitcoin::Message::Error, e.message
36
+ end
31
37
  m
32
38
  end
33
39
 
@@ -51,6 +51,9 @@ module Bitcoin
51
51
 
52
52
  USER_AGENT = "/bitcoinrb:#{Bitcoin::VERSION}/"
53
53
 
54
+ # The maximum payload size a peer may announce in a message header.
55
+ MAX_PROTOCOL_MESSAGE_LENGTH = 4_000_000
56
+
54
57
  SERVICE_FLAGS = {
55
58
  none: 0,
56
59
  network: 1 << 0, # the node is capable of serving the block chain. It is currently set by all Bitcoin Core node, and is unset by SPV clients or other peers that just want network services but don't provide them.
@@ -4,6 +4,10 @@ module Bitcoin
4
4
  # For a complete Merkle tree implementation, migrate to the merkle gem.
5
5
  class PartialTree
6
6
 
7
+ # The maximum number of transactions a block can contain, so the maximum tx_count
8
+ # a valid merkleblock message can commit to.
9
+ MAX_TX_COUNT = MAX_BLOCK_WEIGHT / MIN_TRANSACTION_WEIGHT
10
+
7
11
  attr_accessor :root
8
12
 
9
13
  def initialize(root = nil)
@@ -15,7 +19,13 @@ module Bitcoin
15
19
  end
16
20
 
17
21
  # https://bitcoin.org/en/developer-reference#creating-a-merkleblock-message
22
+ # @param [Integer] tx_count The number of transactions in the block.
23
+ # @param [Array] hashes Array of hash values with hex format.
24
+ # @param [String] flags The sequence of bits, a string of '0' and '1'.
25
+ # @return [Bitcoin::PartialTree]
26
+ # @raise [ArgumentError] If the given parameters are out of range or inconsistent.
18
27
  def self.build(tx_count, hashes, flags)
28
+ validate!(tx_count, hashes, flags)
19
29
  flags = flags.each_char.map(&:to_i)
20
30
  root = build_initial_tree( Array.new(tx_count) { Node.new })
21
31
  current_node = root
@@ -37,7 +47,22 @@ module Bitcoin
37
47
  new(root)
38
48
  end
39
49
 
50
+ # Validate the parameters of a partial merkle tree.
51
+ # A merkleblock message is received from an untrusted peer, so these bounds are required
52
+ # to stop it from making #build allocate an unbounded number of nodes or loop forever.
53
+ # See Bitcoin Core's CPartialMerkleTree::ExtractMatches.
54
+ # @raise [ArgumentError]
55
+ def self.validate!(tx_count, hashes, flags)
56
+ raise ArgumentError, 'tx_count must be greater than 0.' unless tx_count.is_a?(Integer) && tx_count > 0
57
+ raise ArgumentError, "tx_count must be less than or equal to #{MAX_TX_COUNT}." if tx_count > MAX_TX_COUNT
58
+ # There can never be more hashes provided than one for every txid.
59
+ raise ArgumentError, 'hashes must not be greater than tx_count.' if hashes.size > tx_count
60
+ # There must be at least one bit per node in the partial tree, and at least one node per hash.
61
+ raise ArgumentError, 'flags must have at least one bit per hash.' if flags.size < hashes.size
62
+ end
63
+
40
64
  def self.build_initial_tree(nodes)
65
+ raise ArgumentError, 'nodes must not be empty.' if nodes.empty?
41
66
  while nodes.size != 1
42
67
  nodes = nodes.each_slice(2).map { |m|
43
68
  parent = Node.new
@@ -192,6 +192,13 @@ module Bitcoin
192
192
  payload
193
193
  end
194
194
 
195
+ # Get the previous output this input spends.
196
+ # @param [Integer] index The index of the outpoint this input refers to.
197
+ # @return [Bitcoin::TxOut] The previous output, or nil if this input does not carry it.
198
+ def utxo(index)
199
+ (non_witness_utxo ? non_witness_utxo.out[index] : nil) || witness_utxo
200
+ end
201
+
195
202
  # Check whether input's scriptPubkey is correct witness.
196
203
  # @return [Boolean]
197
204
  def valid_witness_input?
@@ -277,16 +277,17 @@ module Bitcoin
277
277
  extract_tx.in[index].script_sig = input.final_script_sig if input.final_script_sig
278
278
  extract_tx.in[index].script_witness = input.final_script_witness if input.final_script_witness
279
279
  end
280
+ # A taproot sighash commits to every prevout of the tx, see BIP-341, so they are all
281
+ # collected before any input is verified.
282
+ prevouts = extract_tx.in.each_with_index.map do |tx_in, index|
283
+ utxo = inputs[index].utxo(tx_in.out_point.index)
284
+ raise ArgumentError, "input[#{index}] does not have utxo." unless utxo
285
+ utxo
286
+ end
280
287
  # validate signature
281
- extract_tx.in.each_with_index do |tx_in, index|
282
- input = inputs[index]
283
- if input.non_witness_utxo
284
- utxo = input.non_witness_utxo.out[tx_in.out_point.index]
285
- raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey)
286
- else
287
- utxo = input.witness_utxo
288
- raise ArgumentError, "input[#{index}] does not have utxo." unless utxo
289
- raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value)
288
+ prevouts.each_with_index do |utxo, index|
289
+ unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value, prevouts: prevouts)
290
+ raise "input[#{index}]'s signature is invalid.'"
290
291
  end
291
292
  end
292
293
  extract_tx
@@ -61,8 +61,9 @@ module Bitcoin
61
61
  request.content_type = 'application/json'
62
62
  request.body = data.to_json
63
63
  response = http.request(request)
64
- body = response.body
65
- json_data = JSON.parse(body.gsub(/\\u([\da-fA-F]{4})/) { [$1].pack('H*').unpack('n*').pack('U*').encode('ISO-8859-1').force_encoding('UTF-8') })
64
+ # Unescape \uXXXX with JSON.parse itself. Doing it beforehand lets a " contained in
65
+ # a string value close that string and inject arbitrary JSON into the parsed result.
66
+ json_data = JSON.parse(response.body)
66
67
  response = convert_floats_to_strings(json_data)
67
68
  raise response['error'].to_json if response['error']
68
69
  response['result']
@@ -124,7 +124,7 @@ module Bitcoin
124
124
  segwit_addr = Bech32::SegwitAddr.new(addr)
125
125
  raise ArgumentError, 'Invalid address.' unless Bitcoin.chain_params.bech32_hrp == segwit_addr.hrp
126
126
  Bitcoin::Script.parse_from_payload(segwit_addr.to_script_pubkey.htb)
127
- rescue Exception => e
127
+ rescue StandardError => e
128
128
  begin
129
129
  hex, addr_version = Bitcoin.decode_base58_address(addr)
130
130
  rescue
@@ -568,7 +568,7 @@ module Bitcoin
568
568
  end
569
569
  rescue ScriptNumError => e
570
570
  return set_error(SCRIPT_ERR_SCRIPTNUM, e.message)
571
- rescue Exception => e
571
+ rescue StandardError => e
572
572
  return set_error(SCRIPT_ERR_UNKNOWN_ERROR, e.message)
573
573
  end
574
574
 
@@ -30,7 +30,7 @@ module Bitcoin
30
30
  begin
31
31
  key = Key.new(pubkey: pubkey, key_type: key_type, allow_hybrid: allow_hybrid)
32
32
  key.verify(sig, sighash)
33
- rescue Exception
33
+ rescue StandardError
34
34
  false
35
35
  end
36
36
  end
@@ -40,9 +40,14 @@ module Bitcoin
40
40
  # @param [String] pubkey a public key with hex format.
41
41
  # @param [Symbol] sig_version whether :taproot or :tapscript
42
42
  # @return [Boolean] verification result
43
+ # @raise [ArgumentError] If +prevouts+ does not cover every input of the tx.
43
44
  def check_schnorr_sig(sig, pubkey, sig_version, opts = {})
44
45
  return false unless [:taproot, :tapscript].include?(sig_version)
45
- return false if prevouts.size < input_index
46
+ # A taproot sighash commits to the amount and scriptPubkey of every prevout, so a partial
47
+ # set does not fail to verify, it verifies against a sighash consensus never computes.
48
+ # Checked here rather than left to the generator, since the rescue below would turn it
49
+ # into a script error.
50
+ raise ArgumentError, 'prevouts must be specified for all inputs.' unless prevouts.size == tx.in.size
46
51
 
47
52
  sig = sig.htb
48
53
  return set_error(SCRIPT_ERR_SCHNORR_SIG_SIZE) unless [64, 65].include?(sig.bytesize)
@@ -6,7 +6,8 @@ module Bitcoin
6
6
  module Secp256k1
7
7
 
8
8
  # binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
9
- # tag: v0.4.0
9
+ # tag: v0.8.0, which secp256k1rb 0.8.0 binds the public API of. An older library is missing
10
+ # symbols the gem attaches, such as secp256k1_ec_pubkey_sort and the musig module.
10
11
  # this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
11
12
  # for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so' or '/usr/lib64/libsecp256k1.so'
12
13
  # for mac,
@@ -98,6 +99,54 @@ module Bitcoin
98
99
  raise ArgumentError, "unknown algo: #{algo}"
99
100
  end
100
101
  end
102
+
103
+ # Whether the loaded library supports BIP-352 silent payments.
104
+ # The module exists in libsecp256k1 v0.8.0 or later.
105
+ # @return [Boolean]
106
+ def sp_available?
107
+ silentpayments_available?
108
+ end
109
+
110
+ # Create the silent payment outputs for +recipients+.
111
+ # @param [Array] recipients An array of [scan public key, spend public key], both with hex
112
+ # format(33 bytes). A recipient may appear more than once.
113
+ # @param [String] outpoint_smallest The lexicographically smallest outpoint of the tx inputs
114
+ # with hex format(36 bytes).
115
+ # @param [Array] plain_seckeys Private keys of the non-taproot inputs with hex format.
116
+ # @param [Array] taproot_seckeys Private keys of the taproot inputs with hex format.
117
+ # @return [Array] An x-only public key with hex format for each recipient, in the same order.
118
+ # @raise [Secp256k1::Error] If the outputs could not be created.
119
+ def sp_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
120
+ silentpayments_sender_create_outputs(
121
+ recipients, outpoint_smallest, plain_seckeys: plain_seckeys, taproot_seckeys: taproot_seckeys)
122
+ end
123
+
124
+ # Create the label and the label tweak of the +m+ th label of +scan_key+.
125
+ # @param [String] scan_key The recipient's scan private key with hex format(32 bytes).
126
+ # @param [Integer] m The label index.
127
+ # @return [Array] The serialized label(33 bytes) and its tweak(32 bytes), both with hex format.
128
+ def sp_create_label(scan_key, m)
129
+ silentpayments_create_label(scan_key, m)
130
+ end
131
+
132
+ # Scan +tx_outputs+ for the silent payment outputs of the recipient.
133
+ # @param [Array] tx_outputs The x-only public key of each taproot output with hex format.
134
+ # @param [String] scan_key The recipient's scan private key with hex format(32 bytes).
135
+ # @param [String] outpoint_smallest The lexicographically smallest outpoint of the tx inputs
136
+ # with hex format(36 bytes).
137
+ # @param [String] spend_pubkey The recipient's spend public key with hex format(33 bytes).
138
+ # @param [Array] plain_pubkeys Public keys of the non-taproot inputs with hex format(33 bytes).
139
+ # @param [Array] xonly_pubkeys X-only public keys of the taproot inputs with hex format.
140
+ # @param [Hash] labels A serialized label to label tweak map, both with hex format.
141
+ # @return [Array] A hash per found output, with the :output, :tweak and :label keys.
142
+ # @raise [Secp256k1::Error] If the tx is not a silent payment transaction.
143
+ def sp_scan_outputs(tx_outputs, scan_key, outpoint_smallest, spend_pubkey,
144
+ plain_pubkeys: [], xonly_pubkeys: [], labels: {})
145
+ summary = silentpayments_create_prevouts_summary(
146
+ outpoint_smallest, plain_pubkeys: plain_pubkeys, xonly_pubkeys: xonly_pubkeys)
147
+ silentpayments_scan_outputs(
148
+ tx_outputs, scan_key, summary, spend_pubkey, labels: labels.empty? ? nil : labels)
149
+ end
101
150
  end
102
151
  end
103
152
  end
@@ -45,7 +45,7 @@ module Bitcoin
45
45
  return false unless pubkey.bytesize == X_ONLY_PUBKEY_SIZE
46
46
  begin
47
47
  ECDSA::Format::PointOctetString.decode(pubkey, ECDSA::Group::Secp256k1)
48
- rescue Exception
48
+ rescue StandardError
49
49
  return false
50
50
  end
51
51
  true
@@ -201,7 +201,7 @@ module Bitcoin
201
201
  k = ECDSA::Format::PointOctetString.decode(repack_pubkey(pubkey), GROUP)
202
202
  signature = ECDSA::Format::SignatureDerString.decode(sig)
203
203
  ECDSA.valid_signature?(k, data, signature)
204
- rescue Exception
204
+ rescue StandardError
205
205
  false
206
206
  end
207
207
  end
@@ -210,6 +210,119 @@ module Bitcoin
210
210
  Schnorr.valid_sig?(data, pubkey.htb, sig)
211
211
  end
212
212
 
213
+ # Whether this module supports BIP-352 silent payments.
214
+ # @return [Boolean]
215
+ def sp_available?
216
+ true
217
+ end
218
+
219
+ # Create the silent payment outputs for +recipients+.
220
+ # See Bitcoin::Secp256k1::Native#sp_create_outputs for the parameters.
221
+ # @return [Array] An x-only public key with hex format for each recipient, in the same order.
222
+ # @raise [ArgumentError] If the input private keys sum to zero.
223
+ def sp_create_outputs(recipients, outpoint_smallest, plain_seckeys: [], taproot_seckeys: [])
224
+ field = ECDSA::PrimeField.new(GROUP.order)
225
+ sum = sp_sum_seckeys(plain_seckeys, taproot_seckeys, field)
226
+ raise ArgumentError, 'The input private keys sum to zero.' if sum.zero?
227
+ agg_pubkey = (GROUP.generator.to_jacobian * sum).to_affine
228
+ input_hash = Bitcoin.tagged_hash('BIP0352/Inputs', outpoint_smallest.htb + agg_pubkey.to_hex.htb)
229
+
230
+ # k counts up within the group of recipients sharing a scan key, but an output keeps the
231
+ # position of the recipient it pays.
232
+ groups = {}
233
+ recipients.each_with_index do |(scan_pubkey, spend_pubkey), index|
234
+ (groups[scan_pubkey] ||= []) << [spend_pubkey, index]
235
+ end
236
+ results = Array.new(recipients.length)
237
+ groups.each do |scan_pubkey, spends|
238
+ scan_point = Bitcoin::Key.new(pubkey: scan_pubkey).to_point.to_jacobian
239
+ shared_secret = (scan_point * field.mod(input_hash.bti * sum)).to_affine.to_hex.htb
240
+ spends.each_with_index do |(spend_pubkey, index), k|
241
+ t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', shared_secret + [k].pack('N'))
242
+ spend_point = Bitcoin::Key.new(pubkey: spend_pubkey).to_point.to_jacobian
243
+ output = (spend_point + GROUP.generator.to_jacobian * t_k.bti).to_affine
244
+ results[index] = sp_xonly(output.x)
245
+ end
246
+ end
247
+ results
248
+ end
249
+
250
+ # Create the label and the label tweak of the +m+ th label of +scan_key+.
251
+ # See Bitcoin::Secp256k1::Native#sp_create_label for the parameters.
252
+ # @return [Array] The serialized label(33 bytes) and its tweak(32 bytes), both with hex format.
253
+ def sp_create_label(scan_key, m)
254
+ tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_key.htb + [m].pack('N'))
255
+ [(GROUP.generator.to_jacobian * tweak.bti).to_affine.to_hex(true), tweak.bth]
256
+ end
257
+
258
+ # Scan +tx_outputs+ for the silent payment outputs of the recipient.
259
+ # See Bitcoin::Secp256k1::Native#sp_scan_outputs for the parameters.
260
+ # @return [Array] A hash per found output, with the :output, :tweak and :label keys.
261
+ # @raise [ArgumentError] If the input public keys sum to the point at infinity.
262
+ def sp_scan_outputs(tx_outputs, scan_key, outpoint_smallest, spend_pubkey,
263
+ plain_pubkeys: [], xonly_pubkeys: [], labels: {})
264
+ field = ECDSA::PrimeField.new(GROUP.order)
265
+ sum_pubkeys = GROUP.infinity.to_jacobian
266
+ plain_pubkeys.each { |p| sum_pubkeys += Bitcoin::Key.new(pubkey: p).to_point.to_jacobian }
267
+ xonly_pubkeys.each { |p| sum_pubkeys += Bitcoin::Key.from_xonly_pubkey(p).to_point.to_jacobian }
268
+ raise ArgumentError, 'The input public keys sum to the point at infinity.' if sum_pubkeys.infinity?
269
+
270
+ input_hash = Bitcoin.tagged_hash(
271
+ 'BIP0352/Inputs', outpoint_smallest.htb + sum_pubkeys.to_affine.to_hex.htb)
272
+ shared_secret = (sum_pubkeys * field.mod(input_hash.bti * scan_key.to_i(16))).to_affine.to_hex.htb
273
+ spend_point = Bitcoin::Key.new(pubkey: spend_pubkey).to_point.to_jacobian
274
+ # A labeled output is P_k + label. Only the x coordinate is compared, which covers the
275
+ # label of either parity without negating the output.
276
+ label_points = labels.map do |label, tweak|
277
+ [label, tweak, Bitcoin::Key.new(pubkey: label).to_point.to_jacobian]
278
+ end
279
+
280
+ results = []
281
+ remaining = tx_outputs.map(&:downcase)
282
+ k = 0
283
+ while k < Bitcoin::SilentPayment::K_MAX
284
+ t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', shared_secret + [k].pack('N'))
285
+ p_k = GROUP.generator.to_jacobian * t_k.bti + spend_point
286
+ index = remaining.index(sp_xonly(p_k.to_affine.x))
287
+ found = if index
288
+ {output: remaining.delete_at(index), tweak: t_k.bth, label: nil}
289
+ else
290
+ sp_find_labeled(p_k, remaining, label_points, t_k, field)
291
+ end
292
+ break unless found
293
+ results << found
294
+ k += 1
295
+ end
296
+ results
297
+ end
298
+
299
+ # Sum the private keys of the inputs, negating a taproot key whose public key has odd y.
300
+ def sp_sum_seckeys(plain_seckeys, taproot_seckeys, field)
301
+ sum = plain_seckeys.inject(0) { |total, sk| field.mod(total + sk.to_i(16)) }
302
+ taproot_seckeys.inject(sum) do |total, sk|
303
+ d = sk.to_i(16)
304
+ d = field.mod(-d) unless (GROUP.generator.to_jacobian * d).to_affine.has_even_y?
305
+ field.mod(total + d)
306
+ end
307
+ end
308
+
309
+ # Find the output +p_k+ pays through one of +label_points+, or nil.
310
+ def sp_find_labeled(p_k, remaining, label_points, t_k, field)
311
+ label_points.each do |label, tweak, point|
312
+ index = remaining.index(sp_xonly((p_k + point).to_affine.x))
313
+ next unless index
314
+ return {output: remaining.delete_at(index),
315
+ tweak: sp_xonly(field.mod(t_k.bti + tweak.to_i(16))),
316
+ label: label}
317
+ end
318
+ nil
319
+ end
320
+
321
+ # Serialize a field element as a 32 byte value with hex format.
322
+ def sp_xonly(value)
323
+ ECDSA::Format::IntegerOctetString.encode(value, 32).bth
324
+ end
325
+
213
326
  end
214
327
  end
215
328
  end
@@ -107,8 +107,14 @@ module Bitcoin
107
107
  # - leaf_hash: leaf hash with binary format if sig_version is :tapscript, it required
108
108
  # - last_code_separator_pos: the position of last code separator
109
109
  # @return [String] signature hash with binary format.
110
+ # @raise [ArgumentError] If +opts[:prevouts]+ does not cover every input of the tx.
110
111
  def generate(tx, input_index, hash_type, opts)
111
112
  raise ArgumentError, 'Invalid sig_version was specified.' unless [:taproot, :tapscript].include?(opts[:sig_version])
113
+ # sha_amounts and sha_scriptpubkeys commit to every prevout, so a partial set silently
114
+ # produces a sighash which is not the one consensus computes for this tx.
115
+ unless opts[:prevouts].is_a?(Array) && opts[:prevouts].size == tx.in.size
116
+ raise ArgumentError, 'prevouts must be specified for all inputs.'
117
+ end
112
118
 
113
119
  ext_flag = opts[:sig_version] == :taproot ? 0 : 1
114
120
  key_version = 0
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  module Bitcoin
4
2
  # BIP-352 silent payment module.
5
3
  # @see https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
@@ -16,7 +14,7 @@ module Bitcoin
16
14
  # @param [Array<Bitcoin::Script>] prevouts An array of previous output script.
17
15
  # @param [Array<Bitcoin::Key>] private_keys An array of Bitcoin::Key objects corresponding to each public key in prevouts.
18
16
  # @param [Array<Bech32::SilentPaymentAddr>] recipients
19
- # @return [Array<ECDSA::Point>] An array of derived points.
17
+ # @return [Array<ECDSA::Point>] An array of derived points, one per recipient in the same order.
20
18
  # @raise [ArgumentError]
21
19
  def derive_payment_points(prevouts, private_keys, recipients)
22
20
  raise ArgumentError, "prevouts must be Array." unless prevouts.is_a? Array
@@ -24,53 +22,41 @@ module Bitcoin
24
22
  raise ArgumentError, "prevouts and private_keys must be the same length." unless prevouts.length == private_keys.length
25
23
  raise ArgumentError, "recipients must be Array." unless recipients.is_a? Array
26
24
 
27
- input_pub_keys = []
28
25
  field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
26
+ plain_seckeys = []
27
+ taproot_seckeys = []
29
28
  sum_priv_keys = 0
30
29
  prevouts.each_with_index do |prevout, index|
31
30
  key = private_keys[index]
32
31
  raise ArgumentError, "private_keys element must be Bitcoin::Key." unless key.is_a? Bitcoin::Key
33
- priv_key_int = key.priv_key.to_i(16)
34
32
  public_key = extract_public_key(prevout, inputs[index])
35
33
  next if public_key.nil?
36
- private_key = if public_key.p2tr? && key.to_point.y.odd?
37
- field.mod(-priv_key_int)
38
- else
39
- priv_key_int
40
- end
41
- input_pub_keys << public_key
42
- sum_priv_keys = field.mod(sum_priv_keys + private_key)
34
+ priv_key_int = key.priv_key.to_i(16)
35
+ if public_key.p2tr?
36
+ taproot_seckeys << key.priv_key
37
+ priv_key_int = field.mod(-priv_key_int) unless key.to_point.has_even_y?
38
+ else
39
+ plain_seckeys << key.priv_key
40
+ end
41
+ sum_priv_keys = field.mod(sum_priv_keys + priv_key_int)
43
42
  end
44
- agg_pubkey = (Bitcoin::Secp256k1::GROUP.generator.to_jacobian * sum_priv_keys).to_affine
45
- return [] if agg_pubkey.infinity?
46
-
47
- outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
48
-
49
- input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + agg_pubkey.to_hex.htb).bth
43
+ return [] if plain_seckeys.empty? && taproot_seckeys.empty?
44
+ # The input private keys sum to zero, so the aggregate public key is the point at infinity
45
+ # and no shared secret exists.
46
+ return [] if sum_priv_keys.zero?
50
47
 
51
- destinations = {}
52
- recipients.each do |sp_addr|
48
+ destinations = recipients.map do |sp_addr|
53
49
  raise ArgumentError, "recipients element must be Bech32::SilentPaymentAddr." unless sp_addr.is_a? Bech32::SilentPaymentAddr
54
- destinations[sp_addr.scan_key] = [] unless destinations.has_key?(sp_addr.scan_key)
55
- destinations[sp_addr.scan_key] << sp_addr.spend_key
50
+ [sp_addr.scan_key, sp_addr.spend_key]
56
51
  end
57
-
58
- # Check K_max limit: fail if any group exceeds the limit
59
- destinations.each_value do |spends|
60
- raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if spends.length > K_MAX
52
+ destinations.group_by(&:first).each_value do |group|
53
+ raise ArgumentError, "Recipient group exceeds K_max limit (#{K_MAX})." if group.length > K_MAX
61
54
  end
62
55
 
63
- outputs = []
64
- destinations.each do |scan_key, spends|
65
- scan_key = Bitcoin::Key.new(pubkey: scan_key).to_point.to_jacobian
66
- ecdh_shared_secret = (scan_key * field.mod(input_hash.to_i(16) * sum_priv_keys)).to_affine.to_hex.htb
67
- spends.each.with_index do |spend, i|
68
- t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [i].pack('N'))
69
- spend_key = Bitcoin::Key.new(pubkey: spend).to_point.to_jacobian
70
- outputs << (spend_key + Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bth.to_i(16)).to_affine
71
- end
72
- end
73
- outputs
56
+ Bitcoin.secp_impl.sp_create_outputs(
57
+ destinations, sp_outpoint_smallest,
58
+ plain_seckeys: plain_seckeys, taproot_seckeys: taproot_seckeys
59
+ ).map { |xonly| Bitcoin::Key.from_xonly_pubkey(xonly).to_point }
74
60
  end
75
61
 
76
62
 
@@ -87,8 +73,11 @@ module Bitcoin
87
73
  raise ArgumentError, "scan_private_key must be Bitcoin::Key." unless scan_private_key.is_a? Bitcoin::Key
88
74
  raise ArgumentError, "spend_pubkey must be Bitcoin::Key." unless spend_pubkey.is_a? Bitcoin::Key
89
75
 
90
- has_taproot = !outputs.find{|o| o.script_pubkey.p2tr? }.nil?
91
- return [] unless has_taproot
76
+ taproot_outputs = outputs.select{|o| o.script_pubkey.p2tr? }
77
+ return [] if taproot_outputs.empty?
78
+
79
+ plain_pubkeys = []
80
+ xonly_pubkeys = []
92
81
  sum_pub_keys = Bitcoin::Secp256k1::GROUP.infinity.to_jacobian
93
82
  maximum_witness_version = Bitcoin::Opcodes.opcode_to_small_int(Bitcoin::Opcodes::OP_1)
94
83
  prevouts.each.with_index do |prevout, index|
@@ -96,64 +85,41 @@ module Bitcoin
96
85
 
97
86
  public_key = extract_public_key(prevout, inputs[index])
98
87
  next if public_key.nil?
88
+ if public_key.p2tr?
89
+ xonly_pubkeys << public_key.xonly_pubkey
90
+ else
91
+ plain_pubkeys << public_key.pubkey
92
+ end
99
93
  sum_pub_keys += public_key.to_point.to_jacobian
100
94
  end
95
+ return [] if plain_pubkeys.empty? && xonly_pubkeys.empty?
96
+ # Not a silent payment transaction, so there is nothing to find. Checked here rather than
97
+ # left to the implementation, which reports it as an error.
101
98
  return [] if sum_pub_keys.infinity?
102
99
 
103
- field = ECDSA::PrimeField.new(Bitcoin::Secp256k1::GROUP.order)
104
- outpoint_l = inputs.map{|i|i.out_point.to_hex}.sort.first
105
- input_hash = Bitcoin.tagged_hash("BIP0352/Inputs", outpoint_l.htb + sum_pub_keys.to_affine.to_hex.htb).bth
106
- ecdh_shared_secret = (sum_pub_keys * field.mod(input_hash.to_i(16) * scan_private_key.priv_key.to_i(16))).to_affine.to_hex.htb
107
-
108
- # Pre-compute label tweak points with their label values and scalar tweaks
109
- label_tweaks = labels.map do |m|
110
- label_tweak = Bitcoin.tagged_hash('BIP0352/Label', scan_private_key.priv_key.htb + [m].pack('N'))
111
- label_point = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * label_tweak.bti
112
- [m, label_tweak, label_point]
100
+ impl = Bitcoin.secp_impl
101
+ label_values = {}
102
+ label_tweaks = {}
103
+ labels.each do |m|
104
+ label, tweak = impl.sp_create_label(scan_private_key.priv_key, m)
105
+ label_values[label] = m
106
+ label_tweaks[label] = tweak
113
107
  end
114
108
 
115
- k = 0
116
- results = []
117
- found_outputs = Set.new
118
- loop do
119
- # Stop scanning if K_max limit is reached
120
- break if k == K_MAX
121
-
122
- t_k = Bitcoin.tagged_hash('BIP0352/SharedSecret', ecdh_shared_secret + [k].pack('N'))
123
- p_k = Bitcoin::Secp256k1::GROUP.generator.to_jacobian * t_k.bti + spend_pubkey.to_point.to_jacobian
124
- found = false
125
- outputs.each do |output|
126
- next unless output.script_pubkey.p2tr?
127
- next if found_outputs.include?(output)
128
- output_pubkey = Bitcoin::Key.from_xonly_pubkey(output.script_pubkey.witness_data[1].bth)
129
-
130
- # Check basic match (no label)
131
- if p_k.to_affine.x == output_pubkey.to_point.x
132
- results << SilentPayment::Output.new(output, t_k)
133
- found_outputs << output
134
- k += 1
135
- found = true
136
- break
137
- end
138
-
139
- # Check labeled matches
140
- label_tweaks.each do |label_value, label_tweak_scalar, label_point|
141
- p_k_labeled = p_k + label_point
142
- if p_k_labeled.to_affine.x == output_pubkey.to_point.x
143
- # Full tweak is t_k + label_tweak (mod order)
144
- full_tweak = field.mod(t_k.bti + label_tweak_scalar.bti).to_s(16).rjust(64, '0').htb
145
- results << SilentPayment::Output.new(output, full_tweak, label_value)
146
- found_outputs << output
147
- k += 1
148
- found = true
149
- break
150
- end
151
- end
152
- break if found
153
- end
154
- break unless found
109
+ tx_outputs = taproot_outputs.map{|o| o.script_pubkey.witness_data[1].bth }
110
+ impl.sp_scan_outputs(tx_outputs, scan_private_key.priv_key, sp_outpoint_smallest, spend_pubkey.pubkey,
111
+ plain_pubkeys: plain_pubkeys, xonly_pubkeys: xonly_pubkeys,
112
+ labels: label_tweaks).map do |found|
113
+ tx_out = taproot_outputs[tx_outputs.index(found[:output])]
114
+ SilentPayment::Output.new(tx_out, found[:tweak].htb, found[:label] && label_values[found[:label]])
155
115
  end
156
- results
116
+ end
117
+
118
+ # The lexicographically smallest outpoint of this tx's inputs, which the input hash of
119
+ # BIP-352 commits to.
120
+ # @return [String] The outpoint with hex format(36 bytes).
121
+ def sp_outpoint_smallest
122
+ inputs.map{|i| i.out_point.to_hex }.min
157
123
  end
158
124
 
159
125
  # Extract public keys from +prevout+ and input.
@@ -35,14 +35,14 @@ module Bitcoin
35
35
 
36
36
  def merkle_root
37
37
  return '' if tree.empty?
38
- script_tree = Merkle::CustomTree.new(config: Merkle::Config.taptree, leaves: extract_leaves(tree))
38
+ script_tree = Merkle::CustomTree.new(config: Merkle::Config.taptree(element_encoding: :hex), leaves: extract_leaves(tree))
39
39
  script_tree.compute_root
40
40
  end
41
41
 
42
42
  def extract_leaves(leaves)
43
43
  leaves.map do |leaf|
44
44
  if leaf.is_a?(Bitcoin::Taproot::LeafNode)
45
- leaf.leaf_hash
45
+ leaf.leaf_hash.bth
46
46
  elsif leaf.is_a?(Array)
47
47
  extract_leaves(leaf)
48
48
  end
@@ -100,15 +100,17 @@ module Bitcoin
100
100
  private
101
101
 
102
102
  def script_tree
103
- leaves = []
103
+ # A node holding one child hashes to that child, so the tree rejects the shape as
104
+ # indistinguishable from the child itself. A branch of one leaf, and a tree of one
105
+ # branch, are therefore that leaf and that branch rather than a node above them.
106
+ tree = nil
104
107
  branches.each do |pair|
105
- if leaves.empty? || leaves.length == 1
106
- leaves << pair.map(&:leaf_hash)
107
- elsif leaves.length == 2
108
- leaves = [leaves, pair.map(&:leaf_hash)]
109
- end
108
+ hashes = pair.map { |leaf| leaf.leaf_hash.bth }
109
+ node = hashes.length == 1 ? hashes.first : hashes
110
+ tree = tree.nil? ? node : [tree, node]
110
111
  end
111
- Merkle::CustomTree.new(config: Merkle::Config.taptree, leaves: leaves)
112
+ leaves = tree.is_a?(Array) ? tree : [tree]
113
+ Merkle::CustomTree.new(config: Merkle::Config.taptree(element_encoding: :hex), leaves: leaves)
112
114
  end
113
115
 
114
116
  def merkle_root
@@ -33,7 +33,12 @@ module Bitcoin
33
33
  def tweak_public_key(internal_key, merkle_root)
34
34
  t = tweak(internal_key, merkle_root)
35
35
  key = Bitcoin::Key.new(priv_key: t.bth, key_type: Key::TYPES[:compressed])
36
- Bitcoin::Key.from_point(key.to_point + internal_key.to_point)
36
+ # BIP-341 tweaks lift_x(P), the point with even y. #tweak commits to the x-only key and
37
+ # #tweak_private_key negates the private key to match, so the point is lifted here too.
38
+ # An internal key with odd y would otherwise give a Q the tweaked private key cannot spend.
39
+ internal_point = internal_key.to_point
40
+ internal_point = internal_point.negate unless internal_point.has_even_y?
41
+ Bitcoin::Key.from_point(key.to_point + internal_point)
37
42
  end
38
43
 
39
44
  # Generate tweak private key
data/lib/bitcoin/util.rb CHANGED
@@ -152,7 +152,7 @@ module Bitcoin
152
152
  begin
153
153
  Bitcoin::Script.parse_from_addr(addr)
154
154
  true
155
- rescue Exception
155
+ rescue StandardError
156
156
  false
157
157
  end
158
158
  end
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.13.0"
2
+ VERSION = "1.14.0"
3
3
  end
@@ -8,15 +8,36 @@ module Bitcoin
8
8
  include Bitcoin::Util
9
9
  include Bitcoin::KeyPath
10
10
 
11
+ # The seed is not encrypted.
12
+ NOT_ENCRYPTED = 0
13
+ # AES-256-CBC with a key derived by PBKDF2-HMAC-SHA1. Only supported to load a wallet
14
+ # which was already encrypted, #encrypt always writes ENCRYPTED_V2.
15
+ ENCRYPTED_V1 = 1
16
+ # AES-256-GCM with a key derived by PBKDF2-HMAC-SHA512.
17
+ ENCRYPTED_V2 = 2
18
+
19
+ ENCRYPTION_VERSIONS = [ENCRYPTED_V1, ENCRYPTED_V2]
20
+
21
+ V1_KDF_ROUNDS = 2000
22
+ # Rounds for PBKDF2-HMAC-SHA512, which take roughly 100 ms.
23
+ KDF_ROUNDS = 210_000
24
+
25
+ SALT_SIZE = 16
26
+ GCM_IV_SIZE = 12
27
+ GCM_TAG_SIZE = 16
28
+
11
29
  attr_reader :seed
12
30
  attr_accessor :salt
13
31
  attr_accessor :encrypted
32
+ # The encryption version of +seed+, nil unless encrypted.
33
+ attr_reader :encryption_version
14
34
  attr_accessor :mnemonic # ephemeral data existing only at initialization
15
35
 
16
- def initialize(seed, salt: '', encrypted: false, mnemonic: nil)
36
+ def initialize(seed, salt: '', encrypted: false, mnemonic: nil, encryption_version: nil)
17
37
  @mnemonic = mnemonic
18
38
  @seed = seed
19
39
  @encrypted = encrypted
40
+ @encryption_version = encrypted ? (encryption_version || ENCRYPTED_V2) : nil
20
41
  @salt = salt
21
42
  end
22
43
 
@@ -42,17 +63,18 @@ module Bitcoin
42
63
  # @return [Bitcoin::Wallet::MasterKey]
43
64
  def self.parse_from_payload(payload)
44
65
  flag, payload = unpack_var_int(payload)
45
- raise 'encrypted flag is invalid.' unless [0, 1].include?(flag)
66
+ raise 'encrypted flag is invalid.' unless [NOT_ENCRYPTED, *ENCRYPTION_VERSIONS].include?(flag)
46
67
  salt, payload = unpack_var_string(payload)
47
68
  salt = '' unless salt
48
69
  seed, payload = unpack_var_string(payload)
49
- self.new(seed.bth, salt: salt.bth, encrypted: flag == 1)
70
+ self.new(seed.bth, salt: salt.bth,
71
+ encrypted: flag != NOT_ENCRYPTED, encryption_version: flag)
50
72
  end
51
73
 
52
74
  # generate payload with following format
53
- # [encrypted(false:0, true:1)][salt(var str)][seed(var str)]
75
+ # [encryption version(not encrypted:0, v1:1, v2:2)][salt(var str)][seed(var str)]
54
76
  def to_payload
55
- flg = encrypted ? 1 : 0
77
+ flg = encrypted ? encryption_version : NOT_ENCRYPTED
56
78
  pack_var_int(flg) << [salt, seed].map{|v|pack_var_string(v.htb)}.join
57
79
  end
58
80
 
@@ -71,39 +93,76 @@ module Bitcoin
71
93
  derived_key
72
94
  end
73
95
 
74
- # encrypt seed
96
+ # Encrypt the seed with +passphrase+.
97
+ # The stored seed is [IV(12 bytes)][auth tag(16 bytes)][ciphertext].
98
+ # @param [String] passphrase
75
99
  def encrypt(passphrase)
76
100
  raise 'The wallet is already encrypted.' if encrypted
77
- @salt = SecureRandom.hex(16)
78
- enc = OpenSSL::Cipher.new('AES-256-CBC')
101
+ salt = SecureRandom.bytes(SALT_SIZE)
102
+ iv = SecureRandom.bytes(GCM_IV_SIZE)
103
+ enc = OpenSSL::Cipher.new('AES-256-GCM')
79
104
  enc.encrypt
80
- enc.key, enc.iv = key_iv(enc, passphrase)
81
- encrypted_data = ''
82
- encrypted_data << enc.update(seed)
83
- encrypted_data << enc.final
84
- @seed = encrypted_data.bth
105
+ enc.key = derive_key(passphrase, salt, enc.key_len)
106
+ enc.iv = iv
107
+ encrypted_data = enc.update(seed) << enc.final
108
+ @salt = salt.bth
109
+ @seed = (iv + enc.auth_tag(GCM_TAG_SIZE) + encrypted_data).bth
110
+ @encryption_version = ENCRYPTED_V2
85
111
  @encrypted = true
86
112
  end
87
113
 
88
- # decrypt seed
114
+ # Decrypt the seed with +passphrase+.
115
+ # @param [String] passphrase
116
+ # @raise [ArgumentError] If +passphrase+ is wrong or the encrypted seed is corrupted.
89
117
  def decrypt(passphrase)
90
118
  raise 'The wallet is not encrypted.' unless encrypted
91
- dec = OpenSSL::Cipher.new('AES-256-CBC')
92
- dec.decrypt
93
- dec.key, dec.iv = key_iv(dec, passphrase)
94
- decrypted_data = ''
95
- decrypted_data << dec.update(seed.htb)
96
- decrypted_data << dec.final
97
- @seed = decrypted_data
119
+ @seed = encryption_version == ENCRYPTED_V1 ? decrypt_v1(passphrase) : decrypt_v2(passphrase)
98
120
  @encrypted = false
121
+ @encryption_version = nil
99
122
  @salt = ''
100
123
  end
101
124
 
102
125
  private
103
126
 
104
- def key_iv(enc, passphrase)
105
- key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, 2000, enc.key_len + enc.iv_len)
106
- [key_iv[0, enc.key_len], key_iv[enc.key_len, enc.iv_len]]
127
+ # Derive an encryption key from +passphrase+.
128
+ # @param [String] passphrase
129
+ # @param [String] salt Salt with binary format.
130
+ # @param [Integer] length Key length in bytes.
131
+ # @return [String] Derived key with binary format.
132
+ def derive_key(passphrase, salt, length)
133
+ OpenSSL::PKCS5.pbkdf2_hmac(passphrase, salt, KDF_ROUNDS, length, OpenSSL::Digest::SHA512.new)
134
+ end
135
+
136
+ # Decrypt a seed stored by ENCRYPTED_V2.
137
+ # A wrong passphrase is rejected by the GCM authentication tag.
138
+ def decrypt_v2(passphrase)
139
+ data = seed.htb
140
+ raise ArgumentError, 'The encrypted seed is too short.' if data.bytesize <= GCM_IV_SIZE + GCM_TAG_SIZE
141
+ dec = OpenSSL::Cipher.new('AES-256-GCM')
142
+ dec.decrypt
143
+ dec.key = derive_key(passphrase, salt.htb, dec.key_len)
144
+ dec.iv = data[0...GCM_IV_SIZE]
145
+ dec.auth_tag = data[GCM_IV_SIZE...(GCM_IV_SIZE + GCM_TAG_SIZE)]
146
+ decrypt_with(dec, data[(GCM_IV_SIZE + GCM_TAG_SIZE)..-1])
147
+ end
148
+
149
+ # Decrypt a seed stored by ENCRYPTED_V1.
150
+ # That scheme has no authentication, so a wrong passphrase is only detected by the CBC
151
+ # padding and passes with a probability of about 1/256, yielding a garbage seed.
152
+ def decrypt_v1(passphrase)
153
+ dec = OpenSSL::Cipher.new('AES-256-CBC')
154
+ dec.decrypt
155
+ # The salt was passed to PBKDF2 as the hex string it is stored as, not as its bytes.
156
+ key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, V1_KDF_ROUNDS, dec.key_len + dec.iv_len)
157
+ dec.key = key_iv[0, dec.key_len]
158
+ dec.iv = key_iv[dec.key_len, dec.iv_len]
159
+ decrypt_with(dec, seed.htb)
160
+ end
161
+
162
+ def decrypt_with(dec, data)
163
+ dec.update(data) << dec.final
164
+ rescue OpenSSL::Cipher::CipherError
165
+ raise ArgumentError, 'Invalid passphrase.'
107
166
  end
108
167
 
109
168
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitcoinrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - '='
115
115
  - !ruby/object:Gem::Version
116
- version: 0.1.1
116
+ version: 0.8.0
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - '='
122
122
  - !ruby/object:Gem::Version
123
- version: 0.1.1
123
+ version: 0.8.0
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: logger
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -141,14 +141,14 @@ dependencies:
141
141
  requirements:
142
142
  - - '='
143
143
  - !ruby/object:Gem::Version
144
- version: 0.3.0
144
+ version: 1.0.0
145
145
  type: :runtime
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - '='
150
150
  - !ruby/object:Gem::Version
151
- version: 0.3.0
151
+ version: 1.0.0
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: dnsruby
154
154
  requirement: !ruby/object:Gem::Requirement