Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ARG RUBY_VERSION=3.2.2
FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION
26 changes: 26 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "Ruby Bitcoin Development",
"dockerFile": "Dockerfile",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"Shopify.ruby-lsp"
],
"settings": {
"editor.formatOnSave": true,
"ruby.useBundler": true,
"ruby.useLanguageServer": true,
"ruby.lint": {
"rubocop": true
}
}
}
},
"forwardPorts": [],
"postCreateCommand": "bundle install",
"remoteUser": "vscode"
}
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ spec-rspec/examples.txt
# emacs
*~
\#*\#
.\#*
.\#*

# VS Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
34 changes: 25 additions & 9 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@ GEM
ast (2.4.0)
bacon (1.2.0)
byebug (10.0.2)
coderay (1.1.2)
coderay (1.1.3)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.3)
docile (1.3.1)
eventmachine (1.2.7)
ffi (1.9.25)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
ffi (1.17.0)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
io-console (0.7.2)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jaro_winkler (1.5.1)
json (2.1.0)
method_source (0.9.0)
json (2.8.2)
method_source (0.9.2)
minitest (5.11.3)
parallel (1.12.1)
parser (2.5.1.2)
Expand All @@ -34,8 +41,14 @@ GEM
pry-byebug (3.6.0)
byebug (~> 10.0)
pry (~> 0.10)
psych (5.2.0)
stringio
rainbow (3.0.0)
rake (12.3.1)
rake (12.3.3)
rdoc (6.8.1)
psych (>= 4.0.0)
reline (0.5.11)
io-console (~> 0.5)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
Expand All @@ -58,13 +71,15 @@ GEM
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.9.0)
scrypt (3.0.5)
scrypt (3.0.8)
ffi-compiler (>= 1.0, < 2.0)
rake (>= 9, < 14)
simplecov (0.16.1)
docile (~> 1.1)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
stringio (3.1.2)
unicode-display_width (1.4.0)

PLATFORMS
Expand All @@ -73,6 +88,7 @@ PLATFORMS
DEPENDENCIES
bacon (~> 1.2.0)
bitcoin-ruby!
debug
minitest (~> 5.11.3)
pry (~> 0.11.3)
pry-byebug (~> 3.6.0)
Expand All @@ -82,4 +98,4 @@ DEPENDENCIES
simplecov (~> 0.16.1)

BUNDLED WITH
1.17.3
2.5.23
4 changes: 4 additions & 0 deletions bitcoin-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'ffi'
s.add_runtime_dependency 'scrypt' # required by Litecoin
s.add_runtime_dependency 'eventmachine' # required for connection code

# Add development dependencies
s.add_development_dependency 'rspec', '~> 3.12'
s.add_development_dependency 'debug'
end
15 changes: 10 additions & 5 deletions lib/bitcoin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Bitcoin
# deprecated in favor of a unification of Fixnum and BigInteger named Integer.
# Since this project strivers for backwards-compatability, we determine the
# appropriate class to use at initialization.
#
#
# This avoids annoying deprecation warnings on newer versions for ourselves
# and library consumers.
Integer =
Expand All @@ -29,6 +29,7 @@ module Bitcoin
autoload :Script, 'bitcoin/script'
autoload :VERSION, 'bitcoin/version'
autoload :Key, 'bitcoin/key'
autoload :PKeyEC, 'bitcoin/pkey_ec'
autoload :ExtKey, 'bitcoin/ext_key'
autoload :ExtPubkey, 'bitcoin/ext_key'
autoload :Builder, 'bitcoin/builder'
Expand All @@ -39,6 +40,7 @@ module Bitcoin

autoload :ContractHash, 'bitcoin/contracthash'


module Trezor
autoload :Mnemonic, 'bitcoin/trezor/mnemonic'
end
Expand Down Expand Up @@ -286,11 +288,11 @@ def decode_target(target_bits)
end

def bitcoin_elliptic_curve
::OpenSSL::PKey::EC.new("secp256k1")
Bitcoin::PKeyEC.new
end

def generate_key
key = bitcoin_elliptic_curve.generate_key
key = Bitcoin::PKeyEC.generate_key
inspect_key( key )
end

Expand Down Expand Up @@ -409,11 +411,13 @@ def verify_signature(hash, signature, public_key)
def open_key(private_key, public_key=nil)
key = bitcoin_elliptic_curve
key.private_key = ::OpenSSL::BN.from_hex(private_key)
public_key = regenerate_public_key(private_key) unless public_key
key.public_key = ::OpenSSL::PKey::EC::Point.from_hex(key.group, public_key)
key
end

def group
OpenSSL::PKey::EC::Group.new('secp256k1')
end

def regenerate_public_key(private_key)
OpenSSL_EC.regenerate_key(private_key)[1]
end
Expand All @@ -439,6 +443,7 @@ def verify_message(address, signature, message)
return false unless valid_address?(address)
return false unless signature
return false unless signature.bytesize == 65

hash = bitcoin_signed_message_hash(message)
pubkey = OpenSSL_EC.recover_compact(hash, signature)
pubkey_to_address(pubkey) == address if pubkey
Expand Down
22 changes: 17 additions & 5 deletions lib/bitcoin/ffi/openssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,23 @@ def self.regenerate_key(private_key)
private_key_hex = private_key.unpack('H*')[0]

group = OpenSSL::PKey::EC::Group.new('secp256k1')
key = OpenSSL::PKey::EC.new(group)
key.private_key = OpenSSL::BN.new(private_key_hex, 16)
key.public_key = group.generator.mul(key.private_key)

priv_hex = key.private_key.to_bn.to_s(16).downcase.rjust(64, '0')
private_key_bn = OpenSSL::BN.new(private_key_hex, 16)

# Generate public key point by multiplying generator with private key
public_key_point = group.generator.mul(private_key_bn)

# Create ASN1 structure for EC key
asn1 = OpenSSL::ASN1::Sequence([
OpenSSL::ASN1::Integer.new(1),
OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT)
])

key = OpenSSL::PKey::EC.new(asn1.to_der)

# Verify the private key was generated correctly
priv_hex = key.private_key.to_s(16).downcase.rjust(64, '0')
if priv_hex != private_key_hex
raise 'regenerated wrong private_key, raise here before generating a faulty public_key too!'
end
Expand Down
26 changes: 21 additions & 5 deletions lib/bitcoin/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def initialize(privkey = nil, pubkey = nil, opts={compressed: true})

# Generate new priv/pub key.
def generate
@key.generate_key
@key = Bitcoin::PKeyEC.generate_key
end

# Get the private key (in hex).
Expand Down Expand Up @@ -155,7 +155,7 @@ def self.recover_compact_signature_to_key(data, signature_base64)

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

compressed = (version >= 31) ? (version -= 4; true) : false

hash = Bitcoin.bitcoin_signed_message_hash(data)
Expand Down Expand Up @@ -183,7 +183,7 @@ def to_bip38(passphrase)
addresshash = Digest::SHA256.digest( Digest::SHA256.digest( self.addr ) )[0...4]

require 'scrypt' unless defined?(::SCrypt::Engine)
buf = SCrypt::Engine.__sc_crypt(passphrase, addresshash, 16384, 8, 8, 64)
buf = SCrypt::Engine.scrypt(passphrase, addresshash, 16384, 8, 8, 64)
derivedhalf1, derivedhalf2 = buf[0...32], buf[32..-1]

aes = proc{|k,a,b|
Expand Down Expand Up @@ -212,7 +212,7 @@ def self.from_bip38(encrypted_privkey, passphrase)
raise "Invalid checksum" unless Digest::SHA256.digest(Digest::SHA256.digest(version + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2))[0...4] == checksum

require 'scrypt' unless defined?(::SCrypt::Engine)
buf = SCrypt::Engine.__sc_crypt(passphrase, addresshash, 16384, 8, 8, 64)
buf = SCrypt::Engine.scrypt(passphrase, addresshash, 16384, 8, 8, 64)
derivedhalf1, derivedhalf2 = buf[0...32], buf[32..-1]

aes = proc{|k,a|
Expand Down Expand Up @@ -259,8 +259,24 @@ def regenerate_pubkey

# Set +priv+ as the new private key (converting from hex).
def set_priv(priv)
# Convert to string and strip whitespace
priv = priv.to_s.strip

# Validate hex string format
unless priv.match?(/\A[0-9a-fA-F]+\z/)
raise ArgumentError, "Private key must be a valid hexadecimal string, got: #{priv.inspect}"
end

# Validate length (64 hex chars = 32 bytes)
unless priv.length == 64
raise ArgumentError, "Private key must be exactly 64 hex characters (got #{priv.length})"
end

value = priv.to_i(16)
raise 'private key is not on curve' unless MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER
unless MIN_PRIV_KEY_MOD_ORDER <= value && value <= MAX_PRIV_KEY_MOD_ORDER
raise ArgumentError, 'Private key value is not within valid range (must be between 1 and n-1 where n is the order of the secp256k1 curve)'
end

@key.private_key = OpenSSL::BN.from_hex(priv)
end

Expand Down
67 changes: 67 additions & 0 deletions lib/bitcoin/pkey_ec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module Bitcoin
class PKeyEC #< OpenSSL::PKey::EC

CURVE = 'secp256k1'

attr_reader :group, :private_key, :public_key


def private_key_hex; private_key.to_hex.rjust(64, '0'); end
def public_key_hex; public_key.to_hex.rjust(130, '0'); end

def initialize
@group = OpenSSL::PKey::EC::Group.new(CURVE)
end

def self.generate_key
OpenSSL::PKey::EC.generate(CURVE)
end

def public_key=(public_key_bn)
@public_key = public_key_bn
end

def private_key=(private_key_bn)
@public_key = restore_public_key(private_key_bn)
asn1 = OpenSSL::ASN1::Sequence(
[
OpenSSL::ASN1::Integer.new(1),
OpenSSL::ASN1::OctetString(private_key_bn.to_s(2)),
OpenSSL::ASN1::ObjectId(CURVE, 0, :EXPLICIT),
OpenSSL::ASN1::BitString(@public_key.to_octet_string(:uncompressed), 1, :EXPLICIT)
]
)
@pk = OpenSSL::PKey::EC.new(asn1.to_der)
@private_key = @pk.private_key
end

def dsa_sign_asn1(data)
@pk.dsa_sign_asn1(data)
end

def dsa_verify_asn1(data, signature)
initialize_from_public_key unless @pk
@pk.dsa_verify_asn1(data, signature)
end

def initialize_from_public_key
asn1 = OpenSSL::ASN1::Sequence.new(
[
OpenSSL::ASN1::Sequence.new([
OpenSSL::ASN1::ObjectId.new('id-ecPublicKey'),
OpenSSL::ASN1::ObjectId.new(@group.curve_name)
]),
OpenSSL::ASN1::BitString.new(@public_key.to_octet_string(:uncompressed))
]
)
@pk = OpenSSL::PKey::EC.new(asn1.to_der)
end

private

def restore_public_key(private_bn)
public_bn = group.generator.mul(private_bn).to_bn
public_bn = OpenSSL::PKey::EC::Point.new(@group, public_bn)
end
end
end
3 changes: 2 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# Code coverage generation
require 'simplecov'

require 'debug'
SimpleCov.start do
add_group('Bitcoin') do |file|
['bitcoin.rb', 'opcodes.rb', 'script.rb', 'key.rb'].include?(
Expand Down Expand Up @@ -106,4 +107,4 @@
config.before(:each) do
Bitcoin.network = :bitcoin
end
end
end
Loading