lib/bitcoin/key.rb in bitcoin-ruby-0.0.5 vs lib/bitcoin/key.rb in bitcoin-ruby-0.0.6

- old
+ new

@@ -5,12 +5,12 @@ # Elliptic Curve key as used in bitcoin. class Key # Generate a new keypair. # Bitcoin::Key.generate - def self.generate - k = new; k.generate; k + def self.generate(opts={compressed: true}) + k = new(nil, nil, opts); k.generate; k end # Import private key from base58 fromat as described in # https://en.bitcoin.it/wiki/Private_key#Base_58_Wallet_Import_format and # https://en.bitcoin.it/wiki/Base58Check_encoding#Encoding_a_private_key. @@ -22,23 +22,24 @@ raise "Invalid version" unless version == Bitcoin.network[:privkey_version] raise "Invalid checksum" unless Bitcoin.checksum(version + key + flag) == checksum key = new(key, nil, compressed) end - def == other + def ==(other) self.priv == other.priv end # Create a new key with given +privkey+ and +pubkey+. # Bitcoin::Key.new # Bitcoin::Key.new(privkey) # Bitcoin::Key.new(nil, pubkey) - def initialize privkey = nil, pubkey = nil, compressed = false + def initialize(privkey = nil, pubkey = nil, opts={compressed: true}) + compressed = opts.is_a?(Hash) ? opts.fetch(:compressed, true) : opts @key = Bitcoin.bitcoin_elliptic_curve @pubkey_compressed = pubkey ? self.class.is_compressed_pubkey?(pubkey) : compressed set_priv(privkey) if privkey - set_pub(pubkey) if pubkey + set_pub(pubkey, @pubkey_compressed) if pubkey end # Generate new priv/pub key. def generate @key.generate_key @@ -57,25 +58,21 @@ # Get the public key (in hex). # In case the key was initialized with only # a private key, the public key is regenerated. def pub + regenerate_pubkey unless @key.public_key + return nil unless @key.public_key @pubkey_compressed ? pub_compressed : pub_uncompressed end def pub_compressed - regenerate_pubkey unless @key.public_key - return nil unless @key.public_key @key.public_key.group.point_conversion_form = :compressed - hex = @key.public_key.to_hex.rjust(66, '0') - @key.public_key.group.point_conversion_form = :uncompressed - hex + @key.public_key.to_hex.rjust(66, '0') end def pub_uncompressed - regenerate_pubkey unless @key.public_key - return nil unless @key.public_key @key.public_key.group.point_conversion_form = :uncompressed @key.public_key.to_hex.rjust(130, '0') end def compressed @@ -97,19 +94,20 @@ Bitcoin.hash160_to_address(hash160) end # Sign +data+ with the key. # key1 = Bitcoin::Key.generate - # sig = key.sign("some data") + # sig = key1.sign("some data") def sign(data) @key.dsa_sign_asn1(data) end # Verify signature +sig+ for +data+. # key2 = Bitcoin::Key.new(nil, key1.pub) # key2.verify("some data", sig) def verify(data, sig) + regenerate_pubkey unless @key.public_key @key.dsa_verify_asn1(data, sig) end def sign_message(message) @@ -160,25 +158,100 @@ data += "01" if @pubkey_compressed hex = data + Bitcoin.checksum(data) Bitcoin.int_to_base58( hex.to_i(16) ) end + + # Export private key to bip38 (non-ec-multiply) format as described in + # https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki + # See also Key.from_bip38 + def to_bip38(passphrase) + flagbyte = compressed ? "\xe0" : "\xc0" + 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) + derivedhalf1, derivedhalf2 = buf[0...32], buf[32..-1] + + aes = proc{|k,a,b| + cipher = OpenSSL::Cipher::AES.new(256, :ECB); cipher.encrypt; cipher.padding = 0; cipher.key = k + cipher.update [ (a.to_i(16) ^ b.unpack("H*")[0].to_i(16)).to_s(16).rjust(32, '0') ].pack("H*") + } + + encryptedhalf1 = aes.call(derivedhalf2, self.priv[0...32], derivedhalf1[0...16]) + encryptedhalf2 = aes.call(derivedhalf2, self.priv[32..-1], derivedhalf1[16..-1]) + + encrypted_privkey = "\x01\x42" + flagbyte + addresshash + encryptedhalf1 + encryptedhalf2 + encrypted_privkey += Digest::SHA256.digest( Digest::SHA256.digest( encrypted_privkey ) )[0...4] + + encrypted_privkey = Bitcoin.encode_base58( encrypted_privkey.unpack("H*")[0] ) + end + + # Import private key from bip38 (non-ec-multiply) fromat as described in + # https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki + # See also #to_bip38 + def self.from_bip38(encrypted_privkey, passphrase) + version, flagbyte, addresshash, encryptedhalf1, encryptedhalf2, checksum = + [ Bitcoin.decode_base58(encrypted_privkey) ].pack("H*").unpack("a2aa4a16a16a4") + compressed = (flagbyte == "\xe0") ? true : false + + raise "Invalid version" unless version == "\x01\x42" + 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) + derivedhalf1, derivedhalf2 = buf[0...32], buf[32..-1] + + aes = proc{|k,a| + cipher = OpenSSL::Cipher::AES.new(256, :ECB); cipher.decrypt; cipher.padding = 0; cipher.key = k + cipher.update(a) + } + + decryptedhalf2 = aes.call(derivedhalf2, encryptedhalf2) + decryptedhalf1 = aes.call(derivedhalf2, encryptedhalf1) + + priv = decryptedhalf1 + decryptedhalf2 + priv = (priv.unpack("H*")[0].to_i(16) ^ derivedhalf1.unpack("H*")[0].to_i(16)).to_s(16).rjust(64, '0') + key = Bitcoin::Key.new(priv, nil, compressed) + + if Digest::SHA256.digest( Digest::SHA256.digest( key.addr ) )[0...4] != addresshash + raise "Invalid addresshash! Password is likely incorrect." + end + + key + end + + # Import private key from warp fromat as described in + # https://github.com/keybase/warpwallet + # https://keybase.io/warp/ + def self.from_warp(passphrase, salt="", compressed=false) + require 'scrypt' unless defined?(::SCrypt::Engine) + s1 = SCrypt::Engine.scrypt(passphrase+"\x01", salt+"\x01", 2**18, 8, 1, 32) + s2 = OpenSSL::PKCS5.pbkdf2_hmac(passphrase+"\x02", salt+"\x02", 2**16, 32, OpenSSL::Digest::SHA256.new) + s3 = s1.bytes.zip(s2.bytes).map{|a,b| a ^ b }.pack("C*") + + key = Bitcoin::Key.new(s3.unpack("H*")[0], nil, compressed) + # [key.addr, key.to_base58, [s1,s2,s3].map{|i| i.unpack("H*")[0] }, compressed] + key + end + + protected # Regenerate public key from the private key. def regenerate_pubkey return nil unless @key.private_key - set_pub(Bitcoin::OpenSSL_EC.regenerate_key(priv)[1]) + set_pub(Bitcoin::OpenSSL_EC.regenerate_key(priv)[1], @pubkey_compressed) end # Set +priv+ as the new private key (converting from hex). def set_priv(priv) @key.private_key = OpenSSL::BN.from_hex(priv) end # Set +pub+ as the new public key (converting from hex). - def set_pub(pub) - @pubkey_compressed ||= self.class.is_compressed_pubkey?(pub) + def set_pub(pub, compressed = nil) + @pubkey_compressed = compressed == nil ? self.class.is_compressed_pubkey?(pub) : compressed @key.public_key = OpenSSL::PKey::EC::Point.from_hex(@key.group, pub) end def self.is_compressed_pubkey?(pub) ["02","03"].include?(pub[0..1])