GemHQ/secp256k1-rb

View on GitHub
lib/secp256k1/ffi.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# encoding: ascii-8bit

# Wraps libsecp256k1 (https://github.com/bitcoin/secp256k1)
module Secp256k1
  require 'ffi'
  extend ::FFI::Library

  SECP256K1_START_VERIFY = (1 << 0)
  SECP256K1_START_SIGN   = (1 << 1)

  # Load secp256k1 library and attach functions from C to Ruby via FFI.
  def self.included(_base)
    ffi_lib 'secp256k1'
    attach_functions!
  rescue LoadError => e
    warn e.message
  end

  def self.attach_functions!
    attach_function :secp256k1_start, [:int], :void
    attach_function :secp256k1_stop, [], :void
    attach_function :secp256k1_ec_seckey_verify, [:pointer], :int
    attach_function :secp256k1_ec_pubkey_create, [:pointer, :pointer, :pointer, :int], :int
    attach_function :secp256k1_ecdsa_sign, [:pointer, :int, :pointer, :pointer, :pointer, :pointer], :int
    attach_function :secp256k1_ecdsa_verify, [:pointer, :int, :pointer, :int, :pointer, :int], :int
    attach_function :secp256k1_ecdsa_sign_compact, [:pointer, :int, :pointer, :pointer, :pointer, :pointer], :int
    attach_function :secp256k1_ecdsa_recover_compact, [:pointer, :int, :pointer, :pointer, :pointer, :int, :int], :int
  end

  def self.init
    return if @secp256k1_started
    secp256k1_start(SECP256K1_START_VERIFY | SECP256K1_START_SIGN)
    @secp256k1_started = true
  end

  def self.generate_key_pair(compressed=true)
    init

    while true do
      priv_key = SecureRandom.random_bytes(32)
      priv_key_buf = FFI::MemoryPointer.new(:uchar, 32)
      priv_key_buf.put_bytes(0, priv_key)
      break if secp256k1_ec_seckey_verify(priv_key_buf)
    end

    pub_key_buf = FFI::MemoryPointer.new(:uchar, 65)
    pub_key_size = FFI::MemoryPointer.new(:int)
    result = secp256k1_ec_pubkey_create(pub_key_buf, pub_key_size, priv_key_buf, compressed ? 1 : 0)
    raise "error creating pubkey" unless result

    [ priv_key, pub_key_buf.read_string(pub_key_size.read_int) ]
  end

  def self.sign(data, priv_key)
    init

    data_buf = FFI::MemoryPointer.new(:uchar, data.bytesize)
    data_buf.put_bytes(0, data)

    sig_buf = FFI::MemoryPointer.new(:uchar, 72)
    sig_size = FFI::MemoryPointer.new(:int)
    sig_size.write_int(72)

    priv_key_buf = FFI::MemoryPointer.new(:uchar, priv_key.bytesize)
    priv_key_buf.put_bytes(0, priv_key)

    while true do
      nonce = FFI::MemoryPointer.new(:uchar, 32)
      nonce.put_bytes(0, SecureRandom.random_bytes(32))
      break if secp256k1_ecdsa_sign(data_buf, data.bytesize, sig_buf, sig_size, priv_key, nonce)
    end

    sig_buf.read_string(sig_size.read_int)
  end

  def self.verify(data, signature, pub_key)
    init

    data_buf = FFI::MemoryPointer.new(:uchar, data.bytesize)
    data_buf.put_bytes(0, data)

    sig_buf = FFI::MemoryPointer.new(:uchar, signature.bytesize)
    sig_buf.put_bytes(0, signature)

    pub_key_buf = FFI::MemoryPointer.new(:uchar, pub_key.bytesize)
    pub_key_buf.put_bytes(0, pub_key)

    result = secp256k1_ecdsa_verify(data_buf, data.bytesize,
                                    sig_buf, signature.bytesize,
                                    pub_key_buf, pub_key.bytesize)
    if result == -1
      raise "error invalid pubkey"
    elsif result == -2
      raise "error invalid signature"
    end

    result == 1
  end

  def self.sign_compact(message, priv_key, compressed=true)
    init

    message_buf = FFI::MemoryPointer.new(:uchar, message.bytesize)
    message_buf.put_bytes(0, message)

    sig_buf = FFI::MemoryPointer.new(:uchar, 64)

    priv_key_buf = FFI::MemoryPointer.new(:uchar, priv_key.bytesize)
    priv_key_buf.put_bytes(0, priv_key)

    rec_id = FFI::MemoryPointer.new(:int)

    while true do
      nonce = FFI::MemoryPointer.new(:uchar, 32)
      nonce.put_bytes(0, SecureRandom.random_bytes(32))
      break if secp256k1_ecdsa_sign_compact(message_buf, message.bytesize, sig_buf, priv_key, nonce, rec_id)
    end

    header = [27 + rec_id.read_int + (compressed ? 4 : 0)].pack("C")
    [ header, sig_buf.read_string(64) ].join
  end

  def self.recover_compact(message, signature)
    init

    return nil if signature.bytesize != 65

    version = signature.unpack('C')[0]
    return nil if version < 27 || version > 34

    compressed = version >= 31 ? true : false
    version -= 4 if compressed
    rec_id = version - 27

    message_buf = FFI::MemoryPointer.new(:uchar, message.bytesize)
    message_buf.put_bytes(0, message)

    signature[0] = ''
    sig_buf = FFI::MemoryPointer.new(:uchar, signature.bytesize)
    sig_buf.put_bytes(0, signature)

    pub_key_len = compressed ? 33 : 65
    pub_key_buf = FFI::MemoryPointer.new(:uchar, pub_key_len)
    pub_key_size = FFI::MemoryPointer.new(:int)
    pub_key_size.write_int(pub_key_len)

    result = secp256k1_ecdsa_recover_compact(message_buf, message.bytesize,
                                             sig_buf,
                                             pub_key_buf, pub_key_size,
                                             compressed ? 1 : 0,
                                             rec_id)
    return nil unless result

    pub_key_buf.read_bytes(pub_key_size.read_int)
  end
end