diff --git a/lbrynet/extras/daemon/Daemon.py b/lbrynet/extras/daemon/Daemon.py index 8f9ccaeeb..9d836cf5d 100644 --- a/lbrynet/extras/daemon/Daemon.py +++ b/lbrynet/extras/daemon/Daemon.py @@ -7,6 +7,8 @@ import inspect import typing import base58 import random +import ecdsa +import hashlib from urllib.parse import urlencode, quote from typing import Callable, Optional, List from binascii import hexlify, unhexlify @@ -2122,40 +2124,69 @@ class Daemon(metaclass=JSONRPCServerType): ) @requires(WALLET_COMPONENT) - async def jsonrpc_channel_export(self, claim_id, password=None, account_id=None): + async def jsonrpc_channel_export(self, channel_id=None, channel_name=None, account_id=None): """ - Export serialized channel signing information for a given certificate claim id + Export channel private key. Usage: - channel_export ( | --claim_id=) + channel_export ( | --channel_id= | --channel_name=) + [--account_id=...] Options: - --claim_id= : (str) Claim ID to export information about + --channel_id= : (str) claim id of channel to export + --channel_name= : (str) name of channel to export + --account_id= : (str) one or more account ids for accounts + to look in for channels, defaults to + all accounts. Returns: - (str) Serialized certificate information + (str) serialized channel private key """ - account = self.get_account_or_default(account_id) - - return await self.wallet_manager.export_certificate_info(claim_id, account, password) + channel = await self.get_channel_or_error(account_id, channel_id, channel_name, for_signing=True) + address = channel.get_address(self.ledger) + public_key = await self.ledger.get_public_key_for_address(channel.get_address(self.ledger)) + if not public_key: + raise Exception("Can't find public key for address holding the channel.") + export = { + 'name': channel.claim_name, + 'channel_id': channel.claim_id, + 'holding_address': address, + 'holding_public_key': public_key.extended_key_string(), + 'signing_private_key': channel.private_key.to_pem().decode() + } + return base58.b58encode(json.dumps(export, separators=(',', ':'))) @requires(WALLET_COMPONENT) - async def jsonrpc_channel_import(self, serialized_certificate_info, password=None, account_id=None): + async def jsonrpc_channel_import(self, channel_data): """ - Import serialized channel signing information (to allow signing new claims to the channel) + Import serialized channel private key (to allow signing new streams to the channel) Usage: - channel_import ( | --serialized_certificate_info=) + channel_import ( | --channel_data=) Options: - --serialized_certificate_info= : (str) certificate info + --channel_data= : (str) serialized channel, as exported by channel export Returns: (dict) Result dictionary """ - account = self.get_account_or_default(account_id) - - return await self.wallet_manager.import_certificate_info(serialized_certificate_info, password, account) + decoded = base58.b58decode(channel_data) + data = json.loads(decoded) + channel_private_key = ecdsa.SigningKey.from_pem( + data['signing_private_key'], hashfunc=hashlib.sha256 + ) + account: LBCAccount = await self.ledger.get_account_for_address(data['holding_address']) + if not account: + new_account = LBCAccount.from_dict(self.ledger, self.default_wallet, { + 'name': f"Holding Account For Channel {data['name']}", + 'public_key': data['holding_public_key'], + 'address_generator': {'name': 'single-address'} + }) + if self.ledger.network.is_connected: + asyncio.create_task(self.ledger.subscribe_account(new_account)) + account.add_channel_private_key(channel_private_key) + self.default_wallet.save() + return f"Added channel signing key for {data['name']}." STREAM_DOC = """ Create, update, abandon, list and inspect your stream claims. diff --git a/lbrynet/wallet/manager.py b/lbrynet/wallet/manager.py index de5894f30..d9627ead9 100644 --- a/lbrynet/wallet/manager.py +++ b/lbrynet/wallet/manager.py @@ -1,7 +1,7 @@ import os import json import logging -from binascii import unhexlify, hexlify +from binascii import unhexlify from datetime import datetime @@ -17,9 +17,6 @@ from lbrynet.wallet.dewies import dewies_to_lbc log = logging.getLogger(__name__) -_NO_PASSWORD_USED_BYTES = b'00' -_PASSWORD_USED_BYTES = b'01' - class LbryWalletManager(BaseWalletManager): @@ -304,58 +301,6 @@ class LbryWalletManager(BaseWalletManager): history.append(item) return history - async def export_certificate_info(self, claim_id, account, password=None, insecure=False): - if password is None and not insecure: - raise ValueError( - "Password not provided. If you wish to export channel without a password, please use the " - "--insecure flag" - ) - - if password is not None and insecure: - raise ValueError( - "Password and insecure flag cannot be provided together. Please remove the insecure flag" - ) - - try: - channel_txo = (await account.get_channels(claim_id=claim_id, limit=1))[0] - private_key_str = channel_txo.pem_to_private_key_str(channel_txo.private_key) - except Exception: - raise LookupError(f"Cannot retrieve private key for channel id: {claim_id}") - - if not password: - serialized_certificate_info = private_key_str + _NO_PASSWORD_USED_BYTES - else: - encrypted_private_key = self.encrypt_private_key_with_password(private_key_str, password) - serialized_certificate_info = encrypted_private_key + _PASSWORD_USED_BYTES - - x = hexlify(serialized_certificate_info).decode() - return x - - async def import_certificate_info(self, serialized_certificate_info, password, account): - serialized_certificate_info = (unhexlify(serialized_certificate_info.encode())) - - if password is None and serialized_certificate_info.endswith(_PASSWORD_USED_BYTES): - raise ValueError("The certificate was encrypted with a password but no password was provided.") - - if password is not None and serialized_certificate_info.endswith(_NO_PASSWORD_USED_BYTES): - raise ValueError("The certificate was not encrypted with a password but a password was provided.") - - serialized_certificate_info = serialized_certificate_info[0:-2] - - if not password: - private_key = Transaction.output_class.private_key_from_str(serialized_certificate_info) - else: - decrypted_private_key = self.decrypt_serilized_info_with_password(serialized_certificate_info, password) - private_key = Transaction.output_class.private_key_from_str(decrypted_private_key) - - public_key_bytes = private_key.get_verifying_key().to_der() - channel_pubkey_hash = account.ledger.public_key_to_address(public_key_bytes) - account.channel_keys[channel_pubkey_hash] = private_key.to_pem().decode() - account.wallet.save() - - def encrypt_private_key_with_password(self, private_key_str, password): - pass - def save(self): for wallet in self.wallets: wallet.save() diff --git a/lbrynet/wallet/transaction.py b/lbrynet/wallet/transaction.py index 7dab340ca..b1430bbe3 100644 --- a/lbrynet/wallet/transaction.py +++ b/lbrynet/wallet/transaction.py @@ -159,14 +159,6 @@ class Output(BaseOutput): def is_channel_private_key(self, private_key): return self.claim.channel.public_key_bytes == private_key.get_verifying_key().to_der() - @staticmethod - def pem_to_private_key_str(private_key_pem): - return ecdsa.SigningKey.from_pem(private_key_pem, hashfunc=hashlib.sha256).to_string() - - @staticmethod - def private_key_from_str(private_key_str): - return ecdsa.SigningKey.from_string(private_key_str, curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256) - @classmethod def pay_claim_name_pubkey_hash( cls, amount: int, claim_name: str, claim: Claim, pubkey_hash: bytes) -> 'Output': diff --git a/tests/integration/test_claim_commands.py b/tests/integration/test_claim_commands.py index 3671775b1..e2bae4487 100644 --- a/tests/integration/test_claim_commands.py +++ b/tests/integration/test_claim_commands.py @@ -299,8 +299,8 @@ class ChannelCommands(CommandTestCase): self.assertIsNone(txo.private_key) # send the private key too - channel_public_key = self.account.get_channel_private_key(unhexlify(channel['public_key'])) - account2.add_channel_private_key(channel_public_key) + private_key = self.account.get_channel_private_key(unhexlify(channel['public_key'])) + account2.add_channel_private_key(private_key) # now should have private key txo = (await account2.get_channels())[0] @@ -318,7 +318,7 @@ class ChannelCommands(CommandTestCase): self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 0) # exporting from default account - serialized_channel_info = await self.out(self.daemon.jsonrpc_channel_export(claim_id, insecure=True)) + serialized_channel_info = await self.out(self.daemon.jsonrpc_channel_export(claim_id)) other_address = await account2.receiving.get_or_create_usable_address() await self.out(self.channel_update(claim_id, claim_address=other_address))