Class: Bitcoin::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/bitcoin/key.rb

Overview

Elliptic Curve key as used in bitcoin.

Constant Summary collapse

MIN_PRIV_KEY_MOD_ORDER =
0x01
MAX_PRIV_KEY_MOD_ORDER =

Order of secp256k1's generator minus 1.

0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(privkey = nil, pubkey = nil, opts = {compressed: true}) ⇒ Key

Create a new key with given privkey and pubkey.

Bitcoin::Key.new
Bitcoin::Key.new(privkey)
Bitcoin::Key.new(nil, pubkey)

41
42
43
44
45
46
47
# File 'lib/bitcoin/key.rb', line 41

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, @pubkey_compressed)  if pubkey
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key


8
9
10
# File 'lib/bitcoin/key.rb', line 8

def key
  @key
end

Class Method Details

.from_base58(str) ⇒ Object

Import private key from base58 fromat as described in en.bitcoin.it/wiki/Private_key#Base_58_Wallet_Import_format and en.bitcoin.it/wiki/Base58Check_encoding#Encoding_a_private_key. See also #to_base58


24
25
26
27
28
29
30
31
# File 'lib/bitcoin/key.rb', line 24

def self.from_base58(str)
  hex = Bitcoin.decode_base58(str)
  compressed = hex.size == 76
  version, key, flag, checksum = hex.unpack("a2a64a#{compressed ? 2 : 0}a8")
  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

.from_bip38(encrypted_privkey, passphrase) ⇒ Object

Import private key from bip38 (non-ec-multiply) fromat as described in github.com/bitcoin/bips/blob/master/bip-0038.mediawiki See also #to_bip38


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/bitcoin/key.rb', line 206

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

.from_warp(passphrase, salt = "", compressed = false) ⇒ Object

Import private key from warp fromat as described in github.com/keybase/warpwallet keybase.io/warp/


240
241
242
243
244
245
246
247
248
249
# File 'lib/bitcoin/key.rb', line 240

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

.generate(opts = {compressed: true}) ⇒ Object

Generate a new keypair.

Bitcoin::Key.generate

16
17
18
# File 'lib/bitcoin/key.rb', line 16

def self.generate(opts={compressed: true})
  k = new(nil, nil, opts); k.generate; k
end

.recover_compact_signature_to_key(data, signature_base64) ⇒ Object

Thanks to whoever wrote pastebin.com/bQtdDzHx for help with compact signatures

Given data and a compact signature (65 bytes, base64-encoded to a larger string), recover the public components of the key whose private counterpart validly signed data.

If the signature validly signed data, create a new Key having the signing public key and address. Otherwise return nil.

Be sure to check that the returned Key matches the one you were expecting! Otherwise you are merely checking that someone validly signed the data.


152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/bitcoin/key.rb', line 152

def self.recover_compact_signature_to_key(data, signature_base64)
  signature = signature_base64.unpack("m0")[0]
  return nil if signature.size != 65

  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)
  pub_hex = Bitcoin::OpenSSL_EC.recover_public_key_from_signature(hash, signature, version-27, compressed)
  return nil unless pub_hex

  Key.new(nil, pub_hex)
end

.verify_message(address, signature, message) ⇒ Object


135
136
137
# File 'lib/bitcoin/key.rb', line 135

def self.verify_message(address, signature, message)
  Bitcoin.verify_message(address, signature, message)
end

Instance Method Details

#==(other) ⇒ Object


33
34
35
# File 'lib/bitcoin/key.rb', line 33

def ==(other)
  self.priv == other.priv
end

#addrObject

Get the address corresponding to the public key.


102
103
104
# File 'lib/bitcoin/key.rb', line 102

def addr
  Bitcoin.hash160_to_address(hash160)
end

#compressedObject


87
88
89
# File 'lib/bitcoin/key.rb', line 87

def compressed
  @pubkey_compressed
end

#generateObject

Generate new priv/pub key.


50
51
52
# File 'lib/bitcoin/key.rb', line 50

def generate
  @key.generate_key
end

#hash160Object

Get the hash160 of the public key.


97
98
99
# File 'lib/bitcoin/key.rb', line 97

def hash160
  Bitcoin.hash160(pub)
end

#privObject

Get the private key (in hex).


55
56
57
58
# File 'lib/bitcoin/key.rb', line 55

def priv
  return nil  unless @key.private_key
  @key.private_key.to_hex.rjust(64, '0')
end

#priv=(priv) ⇒ Object

Set the private key to priv (in hex).


61
62
63
64
# File 'lib/bitcoin/key.rb', line 61

def priv= priv
  set_priv(priv)
  regenerate_pubkey
end

#pubObject

Get the public key (in hex). In case the key was initialized with only a private key, the public key is regenerated.


69
70
71
72
73
# File 'lib/bitcoin/key.rb', line 69

def pub
  regenerate_pubkey unless @key.public_key
  return nil        unless @key.public_key
  @pubkey_compressed ? pub_compressed : pub_uncompressed
end

#pub=(pub) ⇒ Object

Set the public key (in hex).


92
93
94
# File 'lib/bitcoin/key.rb', line 92

def pub= pub
  set_pub(pub)
end

#pub_compressedObject


75
76
77
78
79
# File 'lib/bitcoin/key.rb', line 75

def pub_compressed
  public_key = @key.public_key
  public_key.group.point_conversion_form = :compressed
  public_key.to_hex.rjust(66, '0')
end

#pub_uncompressedObject


81
82
83
84
85
# File 'lib/bitcoin/key.rb', line 81

def pub_uncompressed
  public_key = @key.public_key
  public_key.group.point_conversion_form = :uncompressed
  public_key.to_hex.rjust(130, '0')
end

#sign(data) ⇒ Object

Sign data with the key.

key1 = Bitcoin::Key.generate
sig = key1.sign("some data")

109
110
111
# File 'lib/bitcoin/key.rb', line 109

def sign(data)
  Bitcoin.sign_data(key, data)
end

#sign_message(message) ⇒ Object


127
128
129
# File 'lib/bitcoin/key.rb', line 127

def sign_message(message)
  Bitcoin.sign_message(priv, pub, message)['signature']
end

#to_base58Object

Export private key to base58 format. See also Key.from_base58


170
171
172
173
174
175
# File 'lib/bitcoin/key.rb', line 170

def to_base58
  data = Bitcoin.network[:privkey_version] + priv
  data += "01"  if @pubkey_compressed
  hex  = data + Bitcoin.checksum(data)
  Bitcoin.int_to_base58( hex.to_i(16) )
end

#to_bip38(passphrase) ⇒ Object

Export private key to bip38 (non-ec-multiply) format as described in github.com/bitcoin/bips/blob/master/bip-0038.mediawiki See also Key.from_bip38


181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/bitcoin/key.rb', line 181

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

#verify(data, sig) ⇒ Object

Verify signature sig for data.

key2 = Bitcoin::Key.new(nil, key1.pub)
key2.verify("some data", sig)

116
117
118
119
120
121
122
123
124
# File 'lib/bitcoin/key.rb', line 116

def verify(data, sig)
  regenerate_pubkey unless @key.public_key
  sig = Bitcoin::OpenSSL_EC.repack_der_signature(sig)
  if sig
    @key.dsa_verify_asn1(data, sig)
  else
    false
  end
end

#verify_message(signature, message) ⇒ Object


131
132
133
# File 'lib/bitcoin/key.rb', line 131

def verify_message(signature, message)
  Bitcoin.verify_message(addr, signature, message)
end