Browse Source
- separation between Wallet and key management (Keystore) - simplification of wallet classes - remove support for multiple accounts in the same wallet - add support for OP_RETURN to Trezor plugin - split multi-accounts wallets for backward compatibilitymaster
35 changed files with 1737 additions and 2052 deletions
@ -1,381 +0,0 @@ |
|||||||
#!/usr/bin/env python |
|
||||||
# |
|
||||||
# Electrum - lightweight Bitcoin client |
|
||||||
# Copyright (C) 2013 thomasv@gitorious |
|
||||||
# |
|
||||||
# Permission is hereby granted, free of charge, to any person |
|
||||||
# obtaining a copy of this software and associated documentation files |
|
||||||
# (the "Software"), to deal in the Software without restriction, |
|
||||||
# including without limitation the rights to use, copy, modify, merge, |
|
||||||
# publish, distribute, sublicense, and/or sell copies of the Software, |
|
||||||
# and to permit persons to whom the Software is furnished to do so, |
|
||||||
# subject to the following conditions: |
|
||||||
# |
|
||||||
# The above copyright notice and this permission notice shall be |
|
||||||
# included in all copies or substantial portions of the Software. |
|
||||||
# |
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
||||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|
||||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|
||||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
||||||
# SOFTWARE. |
|
||||||
|
|
||||||
import bitcoin |
|
||||||
from bitcoin import * |
|
||||||
from i18n import _ |
|
||||||
from transaction import Transaction, is_extended_pubkey |
|
||||||
from util import InvalidPassword |
|
||||||
|
|
||||||
|
|
||||||
class Account(object): |
|
||||||
def __init__(self, v): |
|
||||||
self.receiving_pubkeys = v.get('receiving', []) |
|
||||||
self.change_pubkeys = v.get('change', []) |
|
||||||
# addresses will not be stored on disk |
|
||||||
self.receiving_addresses = map(self.pubkeys_to_address, self.receiving_pubkeys) |
|
||||||
self.change_addresses = map(self.pubkeys_to_address, self.change_pubkeys) |
|
||||||
|
|
||||||
def dump(self): |
|
||||||
return {'receiving':self.receiving_pubkeys, 'change':self.change_pubkeys} |
|
||||||
|
|
||||||
def get_pubkey(self, for_change, n): |
|
||||||
pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys |
|
||||||
return pubkeys_list[n] |
|
||||||
|
|
||||||
def get_address(self, for_change, n): |
|
||||||
addr_list = self.change_addresses if for_change else self.receiving_addresses |
|
||||||
return addr_list[n] |
|
||||||
|
|
||||||
def get_pubkeys(self, for_change, n): |
|
||||||
return [ self.get_pubkey(for_change, n)] |
|
||||||
|
|
||||||
def get_addresses(self, for_change): |
|
||||||
addr_list = self.change_addresses if for_change else self.receiving_addresses |
|
||||||
return addr_list[:] |
|
||||||
|
|
||||||
def derive_pubkeys(self, for_change, n): |
|
||||||
pass |
|
||||||
|
|
||||||
def create_new_address(self, for_change): |
|
||||||
pubkeys_list = self.change_pubkeys if for_change else self.receiving_pubkeys |
|
||||||
addr_list = self.change_addresses if for_change else self.receiving_addresses |
|
||||||
n = len(pubkeys_list) |
|
||||||
pubkeys = self.derive_pubkeys(for_change, n) |
|
||||||
address = self.pubkeys_to_address(pubkeys) |
|
||||||
pubkeys_list.append(pubkeys) |
|
||||||
addr_list.append(address) |
|
||||||
return address |
|
||||||
|
|
||||||
def pubkeys_to_address(self, pubkey): |
|
||||||
return public_key_to_bc_address(pubkey.decode('hex')) |
|
||||||
|
|
||||||
def has_change(self): |
|
||||||
return True |
|
||||||
|
|
||||||
def get_name(self, k): |
|
||||||
return _('Main account') |
|
||||||
|
|
||||||
def redeem_script(self, for_change, n): |
|
||||||
return None |
|
||||||
|
|
||||||
def is_used(self, wallet): |
|
||||||
addresses = self.get_addresses(False) |
|
||||||
return any(wallet.address_is_old(a, -1) for a in addresses) |
|
||||||
|
|
||||||
def synchronize_sequence(self, wallet, for_change): |
|
||||||
limit = wallet.gap_limit_for_change if for_change else wallet.gap_limit |
|
||||||
while True: |
|
||||||
addresses = self.get_addresses(for_change) |
|
||||||
if len(addresses) < limit: |
|
||||||
address = self.create_new_address(for_change) |
|
||||||
wallet.add_address(address) |
|
||||||
continue |
|
||||||
if map( lambda a: wallet.address_is_old(a), addresses[-limit:] ) == limit*[False]: |
|
||||||
break |
|
||||||
else: |
|
||||||
address = self.create_new_address(for_change) |
|
||||||
wallet.add_address(address) |
|
||||||
|
|
||||||
def synchronize(self, wallet): |
|
||||||
self.synchronize_sequence(wallet, False) |
|
||||||
self.synchronize_sequence(wallet, True) |
|
||||||
|
|
||||||
|
|
||||||
class ImportedAccount(Account): |
|
||||||
def __init__(self, d): |
|
||||||
self.keypairs = d['imported'] |
|
||||||
|
|
||||||
def synchronize(self, wallet): |
|
||||||
return |
|
||||||
|
|
||||||
def get_addresses(self, for_change): |
|
||||||
return [] if for_change else sorted(self.keypairs.keys()) |
|
||||||
|
|
||||||
def get_pubkey(self, *sequence): |
|
||||||
for_change, i = sequence |
|
||||||
assert for_change == 0 |
|
||||||
addr = self.get_addresses(0)[i] |
|
||||||
return self.keypairs[addr][0] |
|
||||||
|
|
||||||
def get_xpubkeys(self, for_change, n): |
|
||||||
return self.get_pubkeys(for_change, n) |
|
||||||
|
|
||||||
def get_private_key(self, sequence, wallet, password): |
|
||||||
from wallet import pw_decode |
|
||||||
for_change, i = sequence |
|
||||||
assert for_change == 0 |
|
||||||
address = self.get_addresses(0)[i] |
|
||||||
pk = pw_decode(self.keypairs[address][1], password) |
|
||||||
# this checks the password |
|
||||||
if address != address_from_private_key(pk): |
|
||||||
raise InvalidPassword() |
|
||||||
return [pk] |
|
||||||
|
|
||||||
def has_change(self): |
|
||||||
return False |
|
||||||
|
|
||||||
def add(self, address, pubkey, privkey, password): |
|
||||||
from wallet import pw_encode |
|
||||||
self.keypairs[address] = [pubkey, pw_encode(privkey, password)] |
|
||||||
|
|
||||||
def remove(self, address): |
|
||||||
self.keypairs.pop(address) |
|
||||||
|
|
||||||
def dump(self): |
|
||||||
return {'imported':self.keypairs} |
|
||||||
|
|
||||||
def get_name(self, k): |
|
||||||
return _('Imported keys') |
|
||||||
|
|
||||||
def update_password(self, old_password, new_password): |
|
||||||
for k, v in self.keypairs.items(): |
|
||||||
pubkey, a = v |
|
||||||
b = pw_decode(a, old_password) |
|
||||||
c = pw_encode(b, new_password) |
|
||||||
self.keypairs[k] = (pubkey, c) |
|
||||||
|
|
||||||
|
|
||||||
class OldAccount(Account): |
|
||||||
""" Privatekey(type,n) = Master_private_key + H(n|S|type) """ |
|
||||||
|
|
||||||
def __init__(self, v): |
|
||||||
Account.__init__(self, v) |
|
||||||
self.mpk = v['mpk'].decode('hex') |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def mpk_from_seed(klass, seed): |
|
||||||
secexp = klass.stretch_key(seed) |
|
||||||
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) |
|
||||||
master_public_key = master_private_key.get_verifying_key().to_string().encode('hex') |
|
||||||
return master_public_key |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def stretch_key(self,seed): |
|
||||||
oldseed = seed |
|
||||||
for i in range(100000): |
|
||||||
seed = hashlib.sha256(seed + oldseed).digest() |
|
||||||
return string_to_number( seed ) |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def get_sequence(self, mpk, for_change, n): |
|
||||||
return string_to_number( Hash( "%d:%d:"%(n,for_change) + mpk ) ) |
|
||||||
|
|
||||||
def get_address(self, for_change, n): |
|
||||||
pubkey = self.get_pubkey(for_change, n) |
|
||||||
address = public_key_to_bc_address( pubkey.decode('hex') ) |
|
||||||
return address |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def get_pubkey_from_mpk(self, mpk, for_change, n): |
|
||||||
z = self.get_sequence(mpk, for_change, n) |
|
||||||
master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1) |
|
||||||
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator |
|
||||||
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1) |
|
||||||
return '04' + public_key2.to_string().encode('hex') |
|
||||||
|
|
||||||
def derive_pubkeys(self, for_change, n): |
|
||||||
return self.get_pubkey_from_mpk(self.mpk, for_change, n) |
|
||||||
|
|
||||||
def get_private_key_from_stretched_exponent(self, for_change, n, secexp): |
|
||||||
order = generator_secp256k1.order() |
|
||||||
secexp = ( secexp + self.get_sequence(self.mpk, for_change, n) ) % order |
|
||||||
pk = number_to_string( secexp, generator_secp256k1.order() ) |
|
||||||
compressed = False |
|
||||||
return SecretToASecret( pk, compressed ) |
|
||||||
|
|
||||||
|
|
||||||
def get_private_key(self, sequence, wallet, password): |
|
||||||
seed = wallet.get_seed(password) |
|
||||||
self.check_seed(seed) |
|
||||||
for_change, n = sequence |
|
||||||
secexp = self.stretch_key(seed) |
|
||||||
pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp) |
|
||||||
return [pk] |
|
||||||
|
|
||||||
|
|
||||||
def check_seed(self, seed): |
|
||||||
secexp = self.stretch_key(seed) |
|
||||||
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) |
|
||||||
master_public_key = master_private_key.get_verifying_key().to_string() |
|
||||||
if master_public_key != self.mpk: |
|
||||||
print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex')) |
|
||||||
raise InvalidPassword() |
|
||||||
return True |
|
||||||
|
|
||||||
def get_master_pubkeys(self): |
|
||||||
return [self.mpk.encode('hex')] |
|
||||||
|
|
||||||
def get_type(self): |
|
||||||
return _('Old Electrum format') |
|
||||||
|
|
||||||
def get_xpubkeys(self, for_change, n): |
|
||||||
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n))) |
|
||||||
mpk = self.mpk.encode('hex') |
|
||||||
x_pubkey = 'fe' + mpk + s |
|
||||||
return [ x_pubkey ] |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def parse_xpubkey(self, x_pubkey): |
|
||||||
assert is_extended_pubkey(x_pubkey) |
|
||||||
pk = x_pubkey[2:] |
|
||||||
mpk = pk[0:128] |
|
||||||
dd = pk[128:] |
|
||||||
s = [] |
|
||||||
while dd: |
|
||||||
n = int(bitcoin.rev_hex(dd[0:4]), 16) |
|
||||||
dd = dd[4:] |
|
||||||
s.append(n) |
|
||||||
assert len(s) == 2 |
|
||||||
return mpk, s |
|
||||||
|
|
||||||
|
|
||||||
class BIP32_Account(Account): |
|
||||||
|
|
||||||
def __init__(self, v): |
|
||||||
Account.__init__(self, v) |
|
||||||
self.xpub = v['xpub'] |
|
||||||
self.xpub_receive = None |
|
||||||
self.xpub_change = None |
|
||||||
|
|
||||||
def dump(self): |
|
||||||
d = Account.dump(self) |
|
||||||
d['xpub'] = self.xpub |
|
||||||
return d |
|
||||||
|
|
||||||
def first_address(self): |
|
||||||
pubkeys = self.derive_pubkeys(0, 0) |
|
||||||
addr = self.pubkeys_to_address(pubkeys) |
|
||||||
return addr, pubkeys |
|
||||||
|
|
||||||
def get_master_pubkeys(self): |
|
||||||
return [self.xpub] |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def derive_pubkey_from_xpub(self, xpub, for_change, n): |
|
||||||
_, _, _, c, cK = deserialize_xkey(xpub) |
|
||||||
for i in [for_change, n]: |
|
||||||
cK, c = CKD_pub(cK, c, i) |
|
||||||
return cK.encode('hex') |
|
||||||
|
|
||||||
def get_pubkey_from_xpub(self, xpub, for_change, n): |
|
||||||
xpubs = self.get_master_pubkeys() |
|
||||||
i = xpubs.index(xpub) |
|
||||||
pubkeys = self.get_pubkeys(for_change, n) |
|
||||||
return pubkeys[i] |
|
||||||
|
|
||||||
def derive_pubkeys(self, for_change, n): |
|
||||||
xpub = self.xpub_change if for_change else self.xpub_receive |
|
||||||
if xpub is None: |
|
||||||
xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change) |
|
||||||
if for_change: |
|
||||||
self.xpub_change = xpub |
|
||||||
else: |
|
||||||
self.xpub_receive = xpub |
|
||||||
_, _, _, c, cK = deserialize_xkey(xpub) |
|
||||||
cK, c = CKD_pub(cK, c, n) |
|
||||||
result = cK.encode('hex') |
|
||||||
return result |
|
||||||
|
|
||||||
|
|
||||||
def get_private_key(self, sequence, wallet, password): |
|
||||||
out = [] |
|
||||||
xpubs = self.get_master_pubkeys() |
|
||||||
roots = [k for k, v in wallet.master_public_keys.iteritems() if v in xpubs] |
|
||||||
for root in roots: |
|
||||||
xpriv = wallet.get_master_private_key(root, password) |
|
||||||
if not xpriv: |
|
||||||
continue |
|
||||||
_, _, _, c, k = deserialize_xkey(xpriv) |
|
||||||
pk = bip32_private_key( sequence, k, c ) |
|
||||||
out.append(pk) |
|
||||||
return out |
|
||||||
|
|
||||||
def get_type(self): |
|
||||||
return _('Standard 1 of 1') |
|
||||||
|
|
||||||
def get_xpubkeys(self, for_change, n): |
|
||||||
# unsorted |
|
||||||
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change,n))) |
|
||||||
xpubs = self.get_master_pubkeys() |
|
||||||
return map(lambda xpub: 'ff' + bitcoin.DecodeBase58Check(xpub).encode('hex') + s, xpubs) |
|
||||||
|
|
||||||
@classmethod |
|
||||||
def parse_xpubkey(self, pubkey): |
|
||||||
assert is_extended_pubkey(pubkey) |
|
||||||
pk = pubkey.decode('hex') |
|
||||||
pk = pk[1:] |
|
||||||
xkey = bitcoin.EncodeBase58Check(pk[0:78]) |
|
||||||
dd = pk[78:] |
|
||||||
s = [] |
|
||||||
while dd: |
|
||||||
n = int( bitcoin.rev_hex(dd[0:2].encode('hex')), 16) |
|
||||||
dd = dd[2:] |
|
||||||
s.append(n) |
|
||||||
assert len(s) == 2 |
|
||||||
return xkey, s |
|
||||||
|
|
||||||
def get_name(self, k): |
|
||||||
return "Main account" if k == '0' else "Account " + k |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Multisig_Account(BIP32_Account): |
|
||||||
|
|
||||||
def __init__(self, v): |
|
||||||
self.m = v.get('m', 2) |
|
||||||
Account.__init__(self, v) |
|
||||||
self.xpub_list = v['xpubs'] |
|
||||||
|
|
||||||
def dump(self): |
|
||||||
d = Account.dump(self) |
|
||||||
d['xpubs'] = self.xpub_list |
|
||||||
d['m'] = self.m |
|
||||||
return d |
|
||||||
|
|
||||||
def get_pubkeys(self, for_change, n): |
|
||||||
return self.get_pubkey(for_change, n) |
|
||||||
|
|
||||||
def derive_pubkeys(self, for_change, n): |
|
||||||
return map(lambda x: self.derive_pubkey_from_xpub(x, for_change, n), self.get_master_pubkeys()) |
|
||||||
|
|
||||||
def redeem_script(self, for_change, n): |
|
||||||
pubkeys = self.get_pubkeys(for_change, n) |
|
||||||
return Transaction.multisig_script(sorted(pubkeys), self.m) |
|
||||||
|
|
||||||
def pubkeys_to_address(self, pubkeys): |
|
||||||
redeem_script = Transaction.multisig_script(sorted(pubkeys), self.m) |
|
||||||
address = hash_160_to_bc_address(hash_160(redeem_script.decode('hex')), 5) |
|
||||||
return address |
|
||||||
|
|
||||||
def get_address(self, for_change, n): |
|
||||||
return self.pubkeys_to_address(self.get_pubkeys(for_change, n)) |
|
||||||
|
|
||||||
def get_master_pubkeys(self): |
|
||||||
return self.xpub_list |
|
||||||
|
|
||||||
def get_type(self): |
|
||||||
return _('Multisig %d of %d'%(self.m, len(self.xpub_list))) |
|
||||||
@ -0,0 +1,701 @@ |
|||||||
|
#!/usr/bin/env python2 |
||||||
|
# -*- mode: python -*- |
||||||
|
# |
||||||
|
# Electrum - lightweight Bitcoin client |
||||||
|
# Copyright (C) 2016 The Electrum developers |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person |
||||||
|
# obtaining a copy of this software and associated documentation files |
||||||
|
# (the "Software"), to deal in the Software without restriction, |
||||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||||
|
# subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be |
||||||
|
# included in all copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
|
||||||
|
from unicodedata import normalize |
||||||
|
|
||||||
|
from version import * |
||||||
|
import bitcoin |
||||||
|
from bitcoin import pw_encode, pw_decode, bip32_root, bip32_private_derivation, bip32_public_derivation, bip32_private_key, deserialize_xkey |
||||||
|
from bitcoin import public_key_from_private_key, public_key_to_bc_address |
||||||
|
from bitcoin import * |
||||||
|
|
||||||
|
from bitcoin import is_old_seed, is_new_seed |
||||||
|
from util import PrintError, InvalidPassword |
||||||
|
from mnemonic import Mnemonic |
||||||
|
|
||||||
|
|
||||||
|
class KeyStore(PrintError): |
||||||
|
|
||||||
|
def has_seed(self): |
||||||
|
return False |
||||||
|
|
||||||
|
def has_password(self): |
||||||
|
return False |
||||||
|
|
||||||
|
def is_watching_only(self): |
||||||
|
return False |
||||||
|
|
||||||
|
def can_import(self): |
||||||
|
return False |
||||||
|
|
||||||
|
|
||||||
|
class Software_KeyStore(KeyStore): |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
KeyStore.__init__(self) |
||||||
|
self.use_encryption = False |
||||||
|
|
||||||
|
def has_password(self): |
||||||
|
return self.use_encryption |
||||||
|
|
||||||
|
|
||||||
|
class Imported_KeyStore(Software_KeyStore): |
||||||
|
# keystore for imported private keys |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
Software_KeyStore.__init__(self) |
||||||
|
self.keypairs = {} |
||||||
|
|
||||||
|
def is_deterministic(self): |
||||||
|
return False |
||||||
|
|
||||||
|
def can_change_password(self): |
||||||
|
return True |
||||||
|
|
||||||
|
def get_master_public_key(self): |
||||||
|
return None |
||||||
|
|
||||||
|
def load(self, storage, name): |
||||||
|
self.keypairs = storage.get('keypairs', {}) |
||||||
|
self.use_encryption = storage.get('use_encryption', False) |
||||||
|
self.receiving_pubkeys = self.keypairs.keys() |
||||||
|
self.change_pubkeys = [] |
||||||
|
|
||||||
|
def save(self, storage, root_name): |
||||||
|
storage.put('key_type', 'imported') |
||||||
|
storage.put('keypairs', self.keypairs) |
||||||
|
storage.put('use_encryption', self.use_encryption) |
||||||
|
|
||||||
|
def can_import(self): |
||||||
|
return True |
||||||
|
|
||||||
|
def check_password(self, password): |
||||||
|
self.get_private_key((0,0), password) |
||||||
|
|
||||||
|
def import_key(self, sec, password): |
||||||
|
if not self.can_import(): |
||||||
|
raise BaseException('This wallet cannot import private keys') |
||||||
|
try: |
||||||
|
pubkey = public_key_from_private_key(sec) |
||||||
|
except Exception: |
||||||
|
raise Exception('Invalid private key') |
||||||
|
self.keypairs[pubkey] = sec |
||||||
|
return pubkey |
||||||
|
|
||||||
|
def delete_imported_key(self, key): |
||||||
|
self.keypairs.pop(key) |
||||||
|
|
||||||
|
def get_private_key(self, sequence, password): |
||||||
|
for_change, i = sequence |
||||||
|
assert for_change == 0 |
||||||
|
pubkey = (self.change_pubkeys if for_change else self.receiving_pubkeys)[i] |
||||||
|
pk = pw_decode(self.keypairs[pubkey], password) |
||||||
|
# this checks the password |
||||||
|
if pubkey != public_key_from_private_key(pk): |
||||||
|
raise InvalidPassword() |
||||||
|
return pk |
||||||
|
|
||||||
|
def update_password(self, old_password, new_password): |
||||||
|
if old_password is not None: |
||||||
|
self.check_password(old_password) |
||||||
|
if new_password == '': |
||||||
|
new_password = None |
||||||
|
for k, v in self.keypairs.items(): |
||||||
|
b = pw_decode(v, old_password) |
||||||
|
c = pw_encode(b, new_password) |
||||||
|
self.keypairs[k] = b |
||||||
|
self.use_encryption = (new_password is not None) |
||||||
|
|
||||||
|
|
||||||
|
class Deterministic_KeyStore(Software_KeyStore): |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
Software_KeyStore.__init__(self) |
||||||
|
self.seed = '' |
||||||
|
|
||||||
|
def is_deterministic(self): |
||||||
|
return True |
||||||
|
|
||||||
|
def load(self, storage, name): |
||||||
|
self.seed = storage.get('seed', '') |
||||||
|
self.use_encryption = storage.get('use_encryption', False) |
||||||
|
|
||||||
|
def save(self, storage, name): |
||||||
|
storage.put('seed', self.seed) |
||||||
|
storage.put('use_encryption', self.use_encryption) |
||||||
|
|
||||||
|
def has_seed(self): |
||||||
|
return self.seed != '' |
||||||
|
|
||||||
|
def can_change_password(self): |
||||||
|
return not self.is_watching_only() |
||||||
|
|
||||||
|
def add_seed(self, seed, password): |
||||||
|
if self.seed: |
||||||
|
raise Exception("a seed exists") |
||||||
|
self.seed_version, self.seed = self.format_seed(seed) |
||||||
|
if password: |
||||||
|
self.seed = pw_encode(self.seed, password) |
||||||
|
self.use_encryption = (password is not None) |
||||||
|
|
||||||
|
def get_seed(self, password): |
||||||
|
return pw_decode(self.seed, password).encode('utf8') |
||||||
|
|
||||||
|
|
||||||
|
class Xpub: |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
self.xpub = None |
||||||
|
self.xpub_receive = None |
||||||
|
self.xpub_change = None |
||||||
|
|
||||||
|
def add_master_public_key(self, xpub): |
||||||
|
self.xpub = xpub |
||||||
|
|
||||||
|
def get_master_public_key(self): |
||||||
|
return self.xpub |
||||||
|
|
||||||
|
def derive_pubkey(self, for_change, n): |
||||||
|
xpub = self.xpub_change if for_change else self.xpub_receive |
||||||
|
if xpub is None: |
||||||
|
xpub = bip32_public_derivation(self.xpub, "", "/%d"%for_change) |
||||||
|
if for_change: |
||||||
|
self.xpub_change = xpub |
||||||
|
else: |
||||||
|
self.xpub_receive = xpub |
||||||
|
_, _, _, c, cK = deserialize_xkey(xpub) |
||||||
|
cK, c = CKD_pub(cK, c, n) |
||||||
|
result = cK.encode('hex') |
||||||
|
return result |
||||||
|
|
||||||
|
def get_xpubkey(self, c, i): |
||||||
|
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (c, i))) |
||||||
|
return 'ff' + bitcoin.DecodeBase58Check(self.xpub).encode('hex') + s |
||||||
|
|
||||||
|
|
||||||
|
class BIP32_KeyStore(Deterministic_KeyStore, Xpub): |
||||||
|
root_derivation = "m/" |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
Xpub.__init__(self) |
||||||
|
Deterministic_KeyStore.__init__(self) |
||||||
|
self.xprv = None |
||||||
|
|
||||||
|
def format_seed(self, seed): |
||||||
|
return NEW_SEED_VERSION, ' '.join(seed.split()) |
||||||
|
|
||||||
|
def load(self, storage, name): |
||||||
|
Deterministic_KeyStore.load(self, storage, name) |
||||||
|
self.xpub = storage.get('master_public_keys', {}).get(name) |
||||||
|
self.xprv = storage.get('master_private_keys', {}).get(name) |
||||||
|
|
||||||
|
def save(self, storage, name): |
||||||
|
Deterministic_KeyStore.save(self, storage, name) |
||||||
|
d = storage.get('master_public_keys', {}) |
||||||
|
d[name] = self.xpub |
||||||
|
storage.put('master_public_keys', d) |
||||||
|
d = storage.get('master_private_keys', {}) |
||||||
|
d[name] = self.xprv |
||||||
|
storage.put('master_private_keys', d) |
||||||
|
|
||||||
|
def add_master_private_key(self, xprv, password): |
||||||
|
self.xprv = pw_encode(xprv, password) |
||||||
|
|
||||||
|
def get_master_private_key(self, password): |
||||||
|
return pw_decode(self.xprv, password) |
||||||
|
|
||||||
|
def check_password(self, password): |
||||||
|
xprv = pw_decode(self.xprv, password) |
||||||
|
if deserialize_xkey(xprv)[3] != deserialize_xkey(self.xpub)[3]: |
||||||
|
raise InvalidPassword() |
||||||
|
|
||||||
|
def update_password(self, old_password, new_password): |
||||||
|
if old_password is not None: |
||||||
|
self.check_password(old_password) |
||||||
|
if new_password == '': |
||||||
|
new_password = None |
||||||
|
if self.has_seed(): |
||||||
|
decoded = self.get_seed(old_password) |
||||||
|
self.seed = pw_encode( decoded, new_password) |
||||||
|
if self.xprv is not None: |
||||||
|
b = pw_decode(self.xprv, old_password) |
||||||
|
self.xprv = pw_encode(b, new_password) |
||||||
|
self.use_encryption = (new_password is not None) |
||||||
|
|
||||||
|
def is_watching_only(self): |
||||||
|
return self.xprv is None |
||||||
|
|
||||||
|
def get_keypairs_for_sig(self, tx, password): |
||||||
|
keypairs = {} |
||||||
|
for txin in tx.inputs(): |
||||||
|
num_sig = txin.get('num_sig') |
||||||
|
if num_sig is None: |
||||||
|
continue |
||||||
|
x_signatures = txin['signatures'] |
||||||
|
signatures = filter(None, x_signatures) |
||||||
|
if len(signatures) == num_sig: |
||||||
|
# input is complete |
||||||
|
continue |
||||||
|
for k, x_pubkey in enumerate(txin['x_pubkeys']): |
||||||
|
if x_signatures[k] is not None: |
||||||
|
# this pubkey already signed |
||||||
|
continue |
||||||
|
derivation = txin['derivation'] |
||||||
|
sec = self.get_private_key(derivation, password) |
||||||
|
if sec: |
||||||
|
keypairs[x_pubkey] = sec |
||||||
|
|
||||||
|
return keypairs |
||||||
|
|
||||||
|
def sign_transaction(self, tx, password): |
||||||
|
# Raise if password is not correct. |
||||||
|
self.check_password(password) |
||||||
|
# Add private keys |
||||||
|
keypairs = self.get_keypairs_for_sig(tx, password) |
||||||
|
# Sign |
||||||
|
if keypairs: |
||||||
|
tx.sign(keypairs) |
||||||
|
|
||||||
|
def derive_xkeys(self, root, derivation, password): |
||||||
|
x = self.master_private_keys[root] |
||||||
|
root_xprv = pw_decode(x, password) |
||||||
|
xprv, xpub = bip32_private_derivation(root_xprv, root, derivation) |
||||||
|
return xpub, xprv |
||||||
|
|
||||||
|
def get_mnemonic(self, password): |
||||||
|
return self.get_seed(password) |
||||||
|
|
||||||
|
def mnemonic_to_seed(self, seed, password): |
||||||
|
return Mnemonic.mnemonic_to_seed(seed, password) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def make_seed(self, lang=None): |
||||||
|
return Mnemonic(lang).make_seed() |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def address_derivation(self, account_id, change, address_index): |
||||||
|
account_derivation = self.account_derivation(account_id) |
||||||
|
return "%s/%d/%d" % (account_derivation, change, address_index) |
||||||
|
|
||||||
|
def address_id(self, address): |
||||||
|
acc_id, (change, address_index) = self.get_address_index(address) |
||||||
|
return self.address_derivation(acc_id, change, address_index) |
||||||
|
|
||||||
|
def add_seed_and_xprv(self, seed, password, passphrase=''): |
||||||
|
xprv, xpub = bip32_root(self.mnemonic_to_seed(seed, passphrase)) |
||||||
|
xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation) |
||||||
|
self.add_seed(seed, password) |
||||||
|
self.add_master_private_key(xprv, password) |
||||||
|
self.add_master_public_key(xpub) |
||||||
|
|
||||||
|
def add_xprv(self, xprv, password): |
||||||
|
xpub = bitcoin.xpub_from_xprv(xprv) |
||||||
|
self.add_master_private_key(xprv, password) |
||||||
|
self.add_master_public_key(xpub) |
||||||
|
|
||||||
|
def can_sign(self, xpub): |
||||||
|
return xpub == self.xpub and self.xprv is not None |
||||||
|
|
||||||
|
def get_private_key(self, sequence, password): |
||||||
|
xprv = self.get_master_private_key(password) |
||||||
|
_, _, _, c, k = deserialize_xkey(xprv) |
||||||
|
pk = bip32_private_key(sequence, k, c) |
||||||
|
return pk |
||||||
|
|
||||||
|
|
||||||
|
class Old_KeyStore(Deterministic_KeyStore): |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
Deterministic_KeyStore.__init__(self) |
||||||
|
self.mpk = None |
||||||
|
|
||||||
|
def load(self, storage, name): |
||||||
|
Deterministic_KeyStore.load(self, storage, name) |
||||||
|
self.mpk = storage.get('master_public_key').decode('hex') |
||||||
|
|
||||||
|
def save(self, storage, name): |
||||||
|
Deterministic_KeyStore.save(self, storage, name) |
||||||
|
storage.put('wallet_type', 'old') |
||||||
|
storage.put('master_public_key', self.mpk.encode('hex')) |
||||||
|
|
||||||
|
def add_seed(self, seed, password): |
||||||
|
Deterministic_KeyStore.add_seed(self, seed, password) |
||||||
|
self.mpk = self.mpk_from_seed(self.get_seed(password)) |
||||||
|
|
||||||
|
def add_master_public_key(self, mpk): |
||||||
|
self.mpk = mpk.decode('hex') |
||||||
|
|
||||||
|
def format_seed(self, seed): |
||||||
|
import old_mnemonic |
||||||
|
# see if seed was entered as hex |
||||||
|
seed = seed.strip() |
||||||
|
if seed: |
||||||
|
try: |
||||||
|
seed.decode('hex') |
||||||
|
return OLD_SEED_VERSION, str(seed) |
||||||
|
except Exception: |
||||||
|
pass |
||||||
|
words = seed.split() |
||||||
|
seed = old_mnemonic.mn_decode(words) |
||||||
|
if not seed: |
||||||
|
raise Exception("Invalid seed") |
||||||
|
return OLD_SEED_VERSION, seed |
||||||
|
|
||||||
|
def get_mnemonic(self, password): |
||||||
|
import old_mnemonic |
||||||
|
s = self.get_seed(password) |
||||||
|
return ' '.join(old_mnemonic.mn_encode(s)) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def mpk_from_seed(klass, seed): |
||||||
|
secexp = klass.stretch_key(seed) |
||||||
|
master_private_key = ecdsa.SigningKey.from_secret_exponent(secexp, curve = SECP256k1) |
||||||
|
master_public_key = master_private_key.get_verifying_key().to_string() |
||||||
|
return master_public_key |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def stretch_key(self, seed): |
||||||
|
x = seed |
||||||
|
for i in range(100000): |
||||||
|
x = hashlib.sha256(x + seed).digest() |
||||||
|
return string_to_number(x) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def get_sequence(self, mpk, for_change, n): |
||||||
|
return string_to_number(Hash("%d:%d:"%(n, for_change) + mpk)) |
||||||
|
|
||||||
|
def get_address(self, for_change, n): |
||||||
|
pubkey = self.get_pubkey(for_change, n) |
||||||
|
address = public_key_to_bc_address(pubkey.decode('hex')) |
||||||
|
return address |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def get_pubkey_from_mpk(self, mpk, for_change, n): |
||||||
|
z = self.get_sequence(mpk, for_change, n) |
||||||
|
master_public_key = ecdsa.VerifyingKey.from_string(mpk, curve = SECP256k1) |
||||||
|
pubkey_point = master_public_key.pubkey.point + z*SECP256k1.generator |
||||||
|
public_key2 = ecdsa.VerifyingKey.from_public_point(pubkey_point, curve = SECP256k1) |
||||||
|
return '04' + public_key2.to_string().encode('hex') |
||||||
|
|
||||||
|
def derive_pubkey(self, for_change, n): |
||||||
|
return self.get_pubkey_from_mpk(self.mpk, for_change, n) |
||||||
|
|
||||||
|
def get_private_key_from_stretched_exponent(self, for_change, n, secexp): |
||||||
|
order = generator_secp256k1.order() |
||||||
|
secexp = (secexp + self.get_sequence(self.mpk, for_change, n)) % order |
||||||
|
pk = number_to_string(secexp, generator_secp256k1.order()) |
||||||
|
compressed = False |
||||||
|
return SecretToASecret(pk, compressed) |
||||||
|
|
||||||
|
def get_private_key(self, sequence, password): |
||||||
|
seed = self.get_seed(password) |
||||||
|
self.check_seed(seed) |
||||||
|
for_change, n = sequence |
||||||
|
secexp = self.stretch_key(seed) |
||||||
|
pk = self.get_private_key_from_stretched_exponent(for_change, n, secexp) |
||||||
|
return pk |
||||||
|
|
||||||
|
def check_seed(self, seed): |
||||||
|
secexp = self.stretch_key(seed) |
||||||
|
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 ) |
||||||
|
master_public_key = master_private_key.get_verifying_key().to_string() |
||||||
|
if master_public_key != self.mpk: |
||||||
|
print_error('invalid password (mpk)', self.mpk.encode('hex'), master_public_key.encode('hex')) |
||||||
|
raise InvalidPassword() |
||||||
|
|
||||||
|
def check_password(self, password): |
||||||
|
seed = self.get_seed(password) |
||||||
|
self.check_seed(seed) |
||||||
|
|
||||||
|
def get_master_public_key(self): |
||||||
|
return self.mpk.encode('hex') |
||||||
|
|
||||||
|
def get_xpubkeys(self, for_change, n): |
||||||
|
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n))) |
||||||
|
mpk = self.mpk.encode('hex') |
||||||
|
x_pubkey = 'fe' + mpk + s |
||||||
|
return [ x_pubkey ] |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def parse_xpubkey(self, x_pubkey): |
||||||
|
assert is_extended_pubkey(x_pubkey) |
||||||
|
pk = x_pubkey[2:] |
||||||
|
mpk = pk[0:128] |
||||||
|
dd = pk[128:] |
||||||
|
s = [] |
||||||
|
while dd: |
||||||
|
n = int(bitcoin.rev_hex(dd[0:4]), 16) |
||||||
|
dd = dd[4:] |
||||||
|
s.append(n) |
||||||
|
assert len(s) == 2 |
||||||
|
return mpk, s |
||||||
|
|
||||||
|
def update_password(self, old_password, new_password): |
||||||
|
if old_password is not None: |
||||||
|
self.check_password(old_password) |
||||||
|
if new_password == '': |
||||||
|
new_password = None |
||||||
|
if self.has_seed(): |
||||||
|
decoded = self.get_seed(old_password) |
||||||
|
self.seed = pw_encode(decoded, new_password) |
||||||
|
self.use_encryption = (new_password is not None) |
||||||
|
|
||||||
|
|
||||||
|
class Hardware_KeyStore(KeyStore, Xpub): |
||||||
|
# Derived classes must set: |
||||||
|
# - device |
||||||
|
# - DEVICE_IDS |
||||||
|
# - wallet_type |
||||||
|
|
||||||
|
#restore_wallet_class = BIP32_RD_Wallet |
||||||
|
max_change_outputs = 1 |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
Xpub.__init__(self) |
||||||
|
KeyStore.__init__(self) |
||||||
|
# Errors and other user interaction is done through the wallet's |
||||||
|
# handler. The handler is per-window and preserved across |
||||||
|
# device reconnects |
||||||
|
self.handler = None |
||||||
|
|
||||||
|
def is_deterministic(self): |
||||||
|
return True |
||||||
|
|
||||||
|
def load(self, storage, name): |
||||||
|
self.xpub = storage.get('master_public_keys', {}).get(name) |
||||||
|
|
||||||
|
def save(self, storage, name): |
||||||
|
d = storage.get('master_public_keys', {}) |
||||||
|
d[name] = self.xpub |
||||||
|
storage.put('master_public_keys', d) |
||||||
|
|
||||||
|
def unpaired(self): |
||||||
|
'''A device paired with the wallet was diconnected. This can be |
||||||
|
called in any thread context.''' |
||||||
|
self.print_error("unpaired") |
||||||
|
|
||||||
|
def paired(self): |
||||||
|
'''A device paired with the wallet was (re-)connected. This can be |
||||||
|
called in any thread context.''' |
||||||
|
self.print_error("paired") |
||||||
|
|
||||||
|
def can_export(self): |
||||||
|
return False |
||||||
|
|
||||||
|
def is_watching_only(self): |
||||||
|
'''The wallet is not watching-only; the user will be prompted for |
||||||
|
pin and passphrase as appropriate when needed.''' |
||||||
|
assert not self.has_seed() |
||||||
|
return False |
||||||
|
|
||||||
|
def can_change_password(self): |
||||||
|
return False |
||||||
|
|
||||||
|
def derive_xkeys(self, root, derivation, password): |
||||||
|
if self.master_public_keys.get(self.root_name): |
||||||
|
return BIP44_wallet.derive_xkeys(self, root, derivation, password) |
||||||
|
# When creating a wallet we need to ask the device for the |
||||||
|
# master public key |
||||||
|
xpub = self.get_public_key(derivation) |
||||||
|
return xpub, None |
||||||
|
|
||||||
|
|
||||||
|
class BIP44_KeyStore(BIP32_KeyStore): |
||||||
|
root_derivation = "m/44'/0'/0'" |
||||||
|
|
||||||
|
def normalize_passphrase(self, passphrase): |
||||||
|
return normalize('NFKD', unicode(passphrase or '')) |
||||||
|
|
||||||
|
def is_valid_seed(self, seed): |
||||||
|
return True |
||||||
|
|
||||||
|
def mnemonic_to_seed(self, mnemonic, passphrase): |
||||||
|
# See BIP39 |
||||||
|
import pbkdf2, hashlib, hmac |
||||||
|
PBKDF2_ROUNDS = 2048 |
||||||
|
mnemonic = normalize('NFKD', ' '.join(mnemonic.split())) |
||||||
|
passphrase = self.normalize_passphrase(passphrase) |
||||||
|
return pbkdf2.PBKDF2(mnemonic, 'mnemonic' + passphrase, |
||||||
|
iterations = PBKDF2_ROUNDS, macmodule = hmac, |
||||||
|
digestmodule = hashlib.sha512).read(64) |
||||||
|
|
||||||
|
def on_restore_wallet(self, wizard): |
||||||
|
#assert isinstance(keystore, self.keystore_class) |
||||||
|
#msg = _("Enter the seed for your %s wallet:" % self.device) |
||||||
|
#title=_('Restore hardware wallet'), |
||||||
|
f = lambda seed: wizard.run('on_restore_seed', seed) |
||||||
|
wizard.restore_seed_dialog(run_next=f, is_valid=self.is_valid_seed) |
||||||
|
|
||||||
|
def on_restore_seed(self, wizard, seed): |
||||||
|
f = lambda passphrase: wizard.run('on_restore_passphrase', seed, passphrase) |
||||||
|
self.device = '' |
||||||
|
wizard.request_passphrase(self.device, run_next=f) |
||||||
|
|
||||||
|
def on_restore_passphrase(self, wizard, seed, passphrase): |
||||||
|
f = lambda pw: wizard.run('on_restore_password', seed, passphrase, pw) |
||||||
|
wizard.request_password(run_next=f) |
||||||
|
|
||||||
|
def on_restore_password(self, wizard, seed, passphrase, password): |
||||||
|
self.add_seed_and_xprv(seed, password, passphrase) |
||||||
|
self.save(wizard.storage, 'x/') |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
keystores = [] |
||||||
|
|
||||||
|
def load_keystore(storage, name): |
||||||
|
w = storage.get('wallet_type') |
||||||
|
t = storage.get('key_type', 'seed') |
||||||
|
seed_version = storage.get_seed_version() |
||||||
|
if seed_version == OLD_SEED_VERSION or w == 'old': |
||||||
|
k = Old_KeyStore() |
||||||
|
elif t == 'imported': |
||||||
|
k = Imported_KeyStore() |
||||||
|
elif name and name not in [ 'x/', 'x1/' ]: |
||||||
|
k = BIP32_KeyStore() |
||||||
|
elif t == 'seed': |
||||||
|
k = BIP32_KeyStore() |
||||||
|
elif t == 'hardware': |
||||||
|
hw_type = storage.get('hardware_type') |
||||||
|
for cat, _type, constructor in keystores: |
||||||
|
if cat == 'hardware' and _type == hw_type: |
||||||
|
k = constructor() |
||||||
|
break |
||||||
|
else: |
||||||
|
raise BaseException('unknown hardware type') |
||||||
|
elif t == 'hw_seed': |
||||||
|
k = BIP44_KeyStore() |
||||||
|
else: |
||||||
|
raise BaseException('unknown wallet type', t) |
||||||
|
k.load(storage, name) |
||||||
|
return k |
||||||
|
|
||||||
|
|
||||||
|
def register_keystore(category, type, constructor): |
||||||
|
keystores.append((category, type, constructor)) |
||||||
|
|
||||||
|
|
||||||
|
def is_old_mpk(mpk): |
||||||
|
try: |
||||||
|
int(mpk, 16) |
||||||
|
except: |
||||||
|
return False |
||||||
|
return len(mpk) == 128 |
||||||
|
|
||||||
|
def is_xpub(text): |
||||||
|
if text[0:4] != 'xpub': |
||||||
|
return False |
||||||
|
try: |
||||||
|
deserialize_xkey(text) |
||||||
|
return True |
||||||
|
except: |
||||||
|
return False |
||||||
|
|
||||||
|
def is_xprv(text): |
||||||
|
if text[0:4] != 'xprv': |
||||||
|
return False |
||||||
|
try: |
||||||
|
deserialize_xkey(text) |
||||||
|
return True |
||||||
|
except: |
||||||
|
return False |
||||||
|
|
||||||
|
def is_address_list(text): |
||||||
|
parts = text.split() |
||||||
|
return bool(parts) and all(bitcoin.is_address(x) for x in parts) |
||||||
|
|
||||||
|
def is_private_key_list(text): |
||||||
|
parts = text.split() |
||||||
|
return bool(parts) and all(bitcoin.is_private_key(x) for x in parts) |
||||||
|
|
||||||
|
is_seed = lambda x: is_old_seed(x) or is_new_seed(x) |
||||||
|
is_mpk = lambda x: is_old_mpk(x) or is_xpub(x) |
||||||
|
is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x) |
||||||
|
is_any_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x) or is_address_list(x) or is_private_key_list(x) |
||||||
|
is_private_key = lambda x: is_xprv(x) or is_private_key_list(x) |
||||||
|
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x) |
||||||
|
|
||||||
|
|
||||||
|
def from_seed(seed, password): |
||||||
|
if is_old_seed(seed): |
||||||
|
keystore = Old_KeyStore() |
||||||
|
keystore.add_seed(seed, password) |
||||||
|
elif is_new_seed(seed): |
||||||
|
keystore = BIP32_KeyStore() |
||||||
|
keystore.add_seed_and_xprv(seed, password) |
||||||
|
return keystore |
||||||
|
|
||||||
|
def from_private_key_list(text, password): |
||||||
|
keystore = Imported_KeyStore() |
||||||
|
for x in text.split(): |
||||||
|
keystore.import_key(x, None) |
||||||
|
keystore.update_password(None, password) |
||||||
|
return keystore |
||||||
|
|
||||||
|
def from_old_mpk(mpk): |
||||||
|
keystore = Old_KeyStore() |
||||||
|
keystore.add_master_public_key(mpk) |
||||||
|
return keystore |
||||||
|
|
||||||
|
def from_xpub(xpub): |
||||||
|
keystore = BIP32_KeyStore() |
||||||
|
keystore.add_master_public_key(xpub) |
||||||
|
return keystore |
||||||
|
|
||||||
|
def from_xprv(xprv, password): |
||||||
|
xpub = bitcoin.xpub_from_xprv(xprv) |
||||||
|
keystore = BIP32_KeyStore() |
||||||
|
keystore.add_master_private_key(xprv, password) |
||||||
|
keystore.add_master_public_key(xpub) |
||||||
|
return keystore |
||||||
|
|
||||||
|
def xprv_from_seed(seed, password): |
||||||
|
# do not store the seed, only the master xprv |
||||||
|
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed, '')) |
||||||
|
#xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation) |
||||||
|
return from_xprv(xprv, password) |
||||||
|
|
||||||
|
def xpub_from_seed(seed): |
||||||
|
# store only master xpub |
||||||
|
xprv, xpub = bip32_root(Mnemonic.mnemonic_to_seed(seed,'')) |
||||||
|
#xprv, xpub = bip32_private_derivation(xprv, "m/", self.root_derivation) |
||||||
|
return from_xpub(xpub) |
||||||
|
|
||||||
|
def from_text(text, password): |
||||||
|
if is_xprv(text): |
||||||
|
k = from_xprv(text, password) |
||||||
|
elif is_old_mpk(text): |
||||||
|
k = from_old_mpk(text) |
||||||
|
elif is_xpub(text): |
||||||
|
k = from_xpub(text) |
||||||
|
elif is_private_key_list(text): |
||||||
|
k = from_private_key_list(text, password) |
||||||
|
elif is_seed(text): |
||||||
|
k = from_seed(text, password) |
||||||
|
else: |
||||||
|
raise BaseException('Invalid seedphrase or key') |
||||||
|
return k |
||||||
@ -0,0 +1,253 @@ |
|||||||
|
#!/usr/bin/env python |
||||||
|
# |
||||||
|
# Electrum - lightweight Bitcoin client |
||||||
|
# Copyright (C) 2015 Thomas Voegtlin |
||||||
|
# |
||||||
|
# Permission is hereby granted, free of charge, to any person |
||||||
|
# obtaining a copy of this software and associated documentation files |
||||||
|
# (the "Software"), to deal in the Software without restriction, |
||||||
|
# including without limitation the rights to use, copy, modify, merge, |
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
||||||
|
# and to permit persons to whom the Software is furnished to do so, |
||||||
|
# subject to the following conditions: |
||||||
|
# |
||||||
|
# The above copyright notice and this permission notice shall be |
||||||
|
# included in all copies or substantial portions of the Software. |
||||||
|
# |
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||||
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||||
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||||
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||||
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
# SOFTWARE. |
||||||
|
|
||||||
|
import os |
||||||
|
import ast |
||||||
|
import threading |
||||||
|
import random |
||||||
|
import time |
||||||
|
import json |
||||||
|
import copy |
||||||
|
import re |
||||||
|
import stat |
||||||
|
|
||||||
|
from i18n import _ |
||||||
|
from util import NotEnoughFunds, PrintError, profiler |
||||||
|
from plugins import run_hook, plugin_loaders |
||||||
|
|
||||||
|
class WalletStorage(PrintError): |
||||||
|
|
||||||
|
def __init__(self, path): |
||||||
|
self.lock = threading.RLock() |
||||||
|
self.data = {} |
||||||
|
self.path = path |
||||||
|
self.file_exists = False |
||||||
|
self.modified = False |
||||||
|
self.print_error("wallet path", self.path) |
||||||
|
if self.path: |
||||||
|
self.read(self.path) |
||||||
|
|
||||||
|
# check here if I need to load a plugin |
||||||
|
t = self.get('wallet_type') |
||||||
|
l = plugin_loaders.get(t) |
||||||
|
if l: l() |
||||||
|
|
||||||
|
|
||||||
|
def read(self, path): |
||||||
|
"""Read the contents of the wallet file.""" |
||||||
|
try: |
||||||
|
with open(self.path, "r") as f: |
||||||
|
data = f.read() |
||||||
|
except IOError: |
||||||
|
return |
||||||
|
if not data: |
||||||
|
return |
||||||
|
try: |
||||||
|
self.data = json.loads(data) |
||||||
|
except: |
||||||
|
try: |
||||||
|
d = ast.literal_eval(data) #parse raw data from reading wallet file |
||||||
|
labels = d.get('labels', {}) |
||||||
|
except Exception as e: |
||||||
|
raise IOError("Cannot read wallet file '%s'" % self.path) |
||||||
|
self.data = {} |
||||||
|
# In old versions of Electrum labels were latin1 encoded, this fixes breakage. |
||||||
|
for i, label in labels.items(): |
||||||
|
try: |
||||||
|
unicode(label) |
||||||
|
except UnicodeDecodeError: |
||||||
|
d['labels'][i] = unicode(label.decode('latin1')) |
||||||
|
for key, value in d.items(): |
||||||
|
try: |
||||||
|
json.dumps(key) |
||||||
|
json.dumps(value) |
||||||
|
except: |
||||||
|
self.print_error('Failed to convert label to json format', key) |
||||||
|
continue |
||||||
|
self.data[key] = value |
||||||
|
self.file_exists = True |
||||||
|
|
||||||
|
def get(self, key, default=None): |
||||||
|
with self.lock: |
||||||
|
v = self.data.get(key) |
||||||
|
if v is None: |
||||||
|
v = default |
||||||
|
else: |
||||||
|
v = copy.deepcopy(v) |
||||||
|
return v |
||||||
|
|
||||||
|
def put(self, key, value): |
||||||
|
try: |
||||||
|
json.dumps(key) |
||||||
|
json.dumps(value) |
||||||
|
except: |
||||||
|
self.print_error("json error: cannot save", key) |
||||||
|
return |
||||||
|
with self.lock: |
||||||
|
if value is not None: |
||||||
|
if self.data.get(key) != value: |
||||||
|
self.modified = True |
||||||
|
self.data[key] = copy.deepcopy(value) |
||||||
|
elif key in self.data: |
||||||
|
self.modified = True |
||||||
|
self.data.pop(key) |
||||||
|
|
||||||
|
def write(self): |
||||||
|
with self.lock: |
||||||
|
self._write() |
||||||
|
self.file_exists = True |
||||||
|
|
||||||
|
def _write(self): |
||||||
|
if threading.currentThread().isDaemon(): |
||||||
|
self.print_error('warning: daemon thread cannot write wallet') |
||||||
|
return |
||||||
|
if not self.modified: |
||||||
|
return |
||||||
|
s = json.dumps(self.data, indent=4, sort_keys=True) |
||||||
|
temp_path = "%s.tmp.%s" % (self.path, os.getpid()) |
||||||
|
with open(temp_path, "w") as f: |
||||||
|
f.write(s) |
||||||
|
f.flush() |
||||||
|
os.fsync(f.fileno()) |
||||||
|
|
||||||
|
mode = os.stat(self.path).st_mode if os.path.exists(self.path) else stat.S_IREAD | stat.S_IWRITE |
||||||
|
# perform atomic write on POSIX systems |
||||||
|
try: |
||||||
|
os.rename(temp_path, self.path) |
||||||
|
except: |
||||||
|
os.remove(self.path) |
||||||
|
os.rename(temp_path, self.path) |
||||||
|
os.chmod(self.path, mode) |
||||||
|
self.print_error("saved", self.path) |
||||||
|
self.modified = False |
||||||
|
|
||||||
|
def requires_split(self): |
||||||
|
d = self.get('accounts', {}) |
||||||
|
return len(d) > 1 |
||||||
|
|
||||||
|
def split_accounts(storage): |
||||||
|
result = [] |
||||||
|
# backward compatibility with old wallets |
||||||
|
d = storage.get('accounts', {}) |
||||||
|
if len(d) < 2: |
||||||
|
return |
||||||
|
wallet_type = storage.get('wallet_type') |
||||||
|
if wallet_type == 'old': |
||||||
|
assert len(d) == 2 |
||||||
|
storage1 = WalletStorage(storage.path + '.deterministic') |
||||||
|
storage1.data = copy.deepcopy(storage.data) |
||||||
|
storage1.put('accounts', {'0': d['0']}) |
||||||
|
storage1.write() |
||||||
|
storage2 = WalletStorage(storage.path + '.imported') |
||||||
|
storage2.data = copy.deepcopy(storage.data) |
||||||
|
storage2.put('accounts', {'/x': d['/x']}) |
||||||
|
storage2.put('seed', None) |
||||||
|
storage2.put('seed_version', None) |
||||||
|
storage2.put('master_public_key', None) |
||||||
|
storage2.put('wallet_type', 'imported') |
||||||
|
storage2.write() |
||||||
|
storage2.upgrade() |
||||||
|
result = [storage1.path, storage2.path] |
||||||
|
elif wallet_type in ['bip44', 'trezor']: |
||||||
|
mpk = storage.get('master_public_keys') |
||||||
|
for k in d.keys(): |
||||||
|
i = int(k) |
||||||
|
x = d[k] |
||||||
|
if x.get("pending"): |
||||||
|
continue |
||||||
|
xpub = mpk["x/%d'"%i] |
||||||
|
new_path = storage.path + '.' + k |
||||||
|
storage2 = WalletStorage(new_path) |
||||||
|
storage2.data = copy.deepcopy(storage.data) |
||||||
|
storage2.put('wallet_type', 'standard') |
||||||
|
if wallet_type in ['trezor', 'keepkey']: |
||||||
|
storage2.put('key_type', 'hardware') |
||||||
|
storage2.put('hardware_type', wallet_type) |
||||||
|
storage2.put('accounts', {'0': x}) |
||||||
|
# need to save derivation and xpub too |
||||||
|
storage2.put('master_public_keys', {'x/': xpub}) |
||||||
|
storage2.put('account_id', k) |
||||||
|
storage2.write() |
||||||
|
result.append(new_path) |
||||||
|
else: |
||||||
|
raise BaseException("This wallet has multiple accounts and must be split") |
||||||
|
return result |
||||||
|
|
||||||
|
def requires_upgrade(storage): |
||||||
|
# '/x' is the internal ID for imported accounts |
||||||
|
return bool(storage.get('accounts', {}).get('/x', {}).get('imported',{})) |
||||||
|
|
||||||
|
def upgrade(storage): |
||||||
|
d = storage.get('accounts', {}).get('/x', {}).get('imported',{}) |
||||||
|
addresses = [] |
||||||
|
keypairs = {} |
||||||
|
for addr, v in d.items(): |
||||||
|
pubkey, privkey = v |
||||||
|
if privkey: |
||||||
|
keypairs[pubkey] = privkey |
||||||
|
else: |
||||||
|
addresses.append(addr) |
||||||
|
if addresses and keypairs: |
||||||
|
raise BaseException('mixed addresses and privkeys') |
||||||
|
elif addresses: |
||||||
|
storage.put('addresses', addresses) |
||||||
|
storage.put('accounts', None) |
||||||
|
elif keypairs: |
||||||
|
storage.put('wallet_type', 'standard') |
||||||
|
storage.put('key_type', 'imported') |
||||||
|
storage.put('keypairs', keypairs) |
||||||
|
storage.put('accounts', None) |
||||||
|
else: |
||||||
|
raise BaseException('no addresses or privkeys') |
||||||
|
storage.write() |
||||||
|
|
||||||
|
def get_action(self): |
||||||
|
action = run_hook('get_action', self) |
||||||
|
if action: |
||||||
|
return action |
||||||
|
if not self.file_exists: |
||||||
|
return 'new' |
||||||
|
|
||||||
|
def get_seed_version(self): |
||||||
|
from version import OLD_SEED_VERSION, NEW_SEED_VERSION |
||||||
|
seed_version = self.get('seed_version') |
||||||
|
if not seed_version: |
||||||
|
seed_version = OLD_SEED_VERSION if len(self.get('master_public_key','')) == 128 else NEW_SEED_VERSION |
||||||
|
if seed_version not in [OLD_SEED_VERSION, NEW_SEED_VERSION]: |
||||||
|
msg = "Your wallet has an unsupported seed version." |
||||||
|
msg += '\n\nWallet file: %s' % os.path.abspath(self.path) |
||||||
|
if seed_version in [5, 7, 8, 9, 10]: |
||||||
|
msg += "\n\nTo open this wallet, try 'git checkout seed_v%d'"%seed_version |
||||||
|
if seed_version == 6: |
||||||
|
# version 1.9.8 created v6 wallets when an incorrect seed was entered in the restore dialog |
||||||
|
msg += '\n\nThis file was created because of a bug in version 1.9.8.' |
||||||
|
if self.get('master_public_keys') is None and self.get('master_private_keys') is None and self.get('imported_keys') is None: |
||||||
|
# pbkdf2 was not included with the binaries, and wallet creation aborted. |
||||||
|
msg += "\nIt does not contain any keys, and can safely be removed." |
||||||
|
else: |
||||||
|
# creation was complete if electrum was run from source |
||||||
|
msg += "\nPlease open this file with Electrum 1.9.8, and move your coins to a new wallet." |
||||||
|
raise BaseException(msg) |
||||||
|
return seed_version |
||||||
@ -1,2 +1 @@ |
|||||||
from hw_wallet import BIP44_HW_Wallet |
|
||||||
from plugin import HW_PluginBase |
from plugin import HW_PluginBase |
||||||
|
|||||||
@ -1,95 +0,0 @@ |
|||||||
#!/usr/bin/env python2 |
|
||||||
# -*- mode: python -*- |
|
||||||
# |
|
||||||
# Electrum - lightweight Bitcoin client |
|
||||||
# Copyright (C) 2016 The Electrum developers |
|
||||||
# |
|
||||||
# Permission is hereby granted, free of charge, to any person |
|
||||||
# obtaining a copy of this software and associated documentation files |
|
||||||
# (the "Software"), to deal in the Software without restriction, |
|
||||||
# including without limitation the rights to use, copy, modify, merge, |
|
||||||
# publish, distribute, sublicense, and/or sell copies of the Software, |
|
||||||
# and to permit persons to whom the Software is furnished to do so, |
|
||||||
# subject to the following conditions: |
|
||||||
# |
|
||||||
# The above copyright notice and this permission notice shall be |
|
||||||
# included in all copies or substantial portions of the Software. |
|
||||||
# |
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
||||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|
||||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|
||||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|
||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
||||||
# SOFTWARE. |
|
||||||
|
|
||||||
from struct import pack |
|
||||||
|
|
||||||
from electrum.wallet import BIP44_Wallet |
|
||||||
|
|
||||||
class BIP44_HW_Wallet(BIP44_Wallet): |
|
||||||
'''A BIP44 hardware wallet base class.''' |
|
||||||
# Derived classes must set: |
|
||||||
# - device |
|
||||||
# - DEVICE_IDS |
|
||||||
# - wallet_type |
|
||||||
|
|
||||||
restore_wallet_class = BIP44_Wallet |
|
||||||
max_change_outputs = 1 |
|
||||||
|
|
||||||
def __init__(self, storage): |
|
||||||
BIP44_Wallet.__init__(self, storage) |
|
||||||
# Errors and other user interaction is done through the wallet's |
|
||||||
# handler. The handler is per-window and preserved across |
|
||||||
# device reconnects |
|
||||||
self.handler = None |
|
||||||
|
|
||||||
def unpaired(self): |
|
||||||
'''A device paired with the wallet was diconnected. This can be |
|
||||||
called in any thread context.''' |
|
||||||
self.print_error("unpaired") |
|
||||||
|
|
||||||
def paired(self): |
|
||||||
'''A device paired with the wallet was (re-)connected. This can be |
|
||||||
called in any thread context.''' |
|
||||||
self.print_error("paired") |
|
||||||
|
|
||||||
def get_action(self): |
|
||||||
pass |
|
||||||
|
|
||||||
def can_create_accounts(self): |
|
||||||
return True |
|
||||||
|
|
||||||
def can_export(self): |
|
||||||
return False |
|
||||||
|
|
||||||
def is_watching_only(self): |
|
||||||
'''The wallet is not watching-only; the user will be prompted for |
|
||||||
pin and passphrase as appropriate when needed.''' |
|
||||||
assert not self.has_seed() |
|
||||||
return False |
|
||||||
|
|
||||||
def can_change_password(self): |
|
||||||
return False |
|
||||||
|
|
||||||
def get_client(self, force_pair=True): |
|
||||||
return self.plugin.get_client(self, force_pair) |
|
||||||
|
|
||||||
def first_address(self): |
|
||||||
'''Used to check a hardware wallet matches a software wallet''' |
|
||||||
account = self.accounts.get('0') |
|
||||||
derivation = self.address_derivation('0', 0, 0) |
|
||||||
return (account.first_address()[0] if account else None, derivation) |
|
||||||
|
|
||||||
def derive_xkeys(self, root, derivation, password): |
|
||||||
if self.master_public_keys.get(self.root_name): |
|
||||||
return BIP44_wallet.derive_xkeys(self, root, derivation, password) |
|
||||||
|
|
||||||
# When creating a wallet we need to ask the device for the |
|
||||||
# master public key |
|
||||||
xpub = self.get_public_key(derivation) |
|
||||||
return xpub, None |
|
||||||
|
|
||||||
def i4b(self, x): |
|
||||||
return pack('>I', x) |
|
||||||
Loading…
Reference in new issue