electrum

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

labels.py (7615B)


      1 import asyncio
      2 import hashlib
      3 import json
      4 import sys
      5 import traceback
      6 from typing import Union, TYPE_CHECKING
      7 
      8 import base64
      9 
     10 from electrum.plugin import BasePlugin, hook
     11 from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv
     12 from electrum.i18n import _
     13 from electrum.util import log_exceptions, ignore_exceptions, make_aiohttp_session
     14 from electrum.network import Network
     15 
     16 if TYPE_CHECKING:
     17     from electrum.wallet import Abstract_Wallet
     18 
     19 
     20 class ErrorConnectingServer(Exception):
     21     def __init__(self, reason: Union[str, Exception] = None):
     22         self.reason = reason
     23 
     24     def __str__(self):
     25         header = _("Error connecting to {} server").format('Labels')
     26         reason = self.reason
     27         if isinstance(reason, BaseException):
     28             reason = repr(reason)
     29         return f"{header}: {reason}" if reason else header
     30 
     31 
     32 class LabelsPlugin(BasePlugin):
     33 
     34     def __init__(self, parent, config, name):
     35         BasePlugin.__init__(self, parent, config, name)
     36         self.target_host = 'labels.electrum.org'
     37         self.wallets = {}
     38 
     39     def encode(self, wallet: 'Abstract_Wallet', msg: str) -> str:
     40         password, iv, wallet_id = self.wallets[wallet]
     41         encrypted = aes_encrypt_with_iv(password, iv, msg.encode('utf8'))
     42         return base64.b64encode(encrypted).decode()
     43 
     44     def decode(self, wallet: 'Abstract_Wallet', message: str) -> str:
     45         password, iv, wallet_id = self.wallets[wallet]
     46         decoded = base64.b64decode(message)
     47         decrypted = aes_decrypt_with_iv(password, iv, decoded)
     48         return decrypted.decode('utf8')
     49 
     50     def get_nonce(self, wallet: 'Abstract_Wallet'):
     51         # nonce is the nonce to be used with the next change
     52         nonce = wallet.db.get('wallet_nonce')
     53         if nonce is None:
     54             nonce = 1
     55             self.set_nonce(wallet, nonce)
     56         return nonce
     57 
     58     def set_nonce(self, wallet: 'Abstract_Wallet', nonce):
     59         self.logger.info(f"set {wallet.basename()} nonce to {nonce}")
     60         wallet.db.put("wallet_nonce", nonce)
     61 
     62     @hook
     63     def set_label(self, wallet: 'Abstract_Wallet', item, label):
     64         if wallet not in self.wallets:
     65             return
     66         if not item:
     67             return
     68         nonce = self.get_nonce(wallet)
     69         wallet_id = self.wallets[wallet][2]
     70         bundle = {"walletId": wallet_id,
     71                   "walletNonce": nonce,
     72                   "externalId": self.encode(wallet, item),
     73                   "encryptedLabel": self.encode(wallet, label)}
     74         asyncio.run_coroutine_threadsafe(self.do_post_safe("/label", bundle), wallet.network.asyncio_loop)
     75         # Caller will write the wallet
     76         self.set_nonce(wallet, nonce + 1)
     77 
     78     @ignore_exceptions
     79     @log_exceptions
     80     async def do_post_safe(self, *args):
     81         await self.do_post(*args)
     82 
     83     async def do_get(self, url = "/labels"):
     84         url = 'https://' + self.target_host + url
     85         network = Network.get_instance()
     86         proxy = network.proxy if network else None
     87         async with make_aiohttp_session(proxy) as session:
     88             async with session.get(url) as result:
     89                 return await result.json()
     90 
     91     async def do_post(self, url = "/labels", data=None):
     92         url = 'https://' + self.target_host + url
     93         network = Network.get_instance()
     94         proxy = network.proxy if network else None
     95         async with make_aiohttp_session(proxy) as session:
     96             async with session.post(url, json=data) as result:
     97                 try:
     98                     return await result.json()
     99                 except Exception as e:
    100                     raise Exception('Could not decode: ' + await result.text()) from e
    101 
    102     async def push_thread(self, wallet: 'Abstract_Wallet'):
    103         wallet_data = self.wallets.get(wallet, None)
    104         if not wallet_data:
    105             raise Exception('Wallet {} not loaded'.format(wallet))
    106         wallet_id = wallet_data[2]
    107         bundle = {"labels": [],
    108                   "walletId": wallet_id,
    109                   "walletNonce": self.get_nonce(wallet)}
    110         for key, value in wallet.get_all_labels().items():
    111             try:
    112                 encoded_key = self.encode(wallet, key)
    113                 encoded_value = self.encode(wallet, value)
    114             except:
    115                 self.logger.info(f'cannot encode {repr(key)} {repr(value)}')
    116                 continue
    117             bundle["labels"].append({'encryptedLabel': encoded_value,
    118                                      'externalId': encoded_key})
    119         await self.do_post("/labels", bundle)
    120 
    121     async def pull_thread(self, wallet: 'Abstract_Wallet', force: bool):
    122         wallet_data = self.wallets.get(wallet, None)
    123         if not wallet_data:
    124             raise Exception('Wallet {} not loaded'.format(wallet))
    125         wallet_id = wallet_data[2]
    126         nonce = 1 if force else self.get_nonce(wallet) - 1
    127         self.logger.info(f"asking for labels since nonce {nonce}")
    128         try:
    129             response = await self.do_get("/labels/since/%d/for/%s" % (nonce, wallet_id))
    130         except Exception as e:
    131             raise ErrorConnectingServer(e) from e
    132         if response["labels"] is None:
    133             self.logger.info('no new labels')
    134             return
    135         result = {}
    136         for label in response["labels"]:
    137             try:
    138                 key = self.decode(wallet, label["externalId"])
    139                 value = self.decode(wallet, label["encryptedLabel"])
    140             except:
    141                 continue
    142             try:
    143                 json.dumps(key)
    144                 json.dumps(value)
    145             except:
    146                 self.logger.info(f'error: no json {key}')
    147                 continue
    148             result[key] = value
    149 
    150         for key, value in result.items():
    151             if force or not wallet.get_label(key):
    152                 wallet._set_label(key, value)
    153 
    154         self.logger.info(f"received {len(response)} labels")
    155         self.set_nonce(wallet, response["nonce"] + 1)
    156         self.on_pulled(wallet)
    157 
    158     def on_pulled(self, wallet: 'Abstract_Wallet') -> None:
    159         raise NotImplementedError()
    160 
    161     @ignore_exceptions
    162     @log_exceptions
    163     async def pull_safe_thread(self, wallet: 'Abstract_Wallet', force: bool):
    164         try:
    165             await self.pull_thread(wallet, force)
    166         except ErrorConnectingServer as e:
    167             self.logger.info(repr(e))
    168 
    169     def pull(self, wallet: 'Abstract_Wallet', force: bool):
    170         if not wallet.network: raise Exception(_('You are offline.'))
    171         return asyncio.run_coroutine_threadsafe(self.pull_thread(wallet, force), wallet.network.asyncio_loop).result()
    172 
    173     def push(self, wallet: 'Abstract_Wallet'):
    174         if not wallet.network: raise Exception(_('You are offline.'))
    175         return asyncio.run_coroutine_threadsafe(self.push_thread(wallet), wallet.network.asyncio_loop).result()
    176 
    177     def start_wallet(self, wallet: 'Abstract_Wallet'):
    178         if not wallet.network: return  # 'offline' mode
    179         mpk = wallet.get_fingerprint()
    180         if not mpk:
    181             return
    182         mpk = mpk.encode('ascii')
    183         password = hashlib.sha1(mpk).hexdigest()[:32].encode('ascii')
    184         iv = hashlib.sha256(password).digest()[:16]
    185         wallet_id = hashlib.sha256(mpk).hexdigest()
    186         self.wallets[wallet] = (password, iv, wallet_id)
    187         nonce = self.get_nonce(wallet)
    188         self.logger.info(f"wallet {wallet.basename()} nonce is {nonce}")
    189         # If there is an auth token we can try to actually start syncing
    190         asyncio.run_coroutine_threadsafe(self.pull_safe_thread(wallet, False), wallet.network.asyncio_loop)
    191 
    192     def stop_wallet(self, wallet):
    193         self.wallets.pop(wallet, None)