diff --git a/TODOLIST b/TODOLIST index 08b1d6f8..988fbf14 100644 --- a/TODOLIST +++ b/TODOLIST @@ -5,7 +5,6 @@ security: wallet, transactions : - - support compressed keys - dust sweeping - transactions with multiple outputs - BIP 32 diff --git a/lib/bitcoin.py b/lib/bitcoin.py index c8e8ba04..eb205004 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -43,8 +43,57 @@ Hash = lambda x: hashlib.sha256(hashlib.sha256(x).digest()).digest() hash_encode = lambda x: x[::-1].encode('hex') hash_decode = lambda x: x.decode('hex')[::-1] -############ functions from pywallet ##################### +# pywallet openssl private key implementation + +def i2d_ECPrivateKey(pkey, compressed=False): + if compressed: + key = '3081d30201010420' + \ + '%064x' % pkey.secret + \ + 'a081a53081a2020101302c06072a8648ce3d0101022100' + \ + '%064x' % _p + \ + '3006040100040107042102' + \ + '%064x' % _Gx + \ + '022100' + \ + '%064x' % _r + \ + '020101a124032200' + else: + key = '308201130201010420' + \ + '%064x' % pkey.secret + \ + 'a081a53081a2020101302c06072a8648ce3d0101022100' + \ + '%064x' % _p + \ + '3006040100040107044104' + \ + '%064x' % _Gx + \ + '%064x' % _Gy + \ + '022100' + \ + '%064x' % _r + \ + '020101a144034200' + + return key.decode('hex') + i2o_ECPublicKey(pkey, compressed) + +def i2o_ECPublicKey(pkey, compressed=False): + # public keys are 65 bytes long (520 bits) + # 0x04 + 32-byte X-coordinate + 32-byte Y-coordinate + # 0x00 = point at infinity, 0x02 and 0x03 = compressed, 0x04 = uncompressed + # compressed keys: where is 0x02 if y is even and 0x03 if y is odd + if compressed: + if pkey.pubkey.point.y() & 1: + key = '03' + '%064x' % pkey.pubkey.point.x() + else: + key = '02' + '%064x' % pkey.pubkey.point.x() + else: + key = '04' + \ + '%064x' % pkey.pubkey.point.x() + \ + '%064x' % pkey.pubkey.point.y() + + return key.decode('hex') + +# end pywallet openssl private key implementation + + + +############ functions from pywallet ##################### + addrtype = 0 def hash_160(public_key): @@ -151,17 +200,39 @@ def DecodeBase58Check(psz): def PrivKeyToSecret(privkey): return privkey[9:9+32] -def SecretToASecret(secret): - vchIn = chr(addrtype+128) + secret +def SecretToASecret(secret, compressed=False): + vchIn = chr((addrtype+128)&255) + secret + if compressed: vchIn += '\01' return EncodeBase58Check(vchIn) def ASecretToSecret(key): vch = DecodeBase58Check(key) - if vch and vch[0] == chr(addrtype+128): + if vch and vch[0] == chr((addrtype+128)&255): return vch[1:] else: return False +def regenerate_key(sec): + b = ASecretToSecret(sec) + if not b: + return False + b = b[0:32] + secret = int('0x' + b.encode('hex'), 16) + return EC_KEY(secret) + +def GetPubKey(pkey, compressed=False): + return i2o_ECPublicKey(pkey, compressed) + +def GetPrivKey(pkey, compressed=False): + return i2d_ECPrivateKey(pkey, compressed) + +def GetSecret(pkey): + return ('%064x' % pkey.secret).decode('hex') + +def is_compressed(sec): + b = ASecretToSecret(sec) + return len(b) == 33 + ########### end pywallet functions ####################### # secp256k1, http://www.oid-info.com/get/1.3.132.0.10 @@ -176,6 +247,13 @@ generator_secp256k1 = ecdsa.ellipticcurve.Point( curve_secp256k1, _Gx, _Gy, _r ) oid_secp256k1 = (1,3,132,0,10) SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 ) +class EC_KEY(object): + def __init__( self, secret ): + self.pubkey = ecdsa.ecdsa.Public_key( generator_secp256k1, generator_secp256k1 * secret ) + self.privkey = ecdsa.ecdsa.Private_key( self.pubkey, secret ) + self.secret = secret + + def filter(s): out = re.sub('( [^\n]*|)\n','',s) @@ -195,7 +273,6 @@ def raw_tx( inputs, outputs, for_sig = None ): sig = sig + chr(1) # hashtype script = int_to_hex( len(sig)) + ' push %d bytes\n'%len(sig) script += sig.encode('hex') + ' sig\n' - pubkey = chr(4) + pubkey script += int_to_hex( len(pubkey)) + ' push %d bytes\n'%len(pubkey) script += pubkey.encode('hex') + ' pubkey\n' elif for_sig==i: diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py index a4137105..dc6ad13a 100644 --- a/lib/exchange_rate.py +++ b/lib/exchange_rate.py @@ -28,8 +28,11 @@ class Exchanger(threading.Thread): self.discovery() def discovery(self): - connection = httplib.HTTPSConnection('blockchain.info') - connection.request("GET", "/ticker") + try: + connection = httplib.HTTPSConnection('blockchain.info') + connection.request("GET", "/ticker") + except: + return response = connection.getresponse() if response.reason == httplib.responses[httplib.NOT_FOUND]: return @@ -43,9 +46,12 @@ class Exchanger(threading.Thread): self.parent.emit(SIGNAL("refresh_balance()")) except KeyError: pass + + def get_currencies(self): + return [] if self.quote_currencies == None else sorted(self.quote_currencies.keys()) def _lookup_rate(self, response, quote_id): - return decimal.Decimal(response[str(quote_id)]["15m"]) + return decimal.Decimal(str(response[str(quote_id)]["15m"])) if __name__ == "__main__": exch = Exchanger(("BRL", "CNY", "EUR", "GBP", "RUB", "USD")) diff --git a/lib/gui_qt.py b/lib/gui_qt.py index 1c364af8..6b65594f 100644 --- a/lib/gui_qt.py +++ b/lib/gui_qt.py @@ -38,6 +38,7 @@ except: from wallet import format_satoshis import bmp, mnemonic, pyqrnative, qrscanner +import exchange_rate from decimal import Decimal @@ -335,6 +336,9 @@ class ElectrumWindow(QMainWindow): self.connect(self, QtCore.SIGNAL('updatesignal'), self.update_wallet) #self.connect(self, SIGNAL('editamount'), self.edit_amount) self.history_list.setFocus(True) + + self.exchanger = exchange_rate.Exchanger(self) + self.connect(self, SIGNAL("refresh_balance()"), self.update_wallet) # dark magic fix by flatfly; https://bitcointalk.org/index.php?topic=73651.msg959913#msg959913 if platform.system() == 'Windows': @@ -384,6 +388,7 @@ class ElectrumWindow(QMainWindow): c, u = self.wallet.get_balance() text = _( "Balance" ) + ": %s "%( format_satoshis(c,False,self.wallet.num_zeros) ) if u: text += "[%s unconfirmed]"%( format_satoshis(u,True,self.wallet.num_zeros).strip() ) + text += self.create_quote_text(Decimal(c+u)/100000000) icon = QIcon(":icons/status_connected.png") else: text = _( "Not connected" ) @@ -402,7 +407,15 @@ class ElectrumWindow(QMainWindow): self.update_contacts_tab() self.update_completions() - + def create_quote_text(self, btc_balance): + quote_currency = self.config.get("currency", "None") + quote_balance = self.exchanger.exchange(btc_balance, quote_currency) + if quote_balance is None: + quote_text = "" + else: + quote_text = " (%.2f %s)" % (quote_balance, quote_currency) + return quote_text + def create_history_tab(self): self.history_list = l = MyTreeWidget(self) l.setColumnCount(5) @@ -1512,16 +1525,16 @@ class ElectrumWindow(QMainWindow): tabs = QTabWidget(self) vbox.addWidget(tabs) - tab = QWidget() - grid_wallet = QGridLayout(tab) - grid_wallet.setColumnStretch(0,1) - tabs.addTab(tab, _('Wallet') ) - tab2 = QWidget() grid_ui = QGridLayout(tab2) grid_ui.setColumnStretch(0,1) tabs.addTab(tab2, _('Display') ) + tab = QWidget() + grid_wallet = QGridLayout(tab) + grid_wallet.setColumnStretch(0,1) + tabs.addTab(tab, _('Wallet') ) + fee_label = QLabel(_('Transaction fee')) grid_wallet.addWidget(fee_label, 2, 0) fee_e = QLineEdit() @@ -1575,23 +1588,19 @@ class ElectrumWindow(QMainWindow): gui_label=QLabel(_('Default GUI') + ':') grid_ui.addWidget(gui_label , 7, 0) gui_combo = QComboBox() - gui_combo.addItems(['Lite', 'Classic', 'Gtk', 'Text']) + gui_combo.addItems(['Lite', 'Classic']) index = gui_combo.findText(self.config.get("gui","classic").capitalize()) if index==-1: index = 1 gui_combo.setCurrentIndex(index) grid_ui.addWidget(gui_combo, 7, 1) - grid_ui.addWidget(HelpButton(_('Select which GUI mode to use at start up. ')), 7, 2) + grid_ui.addWidget(HelpButton(_('Select which GUI mode to use at start up.'+'\n'+'Note: use the command line to access the "text" and "gtk" GUIs')), 7, 2) if not self.config.is_modifiable('gui'): for w in [gui_combo, gui_label]: w.setEnabled(False) lang_label=QLabel(_('Language') + ':') grid_ui.addWidget(lang_label , 8, 0) lang_combo = QComboBox() - languages = {'':_('Default'), 'br':_('Brasilian'), 'cs':_('Czech'), 'de':_('German'), - 'eo':_('Esperanto'), 'en':_('English'), 'es':_('Spanish'), 'fr':_('French'), - 'it':_('Italian'), 'lv':_('Latvian'), 'nl':_('Dutch'), 'ru':_('Russian'), - 'sl':_('Slovenian'), 'vi':_('Vietnamese'), 'zh':_('Chinese') - } + from i18n import languages lang_combo.addItems(languages.values()) try: index = languages.keys().index(self.config.get("language",'')) @@ -1603,19 +1612,33 @@ class ElectrumWindow(QMainWindow): if not self.config.is_modifiable('language'): for w in [lang_combo, lang_label]: w.setEnabled(False) + currencies = self.exchanger.get_currencies() + currencies.insert(0, "None") + cur_label=QLabel(_('Currency') + ':') + grid_ui.addWidget(cur_label , 9, 0) + cur_combo = QComboBox() + cur_combo.addItems(currencies) + try: + index = currencies.index(self.config.get('currency', "None")) + except: + index = 0 + cur_combo.setCurrentIndex(index) + grid_ui.addWidget(cur_combo, 9, 1) + grid_ui.addWidget(HelpButton(_('Select which currency is used for quotes. ')), 9, 2) + view_label=QLabel(_('Receive Tab') + ':') - grid_ui.addWidget(view_label , 9, 0) + grid_ui.addWidget(view_label , 10, 0) view_combo = QComboBox() view_combo.addItems([_('Simple'), _('Advanced'), _('Point of Sale')]) view_combo.setCurrentIndex(self.receive_tab_mode) - grid_ui.addWidget(view_combo, 9, 1) + grid_ui.addWidget(view_combo, 10, 1) hh = _('This selects the interaction mode of the "Receive" tab. ') + '\n\n' \ + _('Simple') + ': ' + _('Show only addresses and labels.') + '\n\n' \ + _('Advanced') + ': ' + _('Show address balances and add extra menu items to freeze/prioritize addresses.') + '\n\n' \ + _('Point of Sale') + ': ' + _('Show QR code window and amounts requested for each address. Add menu item to request amount.') + '\n\n' - grid_ui.addWidget(HelpButton(hh), 9, 2) + grid_ui.addWidget(HelpButton(hh), 10, 2) vbox.addLayout(ok_cancel_buttons(d)) d.setLayout(vbox) @@ -1678,6 +1701,11 @@ class ElectrumWindow(QMainWindow): if lang_request != self.config.get('language'): self.config.set_key("language", lang_request, True) need_restart = True + + cur_request = str(currencies[cur_combo.currentIndex()]) + if cur_request != self.config.get('currency', "None"): + self.config.set_key('currency', cur_request, True) + self.update_wallet() if need_restart: QMessageBox.warning(self, _('Success'), _('Please restart Electrum to activate the new GUI settings'), _('OK')) diff --git a/lib/i18n.py b/lib/i18n.py index 7ef4458f..9436219c 100644 --- a/lib/i18n.py +++ b/lib/i18n.py @@ -34,3 +34,20 @@ def set_language(x): if x: language = gettext.translation('electrum', LOCALE_DIR, fallback = True, languages=[x]) +languages = { + '':_('Default'), + 'br':_('Brasilian'), + 'cs':_('Czech'), + 'de':_('German'), + 'eo':_('Esperanto'), + 'en':_('English'), + 'es':_('Spanish'), + 'fr':_('French'), + 'it':_('Italian'), + 'lv':_('Latvian'), + 'nl':_('Dutch'), + 'ru':_('Russian'), + 'sl':_('Slovenian'), + 'vi':_('Vietnamese'), + 'zh':_('Chinese') + } diff --git a/lib/simple_config.py b/lib/simple_config.py index a94b837e..8cfa24dd 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -94,7 +94,7 @@ a SimpleConfig instance then reads the wallet file. try: out = ast.literal_eval(out) except: - print "type error, using default value" + print "type error for '%s': using default value"%key out = default return out diff --git a/lib/wallet.py b/lib/wallet.py index 648b30d3..9cde7c89 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -113,22 +113,33 @@ class Wallet: while not self.is_up_to_date(): time.sleep(0.1) def import_key(self, keypair, password): - address, key = keypair.split(':') + + address, sec = keypair.split(':') if not self.is_valid(address): raise BaseException('Invalid Bitcoin address') if address in self.all_addresses(): raise BaseException('Address already in wallet') - b = ASecretToSecret( key ) - if not b: - raise BaseException('Unsupported key format') - secexp = int( b.encode('hex'), 16) - private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 ) + + # rebuild public key from private key, compressed or uncompressed + pkey = regenerate_key(sec) + if not pkey: + return False + + # figure out if private key is compressed + compressed = is_compressed(sec) + + # rebuild private and public key from regenerated secret + private_key = GetPrivKey(pkey, compressed) + public_key = GetPubKey(pkey, compressed) + addr = public_key_to_bc_address(public_key) + # sanity check - public_key = private_key.get_verifying_key() - if not address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ): + if not address == addr : raise BaseException('Address does not match private key') - self.imported_keys[address] = self.pw_encode( key, password ) - + + # store the originally requested keypair into the imported keys table + self.imported_keys[address] = self.pw_encode(sec, password ) + def new_seed(self, password): seed = "%032x"%ecdsa.util.randrange( pow(2,128) ) @@ -172,19 +183,23 @@ class Wallet: return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) ) def get_private_key_base58(self, address, password): - pk = self.get_private_key(address, password) - if pk is None: return None - return SecretToASecret( pk ) + secexp, compressed = self.get_private_key(address, password) + if secexp is None: return None + pk = number_to_string( secexp, generator_secp256k1.order() ) + return SecretToASecret( pk, compressed ) def get_private_key(self, address, password): """ Privatekey(type,n) = Master_private_key + H(n|S|type) """ order = generator_secp256k1.order() if address in self.imported_keys.keys(): - b = self.pw_decode( self.imported_keys[address], password ) - if not b: return None - b = ASecretToSecret( b ) - secexp = int( b.encode('hex'), 16) + sec = self.pw_decode( self.imported_keys[address], password ) + if not sec: return None, None + + pkey = regenerate_key(sec) + compressed = is_compressed(sec) + secexp = pkey.secret + else: if address in self.addresses: n = self.addresses.index(address) @@ -201,20 +216,21 @@ class Wallet: if not seed: return None secexp = self.stretch_key(seed) secexp = ( secexp + self.get_sequence(n,for_change) ) % order + compressed = False - pk = number_to_string(secexp,order) - return pk + return secexp, compressed def msg_magic(self, message): return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message def sign_message(self, address, message, password): - private_key = ecdsa.SigningKey.from_string( self.get_private_key(address, password), curve = SECP256k1 ) + secexp, compressed = self.get_private_key(address, password) + private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) public_key = private_key.get_verifying_key() signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string ) assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string) for i in range(4): - sig = base64.b64encode( chr(27+i) + signature ) + sig = base64.b64encode( chr(27 + i + (4 if compressed else 0)) + signature ) try: self.verify_message( address, sig, message) return sig @@ -598,9 +614,13 @@ class Wallet: s_inputs = [] for i in range(len(inputs)): addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i] - private_key = ecdsa.SigningKey.from_string( self.get_private_key(addr, password), curve = SECP256k1 ) + secexp, compressed = self.get_private_key(addr, password) + private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) public_key = private_key.get_verifying_key() - pubkey = public_key.to_string() + + pkey = EC_KEY(secexp) + pubkey = GetPubKey(pkey, compressed) + tx = filter( raw_tx( inputs, outputs, for_sig = i ) ) sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der ) assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)