bitcoinrb 1.14.0 → 1.15.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: 360a1a8533df9b68228d18279909e7bc5f45d4d5679caa1a02cda75e0a9140d0
4
- data.tar.gz: ff86fcec32b3fe1394eea043080aaedafee61e724ec689cde5efdd7b9ca5c307
3
+ metadata.gz: 5da84e4a456d4b5a47fba7e5cfcfceabbfbdb9fe757f6080519130d2ebabc41c
4
+ data.tar.gz: 903301aa0eb0f349bc08c7133ebe80b9c731646fece6a407b75ace1bf6161aa8
5
5
  SHA512:
6
- metadata.gz: 9aeb17cc9dc42ce33c4c31ed1f6424569d5c0f4f20627015c930d11e5ba2bf9c15f43cd20a666257caba689ffc0010b08b5855c13a3cf61a724c05fe8bcd383d
7
- data.tar.gz: 14848d9c37a69ec5b0d2f993ce92c3cad3bdfc6275151789ff768ad5989d101f9c5545f1dc668d372f847b574fdc60d10e5f90b81b377b23ac20bfb3705b0a3a
6
+ metadata.gz: 39a539b3380c6dba3ac091a2b3706508e790d16d4367ff91f715c9804ac5803dc56893f5aaf4c3bc4dec71d4a1c03a2d4da481e0b060c291a913a96cc3a7f1db
7
+ data.tar.gz: dec61d2d90e67665aa4277de65b68a86c00779b820d5980e8d2facc85d0a0a4ba5c10989a43982fafc88463f40c02dfbfd3421b6f89f3e08620af4c008fa22fd
data/lib/bitcoin/key.rb CHANGED
@@ -192,10 +192,17 @@ module Bitcoin
192
192
  Bitcoin::Script.to_p2wpkh(hash160).to_addr
193
193
  end
194
194
 
195
- # Get pay to taproot address
195
+ # Get pay to taproot address.
196
+ # By default the x-only public key of this key becomes the output key as it is, which is
197
+ # what the rawtr() descriptor does. With +as_internal+ this key is the internal key P and
198
+ # the output key is Q = P + hash_TapTweak(P)G, which is what the tr() descriptor does and
199
+ # what BIP-86 defines for a single key address. Spending such an output needs the tweaked
200
+ # private key, see Bitcoin::Taproot.tweak_private_key.
201
+ # @param [Boolean] as_internal Whether to treat this key as the internal key. Default is false.
196
202
  # @return [String] address
197
- def to_p2tr
198
- Bitcoin::Script.to_p2tr(self).to_addr
203
+ def to_p2tr(as_internal: false)
204
+ output_key = as_internal ? Bitcoin::Taproot.tweak_public_key(self, '') : self
205
+ Bitcoin::Script.to_p2tr(output_key).to_addr
199
206
  end
200
207
 
201
208
  # get p2wpkh address nested in p2sh.
@@ -33,6 +33,7 @@ module Bitcoin
33
33
  # @param [String] format Format of signature data. Default is +FORMAT_LEGACY+.
34
34
  # @param [String] prefix (Optional) Prefix used in legacy format.
35
35
  # @return [String] Signature, base64 encoded.
36
+ # @raise [ArgumentError] If +address+ is not supported, or +key+ does not correspond to it.
36
37
  def sign_message(key, message, prefix: Bitcoin.chain_params.message_magic, format: FORMAT_LEGACY, address: nil)
37
38
  validate_format!(format)
38
39
  digest = message_hash(message, prefix: prefix, legacy: format == FORMAT_LEGACY)
@@ -43,20 +44,15 @@ module Bitcoin
43
44
  else
44
45
  validate_address!(address)
45
46
  addr = Bitcoin::Script.parse_from_addr(address)
46
- sig_ver, algo = if addr.p2wpkh? || addr.p2wsh?
47
- [:witness_v0, :ecdsa]
48
- elsif addr.p2tr?
49
- [:taproot, :schnorr]
50
- else
51
-
52
- end
53
47
  tx = to_sign_tx(digest, address)
54
48
  prev_out = Bitcoin::TxOut.new(script_pubkey: addr, value: 0)
55
49
  if addr.p2tr?
56
- sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
57
50
  tweaked = Bitcoin::Taproot.tweak_private_key(key, '')
51
+ validate_key!(Bitcoin::Script.to_p2tr(tweaked), addr, address)
52
+ sighash = tx.sighash_for_input(0, addr, sig_version: :taproot, prevouts: [prev_out], hash_type: Bitcoin::SIGHASH_TYPE[:default])
58
53
  tx.in[0].script_witness.stack << tweaked.sign(sighash, algo: :schnorr)
59
- elsif addr.p2wpkh? || addr.p2wsh?
54
+ elsif addr.p2wpkh?
55
+ validate_key!(Bitcoin::Script.to_p2wpkh(key.hash160), addr, address)
60
56
  sighash = tx.sighash_for_input(0, addr, sig_version: :witness_v0, amount: 0, prevouts: [prev_out])
61
57
  ecdsa = key.sign(sighash, algo: :ecdsa) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
62
58
  tx.in[0].script_witness.stack << ecdsa
@@ -128,6 +124,23 @@ module Bitcoin
128
124
  Bitcoin::Script.parse_from_addr(address)
129
125
  end
130
126
 
127
+ # Ensure the signing key can satisfy +address+.
128
+ # A key the address does not commit to still produces a signature, and BIP-322 carries
129
+ # nothing that would report it verifies against no one.
130
+ # @param [Bitcoin::Script] derived The script the signing key produces.
131
+ # @param [Bitcoin::Script] expected The script of the address to be signed for.
132
+ # @param [String] address The address to be signed for, for the error message.
133
+ # @raise [ArgumentError] If the key does not correspond to +address+.
134
+ def validate_key!(derived, expected, address)
135
+ return if derived == expected
136
+ message = "Key does not correspond to #{address}."
137
+ if expected.p2tr?
138
+ message += ' A P2TR address holds the output key, which is the internal key this key' \
139
+ ' holds tweaked with an empty merkle root. See Bitcoin::Key#to_p2tr.'
140
+ end
141
+ raise ArgumentError, message
142
+ end
143
+
131
144
  def validate_format!(format)
132
145
  unless [FORMAT_LEGACY, FORMAT_FULL, FORMAT_SIMPLE].include?(format)
133
146
  raise ArgumentError "Invalid format specified."
@@ -197,6 +210,7 @@ module Bitcoin
197
210
  end
198
211
 
199
212
  private_class_method :validate_address!
213
+ private_class_method :validate_key!
200
214
  private_class_method :validate_format!
201
215
  private_class_method :validate_to_sign_tx!
202
216
  private_class_method :run_interpreter
@@ -46,7 +46,7 @@ module Bitcoin
46
46
  end_index = start_index + value_length - padding_length
47
47
  padding_value = data[40...(40 + padding_length)]
48
48
  raise ArgumentError, "Invalid mnemonic. padding must only zero." unless padding_value.to_i(2) == 0
49
- s.value = data[start_index...end_index].to_i(2).to_even_length_hex
49
+ s.value = data[start_index...end_index].to_i(2).to_s(16).rjust((value_length - padding_length) / 4, '0')
50
50
  s.checksum = data[(40 + value_length)..-1].to_i(2)
51
51
  s
52
52
  end
@@ -92,8 +92,12 @@ module Bitcoin
92
92
  raise StandardError, "Group threshold(#{group_threshold}) cannot be greater than group count(#{group_count})." if group_threshold > group_count
93
93
  s << member_index.to_bits(4)
94
94
  s << (member_threshold - 1).to_bits(4)
95
- value_length = value.to_i(16).bit_length
96
- padding_length = RADIX_BITS - (value_length % RADIX_BITS)
95
+ # from_words derives the padding length from the number of value words modulo 16, which only
96
+ # matches the padding written here when the value is an even number of bytes. An odd one is
97
+ # read back as a different value instead of being rejected.
98
+ raise ArgumentError, 'The length of the share value in bytes must be an even number.' unless value.htb.bytesize.even?
99
+ value_length = value.htb.bytesize * 8
100
+ padding_length = (RADIX_BITS - (value_length % RADIX_BITS)) % RADIX_BITS
97
101
  s << value.to_i(16).to_bits(value_length + padding_length)
98
102
  s << checksum.to_bits(30) if include_checksum
99
103
  s.chars.each_slice(10).map{|index| index.join.to_i(2)}
@@ -81,8 +81,10 @@ module Bitcoin
81
81
  #
82
82
  # @param [Array[Bitcoin::SLIP30::Share]] shares an array of shares.
83
83
  # @param [String] passphrase the passphrase using decrypt master secret.
84
+ # @param [Boolean] legacy_salt if true, build the PBKDF2 salt with the minimum number of bytes for the
85
+ # identifier. Shares generated by bitcoinrb 1.14.0 and earlier need this to be recovered.
84
86
  # @return [String] a master secret.
85
- def self.recover_secret(shares, passphrase: '')
87
+ def self.recover_secret(shares, passphrase: '', legacy_salt: false)
86
88
  raise ArgumentError, 'share is empty.' if shares.nil? || shares.empty?
87
89
  groups = {}
88
90
  id = shares[0].id
@@ -127,7 +129,7 @@ module Bitcoin
127
129
  end
128
130
  end
129
131
 
130
- return decrypt(group_shares.values.first, passphrase, exp, id) if group_threshold == 1
132
+ return decrypt(group_shares.values.first, passphrase, exp, id, legacy_salt) if group_threshold == 1
131
133
 
132
134
  raise ArgumentError, "Wrong number of mnemonics. Group threshold is #{group_threshold}, but share count is #{group_shares.length}" if group_shares.length < group_threshold
133
135
 
@@ -138,7 +140,7 @@ module Bitcoin
138
140
  recover_digest = create_digest(secret, random_value)
139
141
  raise ArgumentError, 'Invalid digest of the shared secret.' unless digest == recover_digest
140
142
 
141
- decrypt(secret, passphrase, exp, id)
143
+ decrypt(secret, passphrase, exp, id, legacy_salt)
142
144
  end
143
145
 
144
146
  private
@@ -168,9 +170,10 @@ module Bitcoin
168
170
  # @param [String] passphrase the passphrase when using encrypt master secret with binary format.
169
171
  # @param [Integer] exp iteration exponent
170
172
  # @param [Integer] id identifier
171
- def self.decrypt(ems, passphrase, exp, id)
173
+ # @param [Boolean] legacy_salt if true, build the salt with the minimum number of bytes for the identifier.
174
+ def self.decrypt(ems, passphrase, exp, id, legacy_salt = false)
172
175
  l, r = ems[0...(ems.length / 2)].htb, ems[(ems.length / 2)..-1].htb
173
- salt = get_salt(id)
176
+ salt = get_salt(id, legacy_salt)
174
177
  e = (Bitcoin::SLIP39::BASE_ITERATION_COUNT << exp) / Bitcoin::SLIP39::ROUND_COUNT
175
178
  Bitcoin::SLIP39::ROUND_COUNT.times.to_a.reverse.each do |i|
176
179
  f = OpenSSL::PKCS5.pbkdf2_hmac((i.itb + passphrase), salt + r, e, r.bytesize, 'sha256')
@@ -208,9 +211,12 @@ module Bitcoin
208
211
 
209
212
  # get salt using encryption/decryption form id.
210
213
  # @param [Integer] id id
214
+ # @param [Boolean] legacy if true, encode the identifier with the minimum number of bytes. SLIP-39 requires
215
+ # two bytes, so a salt built this way only matches shares generated by bitcoinrb 1.14.0 and earlier.
211
216
  # @return [String] salt with binary format.
212
- def self.get_salt(id)
213
- (Bitcoin::SLIP39::CUSTOMIZATION_STRING.pack('c*') + id.itb)
217
+ def self.get_salt(id, legacy = false)
218
+ identifier = legacy ? id.itb : [id].pack('n')
219
+ (Bitcoin::SLIP39::CUSTOMIZATION_STRING.pack('c*') + identifier)
214
220
  end
215
221
 
216
222
  # Split the share into +count+ with threshold +threshold+.
@@ -1,3 +1,3 @@
1
1
  module Bitcoin
2
- VERSION = "1.14.0"
2
+ VERSION = "1.15.0"
3
3
  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.14.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi