Class: BlockIo::Helper

Inherits:
Object
  • Object
show all
Defined in:
lib/block_io/helper.rb

Constant Summary collapse

LEGACY_DECRYPTION_ALGORITHM =
{
  :pbkdf2_salt => "",
  :pbkdf2_iterations => 2048,
  :pbkdf2_hash_function => "SHA256",
  :pbkdf2_phase1_key_length => 16,
  :pbkdf2_phase2_key_length => 32,
  :aes_iv => nil,
  :aes_cipher => "AES-256-ECB",
  :aes_auth_tag => nil,
  :aes_auth_data => nil
}

Class Method Summary collapse

Class Method Details

.allSignaturesPresent?(tx, inputs, signatures, input_address_data) ⇒ Boolean

Returns:

  • (Boolean)


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/block_io/helper.rb', line 17

def self.allSignaturesPresent?(tx, inputs, signatures, input_address_data)
  # returns true if transaction has all signatures present
  
  all_signatures_present = false

  inputs.each do |input|
    # check if each input has its required signatures
    
    spending_address = input['spending_address']
    current_input_address_data = input_address_data.detect{|x| x['address'] == spending_address}
    required_signatures = current_input_address_data['required_signatures']
    public_keys = current_input_address_data['public_keys']

    signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}

    # break the loop if all signatures are not present for this input
    all_signatures_present = (signatures_present.keys.size >= required_signatures)
    break unless all_signatures_present
    
  end

  all_signatures_present

end

.base58_to_int(base58_val) ⇒ Object



297
298
299
300
301
302
303
304
305
# File 'lib/block_io/helper.rb', line 297

def self.base58_to_int(base58_val)
  alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
  int_val, base = 0, alpha.size
  base58_val.reverse.each_char.with_index do |char,index|
    raise ArgumentError, "Value not a valid Base58 String." unless char_index = alpha.index(char)
    int_val += char_index*(base**index)
  end
  int_val
end

.decode_base58(base58_val) ⇒ Object



312
313
314
315
316
317
318
319
# File 'lib/block_io/helper.rb', line 312

def self.decode_base58(base58_val)
  s = Helper.base58_to_int(base58_val).to_s(16)
  s = (s.bytesize.odd? ? ("0" << s) : s)
  s = "" if s == "00"
  leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
  s = ("00"*leading_zero_bytes) << s  if leading_zero_bytes > 0
  s
end

.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_tag = nil, auth_data = nil) ⇒ Object

Decrypts a block of data (encrypted_data) given an encryption key

Raises:

  • (Exception)


249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/block_io/helper.rb', line 249

def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_tag = nil, auth_data = nil)

  raise Exception.new("Auth tag must be 16 bytes exactly.") unless auth_tag.nil? or auth_tag.size == 32
  
  response = nil

  begin
    aes = OpenSSL::Cipher.new(cipher_type.downcase)
    aes.decrypt
    aes.key = b64_enc_key.unpack("m0")[0]
    aes.iv = [iv].pack("H*") unless iv.nil?
    aes.auth_tag = [auth_tag].pack("H*") unless auth_tag.nil?
    aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
    response = aes.update(encrypted_data.unpack("m0")[0]) << aes.final
  rescue Exception => e
    # decryption failed, must be an invalid Secret PIN
    raise Exception.new("Invalid Secret PIN provided.")
  end

  response
end

.dynamicExtractKey(user_key, pin) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/block_io/helper.rb', line 189

def self.dynamicExtractKey(user_key, pin)
  # user_key object contains the encrypted user key and decryption algorithm

  algorithm = self.getDecryptionAlgorithm(user_key['algorithm'])

  aes_key = self.pinToAesKey(pin, algorithm[:pbkdf2_iterations],
                             algorithm[:pbkdf2_salt],
                             algorithm[:pbkdf2_hash_function],
                             algorithm[:pbkdf2_phase1_key_length],
                             algorithm[:pbkdf2_phase2_key_length])

  decrypted = self.decrypt(user_key['encrypted_passphrase'], aes_key, algorithm[:aes_iv], algorithm[:aes_cipher], algorithm[:aes_auth_tag], algorithm[:aes_auth_data])
  
  Key.from_passphrase(decrypted)
  
end

.encode_base58(hex) ⇒ Object



307
308
309
310
# File 'lib/block_io/helper.rb', line 307

def self.encode_base58(hex)
  leading_zero_bytes  = (hex.match(/^([0]+)/) ? $1 : "").size / 2
  ("1"*leading_zero_bytes) << Helper.int_to_base58( hex.to_i(16) )
end

.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_data = nil) ⇒ Object

Encrypts a block of data given an encryption key



272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/block_io/helper.rb', line 272

def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_data = nil)
  aes = OpenSSL::Cipher.new(cipher_type.downcase)
  aes.encrypt
  aes.key = b64_enc_key.unpack("m0")[0]
  aes.iv = [iv].pack("H*") unless iv.nil?
  aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
  result = [aes.update(data) << aes.final].pack("m0")
  auth_tag = (cipher_type.end_with?("-GCM") ? aes.auth_tag.unpack("H*")[0] : nil)

  {:aes_auth_tag => auth_tag, :aes_cipher_text => result, :aes_iv => iv, :aes_cipher => cipher_type, :aes_auth_data => auth_data}
  
end

.extractKey(encrypted_data, b64_enc_key) ⇒ Object



206
207
208
209
210
211
212
213
214
215
# File 'lib/block_io/helper.rb', line 206

def self.extractKey(encrypted_data, b64_enc_key)
  # passphrase is in plain text
  # encrypted_data is in base64, as it was stored on Block.io
  # returns the private key extracted from the given encrypted data
  
  decrypted = self.decrypt(encrypted_data, b64_enc_key)
  
  Key.from_passphrase(decrypted)

end

.finalizeTransaction(tx, inputs, signatures, input_address_data) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/block_io/helper.rb', line 57

def self.finalizeTransaction(tx, inputs, signatures, input_address_data)
  # append signatures to the transaction and return its hexadecimal representation
  
  inputs.each do |input|
    # for each input

    signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
    address_data = input_address_data.detect{|x| x['address'] == input['spending_address']} # contains public keys (ordered) and the address type
    input_index = input['input_index']
    is_segwit = isSegwitAddressType?(address_data['address_type'])
    script_stack = (is_segwit ? tx.in[input_index].script_witness.stack : tx.in[input_index].script_sig)
    
    if ['P2PKH', 'P2WPKH', 'P2WPKH-over-P2SH'].include?(address_data['address_type']) then
      # P2PKH will use script_sig as script_stack
      # P2WPKH input, or P2WPKH-over-P2SH input will use script_witness.stack as script_stack

      current_public_key = address_data['public_keys'][0]
      current_signature = signatures_present[current_public_key]

      # no blank push necessary for P2PKH, P2WPKH, P2WPKH-over-P2SH
      script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
      script_stack << [current_public_key].pack("H*")

      # P2WPKH-over-P2SH required script_sig still
      tx.in[input_index].script_sig << (
        Bitcoin::Script.to_p2wpkh(
          Bitcoin::Key.new(:pubkey => current_public_key, :key_type => Bitcoin::Key::TYPES[:compressed]).hash160 # hash160 of the compressed pubkey
        ).to_payload
      ) if address_data['address_type'] == "P2WPKH-over-P2SH"
                
    elsif ['P2SH', 'WITNESS_V0', 'P2WSH-over-P2SH'].include?(address_data['address_type']) then
      # P2SH will use script_sig as script_stack
      # P2WSH or P2WSH-over-P2SH input will use script_witness.stack as script_stack

      script = Bitcoin::Script.to_p2sh_multisig_script(address_data['required_signatures'], address_data['public_keys'])

      script_stack << '' # blank push for scripthash always

      signatures_added = 0

      address_data['public_keys'].each do |public_key|
        next unless signatures_present.key?(public_key)

        # append signatures, no sighash needed, in correct order of public keys
        current_signature = signatures_present[public_key]
        script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))

        signatures_added += 1

        # required signatures added? break loop and move on
        break if signatures_added == address_data['required_signatures']
      end

      script_stack << script.last.to_payload

      # P2WSH-over-P2SH needs script_sig populated still
      tx.in[input_index].script_sig << Bitcoin::Script.to_p2wsh(script.last).to_payload if address_data['address_type'] == "P2WSH-over-P2SH"
      
    else
      raise "Unrecognized input address: #{address_data['address_type']}"
    end
    
  end

  tx.to_hex
  
end

.getDecryptionAlgorithm(user_key_algorithm = nil) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/block_io/helper.rb', line 168

def self.getDecryptionAlgorithm(user_key_algorithm = nil)
  # mainly used so existing unit tests do not break
  
  algorithm = ({}).merge(LEGACY_DECRYPTION_ALGORITHM)

  if !user_key_algorithm.nil? then
    algorithm[:pbkdf2_salt] = user_key_algorithm['pbkdf2_salt']
    algorithm[:pbkdf2_iterations] = user_key_algorithm['pbkdf2_iterations']
    algorithm[:pbkdf2_hash_function] = user_key_algorithm['pbkdf2_hash_function']
    algorithm[:pbkdf2_phase1_key_length] = user_key_algorithm['pbkdf2_phase1_key_length']
    algorithm[:pbkdf2_phase2_key_length] = user_key_algorithm['pbkdf2_phase2_key_length']
    algorithm[:aes_iv] = user_key_algorithm['aes_iv']
    algorithm[:aes_cipher] = user_key_algorithm['aes_cipher']
    algorithm[:aes_auth_tag] = user_key_algorithm['aes_auth_tag']
    algorithm[:aes_auth_data] = user_key_algorithm['aes_auth_data']
  end

  algorithm
  
end

.getSigHashForInput(tx, input_index, input_data, input_address_data) ⇒ Object



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/block_io/helper.rb', line 125

def self.getSigHashForInput(tx, input_index, input_data, input_address_data)
  # returns the sighash for the given input in bytes
  
  address_type = input_address_data["address_type"]
  input_value = (BigDecimal(input_data['input_value']) * BigDecimal(100000000)).to_i # in sats
  sighash = nil
  
  if address_type == "P2SH" then
    # P2SH addresses
    
    script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
    sighash = tx.sighash_for_input(input_index, script.last)
    
  elsif address_type == "P2WSH-over-P2SH" or address_type == "WITNESS_V0" then
    # P2WSH-over-P2SH addresses
    # WITNESS_V0 addresses

    script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
    sighash = tx.sighash_for_input(input_index, script.last, amount: input_value, sig_version: :witness_v0)
    
  elsif address_type == "P2WPKH-over-P2SH" or address_type == "P2WPKH" then
    # P2WPKH-over-P2SH addresses
    # P2WPKH addresses
    
    pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
    script = Bitcoin::Script.to_p2wpkh(pub_key.hash160)
    sighash = tx.sighash_for_input(input_index, script, amount: input_value, sig_version: :witness_v0)
    
  elsif address_type == "P2PKH" then
    # P2PKH addresses

    pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
    script = Bitcoin::Script.to_p2pkh(pub_key.hash160)
    sighash = tx.sighash_for_input(input_index, script)

  else
    raise "Unrecognize address type: #{address_type}"
  end

  sighash
  
end

.int_to_base58(int_val, leading_zero_bytes = 0) ⇒ Object

courtesy bitcoin-ruby



287
288
289
290
291
292
293
294
295
# File 'lib/block_io/helper.rb', line 287

def self.int_to_base58(int_val, leading_zero_bytes=0)
  alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
  base58_val, base = "", alpha.size
  while int_val > 0
    int_val, remainder = int_val.divmod(base)
    base58_val = alpha[remainder] << base58_val
  end
  base58_val
end

.isSegwitAddressType?(address_type) ⇒ Boolean

Returns:

  • (Boolean)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/block_io/helper.rb', line 42

def self.isSegwitAddressType?(address_type)

  case address_type
  when /^P2WPKH(-over-P2SH)?$/
    true
  when /^P2WSH(-over-P2SH)?$/
    true
  when /^WITNESS_V(\d)$/
    true
  else
    false
  end
    
end

.pinToAesKey(secret_pin, iterations = 2048, salt = "", hash_function = "SHA256", pbkdf2_phase1_key_length = 16, pbkdf2_phase2_key_length = 32) ⇒ Object

Raises:

  • (Exception)


222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/block_io/helper.rb', line 222

def self.pinToAesKey(secret_pin, iterations = 2048, salt = "", hash_function = "SHA256", pbkdf2_phase1_key_length = 16, pbkdf2_phase2_key_length = 32)
  # converts the pincode string to PBKDF2
  # returns a base64 version of PBKDF2 pincode

  raise Exception.new("Unknown hash function specified. Are you using current version of this library?") unless hash_function == "SHA256"
  
  part1 = OpenSSL::PKCS5.pbkdf2_hmac(
    secret_pin,
    salt,
    iterations/2,
    pbkdf2_phase1_key_length,
    OpenSSL::Digest::SHA256.new
  ).unpack("H*")[0]
  
  part2 = OpenSSL::PKCS5.pbkdf2_hmac(
    part1,
    salt,
    iterations/2,
    pbkdf2_phase2_key_length,
    OpenSSL::Digest::SHA256.new
  ) # binary

  [part2].pack("m0") # the base64 encryption key

end

.sha256(value) ⇒ Object



217
218
219
220
# File 'lib/block_io/helper.rb', line 217

def self.sha256(value)
  # returns the hex of the hash of the given value
  OpenSSL::Digest::SHA256.digest(value).unpack("H*")[0]
end