diff --git a/requirements.txt b/requirements.txt index ce4adbcd60..2526335872 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +bip32==0.0.8 certifi==2019.9.11 chardet==3.0.4 Click==7.0 diff --git a/src/cryptoadvance/specter/controller.py b/src/cryptoadvance/specter/controller.py index 0d3bc0d6ec..08d321e39d 100644 --- a/src/cryptoadvance/specter/controller.py +++ b/src/cryptoadvance/specter/controller.py @@ -2,6 +2,7 @@ import requests import random, copy from collections import OrderedDict +from mnemonic import Mnemonic from threading import Thread from .key import Key @@ -13,9 +14,10 @@ from flask_login.config import EXEMPT_METHODS +from .devices.bitcoin_core import BitcoinCore from .helpers import (alias, get_devices_with_keys_by_type, hash_password, get_loglevel, get_version_info, run_shell, set_loglevel, - verify_password, bcur2base64, get_txid) + verify_password, bcur2base64, get_txid, generate_mnemonic) from .specter import Specter from .specter_error import SpecterError from .wallet_manager import purposes @@ -705,6 +707,33 @@ def wallet_send(wallet_alias): wallet.delete_pending_psbt(ast.literal_eval(request.form["pending_psbt"])["tx"]["txid"]) except Exception as e: flash("Could not delete Pending PSBT!", "error") + elif action == 'signhotwallet': + passphrase = request.form['passphrase'] + psbt = ast.literal_eval(request.form["psbt"]) + b64psbt = wallet.pending_psbts[psbt['tx']['txid']]['base64'] + device = request.form['device'] + if 'devices_signed' not in psbt or device not in psbt['devices_signed']: + try: + signed_psbt = app.specter.device_manager.get_by_alias(device).sign_psbt(b64psbt, wallet, passphrase) + if signed_psbt['complete']: + if 'devices_signed' not in psbt: + psbt['devices_signed'] = [] + # TODO: This uses device name, but should use device alias... + psbt['devices_signed'].append(app.specter.device_manager.get_by_alias(device).name) + psbt['sigs_count'] = len(psbt['devices_signed']) + raw = wallet.cli.finalizepsbt(b64psbt) + if "hex" in raw: + psbt["raw"] = raw["hex"] + signed_psbt = signed_psbt['psbt'] + except Exception as e: + signed_psbt = None + flash("Failed to sign PSBT: %s" % e, "error") + else: + signed_psbt = None + flash("Device already signed the PSBT", "error") + return render_template("wallet/send/sign/wallet_send_sign_psbt.jinja", signed_psbt=signed_psbt, psbt=psbt, label=label, + wallet_alias=wallet_alias, wallet=wallet, + specter=app.specter, rand=rand) return render_template("wallet/send/new/wallet_send.jinja", psbt=psbt, label=label, wallet_alias=wallet_alias, wallet=wallet, specter=app.specter, rand=rand, error=err) @@ -819,23 +848,46 @@ def new_device(): device_type = "other" device_name = "" xpubs = "" + strength = 128 + mnemonic = generate_mnemonic(strength=strength) if request.method == 'POST': + action = request.form['action'] device_type = request.form['device_type'] device_name = request.form['device_name'] - if not device_name: - err = "Device name must not be empty" - elif device_name in app.specter.device_manager.devices_names: - err = "Device with this name already exists" - xpubs = request.form['xpubs'] - if not xpubs: - err = "xpubs name must not be empty" - keys, failed = Key.parse_xpubs(xpubs) - if len(failed) > 0: - err = "Failed to parse these xpubs:\n" + "\n".join(failed) - if err is None: - device = app.specter.device_manager.add_device(name=device_name, device_type=device_type, keys=keys) - return redirect("/devices/%s/" % device.alias) - return render_template("device/new_device.jinja", device_type=device_type, device_name=device_name, xpubs=xpubs, error=err, specter=app.specter, rand=rand) + if action == "newcolddevice": + if not device_name: + err = "Device name must not be empty" + elif device_name in app.specter.device_manager.devices_names: + err = "Device with this name already exists" + xpubs = request.form['xpubs'] + if not xpubs: + err = "xpubs name must not be empty" + keys, failed = Key.parse_xpubs(xpubs) + if len(failed) > 0: + err = "Failed to parse these xpubs:\n" + "\n".join(failed) + if err is None: + device = app.specter.device_manager.add_device(name=device_name, device_type=device_type, keys=keys) + return redirect("/devices/%s/" % device.alias) + elif action == "newhotdevice": + if not device_name: + err = "Device name must not be empty" + elif device_name in app.specter.device_manager.devices_names: + err = "Device with this name already exists" + if len(request.form['mnemonic'].split(' ')) not in [12, 15, 18, 21, 24]: + err = "Invalid mnemonic entered: Must contain either: 12, 15, 18, 21, or 24 words." + mnemo = Mnemonic('english') + if not mnemo.check(request.form['mnemonic']): + err = "Invalid mnemonic entered." + if err is None: + mnemonic = request.form['mnemonic'] + passphrase = request.form['passphrase'] + device = app.specter.device_manager.add_device(name=device_name, device_type=device_type, keys=[]) + device.setup_device(mnemonic, passphrase, app.specter.wallet_manager, app.specter.chain != 'main') + return redirect("/devices/%s/" % device.alias) + elif action == 'generatemnemonic': + strength = int(request.form['strength']) + mnemonic = generate_mnemonic(strength=strength) + return render_template("device/new_device.jinja", device_type=device_type, device_name=device_name, xpubs=xpubs, mnemonic=mnemonic, strength=strength, error=err, specter=app.specter, rand=rand) @app.route('/devices//', methods=['GET', 'POST']) @login_required @@ -853,7 +905,7 @@ def device(device_alias): if len(wallets) != 0: err = "Device could not be removed since it is used in wallets: {}.\nYou must delete those wallets before you can remove this device.".format([wallet.name for wallet in wallets]) else: - app.specter.device_manager.remove_device(device) + app.specter.device_manager.remove_device(device, app.specter.wallet_manager) return redirect("/") elif action == "delete_key": key = request.form['key'] diff --git a/src/cryptoadvance/specter/device_manager.py b/src/cryptoadvance/specter/device_manager.py index 66785da479..27b9fe6773 100644 --- a/src/cryptoadvance/specter/device_manager.py +++ b/src/cryptoadvance/specter/device_manager.py @@ -6,6 +6,7 @@ from .devices.keepkey import Keepkey from .devices.specter import Specter from .devices.cobo import Cobo +from .devices.bitcoin_core import BitcoinCore from .helpers import alias, load_jsons, fslock @@ -18,6 +19,7 @@ 'ledger': Ledger, 'specter': Specter, 'cobo': Cobo, + 'bitcoincore': BitcoinCore, } def get_device_class(device_type): @@ -85,6 +87,8 @@ def get_by_alias(self, device_alias): return self.devices[device_name] logger.error("Could not find Device %s" % device_alias) - def remove_device(self, device): + def remove_device(self, device, wallet_manager=None): os.remove(device.fullpath) + if isinstance(device, BitcoinCore): + device.delete(wallet_manager) self.update() diff --git a/src/cryptoadvance/specter/devices/bitcoin_core.py b/src/cryptoadvance/specter/devices/bitcoin_core.py new file mode 100644 index 0000000000..fab49562cd --- /dev/null +++ b/src/cryptoadvance/specter/devices/bitcoin_core.py @@ -0,0 +1,117 @@ +import os, shutil +from bip32 import BIP32 +from mnemonic import Mnemonic +from ..descriptor import AddChecksum +from ..device import Device +from ..helpers import alias, convert_xpub_prefix, encode_base58_checksum, get_xpub_fingerprint, seed_to_hd_master_key +from ..key import Key + +class BitcoinCore(Device): + def __init__(self, name, alias, device_type, keys, fullpath, manager): + Device.__init__(self, name, alias, device_type, keys, fullpath, manager) + self.hwi_support = False + self.exportable_to_wallet = False + self.hot_wallet = True + + def setup_device(self, mnemonic, passphrase, wallet_manager, testnet): + seed = Mnemonic.to_seed(mnemonic) + xprv = seed_to_hd_master_key(seed, testnet=testnet) + wallet_name = os.path.join(wallet_manager.cli_path + '_hotstorage', self.alias) + wallet_manager.cli.createwallet(wallet_name, False, True) + cli = wallet_manager.cli.wallet(wallet_name) + # TODO: Maybe more than 1000? Maybe add mechanism to add more later. + ## NOTE: This will work only on the network the device was added, so hot devices should be filtered out by network. + coin = int(testnet) + cli.importmulti([ + { 'desc': AddChecksum('sh(wpkh({}/49h/{}h/0h/0/*))'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('sh(wpkh({}/49h/{}h/0h/1/*))'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('wpkh({}/84h/{}h/0h/0/*)'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('wpkh({}/84h/{}h/0h/1/*)'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('sh(wpkh({}/48h/{}h/0h/1h/0/*))'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('sh(wpkh({}/48h/{}h/0h/1h/1/*))'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('wpkh({}/48h/{}h/0h/2h/0/*)'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + { 'desc': AddChecksum('wpkh({}/48h/{}h/0h/2h/1/*)'.format(xprv, coin)), 'range': 1000, 'timestamp': 'now'}, + ]) + if passphrase: + cli.encryptwallet(passphrase) + + bip32 = BIP32.from_seed(seed) + xpubs = "" + master_fpr = get_xpub_fingerprint(bip32.get_xpub_from_path('m/0h')).hex() + + if not testnet: + # Nested Segwit + xpub = bip32.get_xpub_from_path('m/49h/0h/0h') + ypub = convert_xpub_prefix(xpub, b'\x04\x9d\x7c\xb2') + xpubs += "[%s/49'/0'/0']%s\n" % (master_fpr, ypub) + # native Segwit + xpub = bip32.get_xpub_from_path('m/84h/0h/0h') + zpub = convert_xpub_prefix(xpub, b'\x04\xb2\x47\x46') + xpubs += "[%s/84'/0'/0']%s\n" % (master_fpr, zpub) + # Multisig nested Segwit + xpub = bip32.get_xpub_from_path('m/48h/0h/0h/1h') + Ypub = convert_xpub_prefix(xpub, b'\x02\x95\xb4\x3f') + xpubs += "[%s/48'/0'/0'/1']%s\n" % (master_fpr, Ypub) + # Multisig native Segwit + xpub = bip32.get_xpub_from_path('m/48h/0h/0h/2h') + Zpub = convert_xpub_prefix(xpub, b'\x02\xaa\x7e\xd3') + xpubs += "[%s/48'/0'/0'/2']%s\n" % (master_fpr, Zpub) + else: + # Testnet nested Segwit + xpub = bip32.get_xpub_from_path('m/49h/1h/0h') + upub = convert_xpub_prefix(xpub, b'\x04\x4a\x52\x62') + xpubs += "[%s/49'/1'/0']%s\n" % (master_fpr, upub) + # Testnet native Segwit + xpub = bip32.get_xpub_from_path('m/84h/1h/0h') + vpub = convert_xpub_prefix(xpub, b'\x04\x5f\x1c\xf6') + xpubs += "[%s/84'/1'/0']%s\n" % (master_fpr, vpub) + # Testnet multisig nested Segwit + xpub = bip32.get_xpub_from_path('m/48h/1h/0h/1h') + Upub = convert_xpub_prefix(xpub, b'\x02\x42\x89\xef') + xpubs += "[%s/48'/1'/0'/1']%s\n" % (master_fpr, Upub) + # Testnet multisig native Segwit + xpub = bip32.get_xpub_from_path('m/48h/1h/0h/2h') + Vpub = convert_xpub_prefix(xpub, b'\x02\x57\x54\x83') + xpubs += "[%s/48'/1'/0'/2']%s\n" % (master_fpr, Vpub) + + keys, failed = Key.parse_xpubs(xpubs) + if len(failed) > 0: + # TODO: This should never occur, but just in case, we must make sure to catch it properly so it doesn't crash the app no matter what. + raise Exception("Failed to parse these xpubs:\n" + "\n".join(failed)) + else: + self.add_keys(keys) + + def _load_wallet(self, wallet_manager): + existing_wallets = [w["name"] for w in wallet_manager.cli.listwalletdir()["wallets"]] + loaded_wallets = wallet_manager.cli.listwallets() + not_loaded_wallets = [w for w in existing_wallets if w not in loaded_wallets] + if os.path.join(wallet_manager.cli_path + "_hotstorage", self.alias) in existing_wallets: + if os.path.join(wallet_manager.cli_path + "_hotstorage", self.alias) in not_loaded_wallets: + wallet_manager.cli.loadwallet(os.path.join(wallet_manager.cli_path + "_hotstorage", self.alias)) + + def create_psbts(self, base64_psbt, wallet): + return { 'core': base64_psbt } + + def sign_psbt(self, base64_psbt, wallet, passphrase): + # Load the wallet if not loaded + self._load_wallet(wallet.manager) + cli = wallet.manager.cli.wallet(os.path.join(wallet.manager.cli_path + "_hotstorage", self.alias)) + if passphrase: + cli.walletpassphrase(passphrase, 60) + signed_psbt = cli.walletprocesspsbt(base64_psbt) + if base64_psbt == signed_psbt['psbt']: + raise Exception('Make sure you have entered the passphrase correctly.') + if passphrase: + cli.walletlock() + return signed_psbt + + def delete(self, wallet_manager): + try: + wallet_cli_path = os.path.join(wallet_manager.cli_path + "_hotstorage", self.alias) + cli = wallet_manager.cli.wallet(wallet_cli_path) + cli.unloadwallet(wallet_cli_path) + # Try deleting wallet file + if wallet_manager.get_default_datadir() and os.path.exists(wallet_cli_path): + shutil.rmtree(os.path.join(wallet_manager.get_default_datadir(), wallet_cli_path)) + except: + pass # We tried... diff --git a/src/cryptoadvance/specter/helpers.py b/src/cryptoadvance/specter/helpers.py index 44c989db57..1721c61c25 100644 --- a/src/cryptoadvance/specter/helpers.py +++ b/src/cryptoadvance/specter/helpers.py @@ -1,5 +1,6 @@ -import binascii, collections, copy, hashlib, json, logging, os, six, subprocess, sys +import binascii, collections, copy, hashlib, hmac, json, logging, os, six, subprocess, sys from collections import OrderedDict +from mnemonic import Mnemonic from .descriptor import AddChecksum from hwilib.serializations import PSBT, CTransaction from .bcur import bcur_decode @@ -360,3 +361,51 @@ def get_txid(tx): inp.scriptSig = b"" t.rehash() return t.hash + +# Hot wallet helpers +def generate_mnemonic(strength=256): + # Generate words list + mnemo = Mnemonic("english") + words = mnemo.generate(strength=strength) + return words + +# From https://github.com/trezor/python-mnemonic/blob/ad06157e21fc2c2145c726efbfdcf69df1350061/mnemonic/mnemonic.py#L246 +# Refactored code segments from +def b58encode(v: bytes) -> str: + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + p, acc = 1, 0 + for c in reversed(v): + acc += p * c + p = p << 8 + + string = "" + while acc: + acc, idx = divmod(acc, 58) + string = alphabet[idx : idx + 1] + string + return string +# We need to copy it like this because HWI uses it as a dependency, but requires v0.18 which doesn't have this function. +def seed_to_hd_master_key(seed, testnet=False) -> str: + if len(seed) != 64: + raise ValueError("Provided seed should have length of 64") + + # Compute HMAC-SHA512 of seed + seed = hmac.new(b"Bitcoin seed", seed, digestmod=hashlib.sha512).digest() + + # Serialization format can be found at: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#Serialization_format + xprv = b"\x04\x88\xad\xe4" # Version for private mainnet + if testnet: + xprv = b"\x04\x35\x83\x94" # Version for private testnet + xprv += b"\x00" * 9 # Depth, parent fingerprint, and child number + xprv += seed[32:] # Chain code + xprv += b"\x00" + seed[:32] # Master key + + # Double hash using SHA256 + hashed_xprv = hashlib.sha256(xprv).digest() + hashed_xprv = hashlib.sha256(hashed_xprv).digest() + + # Append 4 bytes of checksum + xprv += hashed_xprv[:4] + + # Return base58 + return b58encode(xprv) diff --git a/src/cryptoadvance/specter/templates/device/components/device_type.jinja b/src/cryptoadvance/specter/templates/device/components/device_type.jinja index 2d109a0fdf..aacb42b0ee 100644 --- a/src/cryptoadvance/specter/templates/device/components/device_type.jinja +++ b/src/cryptoadvance/specter/templates/device/components/device_type.jinja @@ -3,6 +3,7 @@
- -
- {% include "device/components/device_type.jinja" %} - - {% endif %} -

Scan, paste or load xpubs:

-
+
{% if not device %} - You will be able add more keys later, from the device menu. +
+ Name it     + +
+
+ {% include "device/components/device_type.jinja" %} + {% endif %} -
- -
- - Scan -
- i -

- - Using an airgapped Specter DIY with QR codes

- Scan QR codes with extended public keys one by one.

- We recommend importing Native Segwit and Segwit Multisig keys. -
-

+
+

Scan, paste or load xpubs:

+
+ {% if not device %} + You will be able add more keys later, from the device menu. + {% endif %} +
+ +
+ + Scan +
+ i +

+ + Using an airgapped Specter DIY with QR codes

+ Scan QR codes with extended public keys one by one.

+ We recommend importing Native Segwit and Segwit Multisig keys. +
+

+
+
+ +
- - -
+ +
{% endblock %} {% block scripts %} + + {% endif %} + {% if device.hot_wallet %} + + + + {% endif %}
{%- endmacro %} \ No newline at end of file diff --git a/src/cryptoadvance/specter/templates/settings/bitcoin_core_settings.jinja b/src/cryptoadvance/specter/templates/settings/bitcoin_core_settings.jinja index caf1acc19e..f8658bf064 100644 --- a/src/cryptoadvance/specter/templates/settings/bitcoin_core_settings.jinja +++ b/src/cryptoadvance/specter/templates/settings/bitcoin_core_settings.jinja @@ -1,7 +1,7 @@ {% extends "base.jinja" %} {% block main %}
-

Bitoin Core settings - Specter Desktop {{ current_version }}

+

Bitcoin Core settings - Specter Desktop {{ current_version }}

{% from 'settings/components/settings_menu.jinja' import settings_menu %} {{ settings_menu('bitcoin_core_settings', current_user) }}
diff --git a/src/cryptoadvance/specter/templates/wallet/receive/wallet_receive.jinja b/src/cryptoadvance/specter/templates/wallet/receive/wallet_receive.jinja index f28a8647bc..0b885016c5 100644 --- a/src/cryptoadvance/specter/templates/wallet/receive/wallet_receive.jinja +++ b/src/cryptoadvance/specter/templates/wallet/receive/wallet_receive.jinja @@ -37,11 +37,15 @@
  + {% set supports_hwi = [] %} {% set supports_hwi_multisig_display_address = [] %} {% for device in wallet.devices if device.supports_hwi_multisig_display_address %} {% set supports_hwi_multisig_display_address = supports_hwi_multisig_display_address.append(device) %} {% endfor %} - {% if supports_hwi_multisig_display_address != [] or not wallet.is_multisig %} + {% for device in wallet.devices if device.hwi_support %} + {% set supports_hwi = supports_hwi.append(device) %} + {% endfor %} + {% if supports_hwi != [] and (supports_hwi_multisig_display_address != [] or not wallet.is_multisig) %}

Multsig address on-device display is only available for ColdCard, KeepKey, and Trezor devices.

{% endif %} diff --git a/src/cryptoadvance/specter/templates/wallet/send/sign/wallet_send_sign_psbt.jinja b/src/cryptoadvance/specter/templates/wallet/send/sign/wallet_send_sign_psbt.jinja index ffb1c9f374..0c86991cbe 100644 --- a/src/cryptoadvance/specter/templates/wallet/send/sign/wallet_send_sign_psbt.jinja +++ b/src/cryptoadvance/specter/templates/wallet/send/sign/wallet_send_sign_psbt.jinja @@ -4,7 +4,7 @@ {% include "includes/hwi/hwi.jinja" %} {% from 'includes/overlay/sign_tx_method.jinja' import sign_tx_method %} {% for device in wallet.devices %} - {{ sign_tx_method(wallet, device, psbt) }} + {{ sign_tx_method(wallet, device, psbt, specter.info.chain) }} {% endfor %}