class BlockIo::Helper

Constants

BASE58_ALPHABET

courtesy bitcoin-ruby

LEGACY_DECRYPTION_ALGORITHM

Public Class Methods

allSignaturesPresent?(tx, inputs, signatures, input_address_data) click to toggle source
# 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

  i = 0
  loop do
    # check if each input has its required signatures
    input = inputs[i]
    break if input.nil?
    i += 1
    
    spending_address = input['spending_address']
    current_input_address_data = input_address_data.detect{|x| x['address'].eql?(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'].eql?(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.size >= required_signatures)
    break unless all_signatures_present        
  end

  all_signatures_present

end
base58_to_int(base58_val) click to toggle source
# File lib/block_io/helper.rb, line 307
def self.base58_to_int(base58_val)
  int_val, base = 0, BASE58_ALPHABET.size
  base58_val.reverse.each_char.with_index do |char,index|
    raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = BASE58_ALPHABET.index(char)
    int_val += char_index*(base**index)
  end
  int_val
end
decode_base58(base58_val) click to toggle source
# File lib/block_io/helper.rb, line 321
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.eql?('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) click to toggle source

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

# File lib/block_io/helper.rb, line 260
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.eql?(32)
  
  response = nil

  begin
    aes = OpenSSL::Cipher.new(cipher_type.downcase)
    aes.decrypt
    aes.key = b64_enc_key.unpack1('m0')
    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.unpack1('m0')) << 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) click to toggle source
# File lib/block_io/helper.rb, line 200
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) click to toggle source
# File lib/block_io/helper.rb, line 316
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) click to toggle source

Encrypts a block of data given an encryption key

# File lib/block_io/helper.rb, line 283
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.unpack1('m0')
  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.unpack1('H*') : 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) click to toggle source
# File lib/block_io/helper.rb, line 217
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) click to toggle source
# File lib/block_io/helper.rb, line 60
def self.finalizeTransaction(tx, inputs, signatures, input_address_data)
  # append signatures to the transaction and return its hexadecimal representation

  i = 0
  loop do
    # for each input
    input = inputs[i]
    break if input.nil?
    i += 1

    signatures_present = signatures.map{|x| x if x['input_index'].eql?(input['input_index'])}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
    address_data = input_address_data.detect{|x| x['address'].eql?(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'].eql?('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

      j = 0
      loop do
        public_key = address_data['public_keys'][j]
        break if public_key.nil?
        j += 1
        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.eql?(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'].eql?('P2WSH-over-P2SH')
      
    else
      raise "Unrecognized input address: #{address_data['address_type']}"
    end
    
  end

  tx.to_hex
  
end
getDecryptionAlgorithm(user_key_algorithm = nil) click to toggle source
# File lib/block_io/helper.rb, line 179
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) click to toggle source
# File lib/block_io/helper.rb, line 136
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']) * 100_000_000).to_i # in sats
  sighash = nil
  
  if address_type.eql?('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.eql?('P2WSH-over-P2SH') or address_type.eql?('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.eql?('P2WPKH-over-P2SH') or address_type.eql?('P2WPKH') then
    # P2WPKH-over-P2SH addresses
    # P2WPKH addresses
    
    pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'][0], :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.eql?('P2PKH') then
    # P2PKH addresses

    pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'][0], :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) click to toggle source
# File lib/block_io/helper.rb, line 298
def self.int_to_base58(int_val, leading_zero_bytes=0)
  base58_val, base = '', BASE58_ALPHABET.size
  while int_val > 0
    int_val, remainder = int_val.divmod(base)
    base58_val = '' << BASE58_ALPHABET[remainder] << base58_val
  end
  base58_val
end
isSegwitAddressType?(address_type) click to toggle source
# File lib/block_io/helper.rb, line 45
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) click to toggle source
# File lib/block_io/helper.rb, line 233
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.eql?('SHA256')
  
  part1 = OpenSSL::PKCS5.pbkdf2_hmac(
    secret_pin,
    salt,
    iterations/2,
    pbkdf2_phase1_key_length,
    OpenSSL::Digest::SHA256.new
  ).unpack1('H*')
  
  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) click to toggle source
# File lib/block_io/helper.rb, line 228
def self.sha256(value)
  # returns the hex of the hash of the given value
  OpenSSL::Digest::SHA256.digest(value).unpack1('H*')
end