electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit fd5b1acdc896fbe86d585b2e721edde7a357afc3
parent 387834164cb28a0853d98f609491bab74d1c0975
Author: SomberNight <somber.night@protonmail.com>
Date:   Fri,  3 May 2019 03:10:31 +0200

commands: fix encrypt/decrypt

based on Electron-Cash/Electron-Cash@62aa08a0ffde227ababe58a14285b588eab4fd15

Diffstat:
Melectrum/commands.py | 27+++++++++++++++++----------
Melectrum/ecc.py | 2+-
Melectrum/keystore.py | 4++--
Melectrum/tests/__init__.py | 2+-
Melectrum/tests/test_commands.py | 13+++++++++++++
Melectrum/util.py | 9+++++++--
Melectrum/wallet.py | 5+++--
7 files changed, 44 insertions(+), 18 deletions(-)

diff --git a/electrum/commands.py b/electrum/commands.py @@ -35,20 +35,16 @@ from decimal import Decimal from typing import Optional, TYPE_CHECKING from .import util, ecc -from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str +from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes from . import bitcoin from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS -from . import bip32 from .bip32 import BIP32Node from .i18n import _ from .transaction import Transaction, multisig_script, TxOutput from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .synchronizer import Notifier -from .storage import WalletStorage -from . import keystore -from .wallet import Wallet, Imported_Wallet, Abstract_Wallet, create_new_wallet, restore_wallet_from_text +from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text from .address_synchronizer import TX_HEIGHT_LOCAL -from .mnemonic import Mnemonic if TYPE_CHECKING: from .network import Network @@ -582,16 +578,27 @@ class Commands: return tx.as_dict() @command('') - def encrypt(self, pubkey, message): + def encrypt(self, pubkey, message) -> str: """Encrypt a message with a public key. Use quotes if the message contains whitespaces.""" + if not is_hex_str(pubkey): + raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}") + try: + message = to_bytes(message) + except TypeError: + raise Exception(f"message must be a string-like object instead of {repr(message)}") public_key = ecc.ECPubkey(bfh(pubkey)) encrypted = public_key.encrypt_message(message) - return encrypted + return encrypted.decode('utf-8') @command('wp') - def decrypt(self, pubkey, encrypted, password=None): + def decrypt(self, pubkey, encrypted, password=None) -> str: """Decrypt a message encrypted with a public key.""" - return self.wallet.decrypt_message(pubkey, encrypted, password) + if not is_hex_str(pubkey): + raise Exception(f"pubkey must be a hex string instead of {repr(pubkey)}") + if not isinstance(encrypted, (str, bytes, bytearray)): + raise Exception(f"encrypted must be a string-like object instead of {repr(encrypted)}") + decrypted = self.wallet.decrypt_message(pubkey, encrypted, password) + return decrypted.decode('utf-8') def _format_request(self, out): pr_str = { diff --git a/electrum/ecc.py b/electrum/ecc.py @@ -274,7 +274,7 @@ class ECPubkey(object): verifying_key = _MyVerifyingKey.from_public_point(ecdsa_point, curve=SECP256k1) verifying_key.verify_digest(sig_string, msg_hash, sigdecode=ecdsa.util.sigdecode_string) - def encrypt_message(self, message: bytes, magic: bytes = b'BIE1'): + def encrypt_message(self, message: bytes, magic: bytes = b'BIE1') -> bytes: """ ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac """ diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -105,12 +105,12 @@ class Software_KeyStore(KeyStore): def may_have_password(self): return not self.is_watching_only() - def sign_message(self, sequence, message, password): + def sign_message(self, sequence, message, password) -> bytes: privkey, compressed = self.get_private_key(sequence, password) key = ecc.ECPrivkey(privkey) return key.sign_message(message, compressed) - def decrypt_message(self, sequence, message, password): + def decrypt_message(self, sequence, message, password) -> bytes: privkey, compressed = self.get_private_key(sequence, password) ec = ecc.ECPrivkey(privkey) decrypted = ec.decrypt_message(message) diff --git a/electrum/tests/__init__.py b/electrum/tests/__init__.py @@ -8,7 +8,7 @@ from electrum import constants # If set, unit tests that would normally test functions with multiple implementations, # will only be run once, using the fastest implementation. # e.g. libsecp256k1 vs python-ecdsa. pycryptodomex vs pyaes. -FAST_TESTS = False +FAST_TESTS = 1 # some unit tests are modifying globals; sorry. diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py @@ -1,7 +1,10 @@ import unittest +from unittest import mock from decimal import Decimal from electrum.commands import Commands, eval_bool +from electrum import storage +from electrum.wallet import restore_wallet_from_text from . import TestCaseForTestnet @@ -62,6 +65,16 @@ class TestCommands(unittest.TestCase): for xkey2, xtype2 in xprvs: self.assertEqual(xkey2, cmds.convert_xkey(xkey1, xtype2)) + @mock.patch.object(storage.WalletStorage, '_write') + def test_encrypt_decrypt(self, mock_write): + wallet = restore_wallet_from_text('p2wpkh:L4rYY5QpfN6wJEF4SEKDpcGhTPnCe9zcGs6hiSnhpprZqVywFifN', + path='if_this_exists_mocking_failed_648151893')['wallet'] + cmds = Commands(config=None, wallet=wallet, network=None) + cleartext = "asdasd this is the message" + pubkey = "021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da" + ciphertext = cmds.encrypt(pubkey, cleartext) + self.assertEqual(cleartext, cmds.decrypt(pubkey, ciphertext)) + class TestCommandsTestnet(TestCaseForTestnet): diff --git a/electrum/util.py b/electrum/util.py @@ -23,7 +23,7 @@ import binascii import os, sys, re, json from collections import defaultdict, OrderedDict -from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable +from typing import NamedTuple, Union, TYPE_CHECKING, Tuple, Optional, Callable, Any from datetime import datetime import decimal from decimal import Decimal @@ -493,9 +493,14 @@ def is_valid_email(s): return re.match(regexp, s) is not None -def is_hash256_str(text: str) -> bool: +def is_hash256_str(text: Any) -> bool: if not isinstance(text, str): return False if len(text) != 64: return False + return is_hex_str(text) + + +def is_hex_str(text: Any) -> bool: + if not isinstance(text, str): return False try: bytes.fromhex(text) except: diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -1252,7 +1252,7 @@ class Abstract_Wallet(AddressSynchronizer): index = self.get_address_index(address) return self.keystore.sign_message(index, message, password) - def decrypt_message(self, pubkey, message, password): + def decrypt_message(self, pubkey, message, password) -> bytes: addr = self.pubkeys_to_address(pubkey) index = self.get_address_index(addr) return self.keystore.decrypt_message(index, message, password) @@ -1889,7 +1889,8 @@ def create_new_wallet(*, path, passphrase=None, password=None, encrypt_file=True return {'seed': seed, 'wallet': wallet, 'msg': msg} -def restore_wallet_from_text(text, *, path, network, passphrase=None, password=None, encrypt_file=True): +def restore_wallet_from_text(text, *, path, network=None, + passphrase=None, password=None, encrypt_file=True): """Restore a wallet from text. Text can be a seed phrase, a master public key, a master private key, a list of bitcoin addresses or bitcoin private keys."""