From 6d462ad6d8efd69380d2874049e88aa49d1606dd Mon Sep 17 00:00:00 2001 From: hackrush Date: Wed, 22 May 2019 16:03:57 +0530 Subject: [PATCH] channel export import --- lbrynet/extras/daemon/Daemon.py | 10 +++-- lbrynet/wallet/manager.py | 57 +++++++++++++++++++++++- lbrynet/wallet/transaction.py | 8 ++++ tests/integration/test_claim_commands.py | 26 +++++++++++ 4 files changed, 96 insertions(+), 5 deletions(-) diff --git a/lbrynet/extras/daemon/Daemon.py b/lbrynet/extras/daemon/Daemon.py index 462c63641..9a264edbd 100644 --- a/lbrynet/extras/daemon/Daemon.py +++ b/lbrynet/extras/daemon/Daemon.py @@ -2122,7 +2122,7 @@ class Daemon(metaclass=JSONRPCServerType): ) @requires(WALLET_COMPONENT) - async def jsonrpc_channel_export(self, claim_id): + async def jsonrpc_channel_export(self, claim_id, password=None, account_id=None, insecure=False): """ Export serialized channel signing information for a given certificate claim id @@ -2135,11 +2135,12 @@ class Daemon(metaclass=JSONRPCServerType): Returns: (str) Serialized certificate information """ + account = self.get_account_or_default(account_id) - return await self.wallet_manager.export_certificate_info(claim_id) + return await self.wallet_manager.export_certificate_info(claim_id, account, password, insecure) @requires(WALLET_COMPONENT) - async def jsonrpc_channel_import(self, serialized_certificate_info): + async def jsonrpc_channel_import(self, serialized_certificate_info, password=None, account_id=None): """ Import serialized channel signing information (to allow signing new claims to the channel) @@ -2152,8 +2153,9 @@ class Daemon(metaclass=JSONRPCServerType): Returns: (dict) Result dictionary """ + account = self.get_account_or_default(account_id) - return await self.wallet_manager.import_certificate_info(serialized_certificate_info) + return await self.wallet_manager.import_certificate_info(serialized_certificate_info, password, account) STREAM_DOC = """ Create, update, abandon, list and inspect your stream claims. diff --git a/lbrynet/wallet/manager.py b/lbrynet/wallet/manager.py index d9627ead9..de5894f30 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 +from binascii import unhexlify, hexlify from datetime import datetime @@ -17,6 +17,9 @@ 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): @@ -301,6 +304,58 @@ 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 b1430bbe3..7dab340ca 100644 --- a/lbrynet/wallet/transaction.py +++ b/lbrynet/wallet/transaction.py @@ -159,6 +159,14 @@ 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 cb430704e..3671775b1 100644 --- a/tests/integration/test_claim_commands.py +++ b/tests/integration/test_claim_commands.py @@ -306,6 +306,32 @@ class ChannelCommands(CommandTestCase): txo = (await account2.get_channels())[0] self.assertIsNotNone(txo.private_key) + async def test_channel_export_import_without_password(self): + tx = await self.channel_create('@foo', '1.0') + claim_id = tx['outputs'][0]['claim_id'] + channel_private_key = (await self.account.get_channels())[0].private_key + + _account2 = await self.out(self.daemon.jsonrpc_account_create("Account 2")) + account2_id, account2 = _account2["id"], self.daemon.get_account_or_error(_account2['id']) + + # before exporting/importing channel + 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)) + + other_address = await account2.receiving.get_or_create_usable_address() + await self.out(self.channel_update(claim_id, claim_address=other_address)) + + # importing into second account + await self.daemon.jsonrpc_channel_import(serialized_channel_info, password=None, account_id=account2_id) + + # after exporting/importing channel + self.assertEqual(len(await self.daemon.jsonrpc_channel_list(account_id=account2_id)), 1) + txo_channel_account2 = (await account2.get_channels())[0] + + self.assertEqual(channel_private_key, txo_channel_account2.private_key) + class StreamCommands(CommandTestCase):