electrum

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

password_dialog.py (12218B)


      1 from typing import Callable, TYPE_CHECKING, Optional, Union
      2 import os
      3 
      4 from kivy.app import App
      5 from kivy.factory import Factory
      6 from kivy.properties import ObjectProperty
      7 from kivy.lang import Builder
      8 from decimal import Decimal
      9 from kivy.clock import Clock
     10 
     11 from electrum.util import InvalidPassword
     12 from electrum.wallet import WalletStorage, Wallet
     13 from electrum.gui.kivy.i18n import _
     14 from electrum.wallet_db import WalletDB
     15 
     16 from .wallets import WalletDialog
     17 
     18 if TYPE_CHECKING:
     19     from ...main_window import ElectrumWindow
     20     from electrum.wallet import Abstract_Wallet
     21     from electrum.storage import WalletStorage
     22 
     23 Builder.load_string('''
     24 #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH
     25 
     26 <PasswordDialog@Popup>
     27     id: popup
     28     title: 'Electrum'
     29     message: ''
     30     basename:''
     31     is_change: False
     32     hide_wallet_label: False
     33     require_password: True
     34     BoxLayout:
     35         size_hint: 1, 1
     36         orientation: 'vertical'
     37         spacing: '12dp'
     38         padding: '12dp'
     39         BoxLayout:
     40             size_hint: 1, None
     41             orientation: 'horizontal'
     42             height: '40dp'
     43             Label:
     44                 size_hint: 0.85, None
     45                 height: '40dp'
     46                 font_size: '20dp'
     47                 text: _('Wallet') + ': ' + root.basename
     48                 text_size: self.width, None
     49                 disabled: root.hide_wallet_label
     50                 opacity: 0 if root.hide_wallet_label else 1
     51             IconButton:
     52                 size_hint: 0.15, None
     53                 height: '40dp'
     54                 icon: f'atlas://{KIVY_GUI_PATH}/theming/light/btn_create_account'
     55                 on_release: root.select_file()
     56                 disabled: root.hide_wallet_label or root.is_change
     57                 opacity: 0 if root.hide_wallet_label or root.is_change else 1
     58         Widget:
     59             size_hint: 1, 0.05
     60         Label:
     61             size_hint: 0.70, None
     62             font_size: '20dp'
     63             text: root.message
     64             text_size: self.width, None
     65         Widget:
     66             size_hint: 1, 0.05
     67         BoxLayout:
     68             orientation: 'horizontal'
     69             id: box_generic_password
     70             disabled: not root.require_password
     71             opacity: int(root.require_password)
     72             size_hint_y: 0.05
     73             height: '40dp'
     74             TextInput:
     75                 height: '40dp'
     76                 id: textinput_generic_password
     77                 valign: 'center'
     78                 multiline: False
     79                 on_text_validate:
     80                     popup.on_password(self.text)
     81                 password: True
     82                 size_hint: 0.85, None
     83                 unfocus_on_touch: False
     84                 focus: True
     85             IconButton:
     86                 height: '40dp'
     87                 size_hint: 0.15, None
     88                 icon: f'atlas://{KIVY_GUI_PATH}/theming/light/eye1'
     89                 icon_size: '40dp'
     90                 on_release:
     91                     textinput_generic_password.password = False if textinput_generic_password.password else True
     92         Widget:
     93             size_hint: 1, 1
     94         BoxLayout:
     95             orientation: 'horizontal'
     96             size_hint: 1, 0.5
     97             Button:
     98                 text: 'Cancel'
     99                 size_hint: 0.5, None
    100                 height: '48dp'
    101                 on_release: popup.dismiss()
    102             Button:
    103                 text: 'Next'
    104                 size_hint: 0.5, None
    105                 height: '48dp'
    106                 on_release:
    107                     popup.on_password(textinput_generic_password.text)
    108 
    109 
    110 <PincodeDialog@Popup>
    111     id: popup
    112     title: 'Electrum'
    113     message: ''
    114     basename:''
    115     BoxLayout:
    116         size_hint: 1, 1
    117         orientation: 'vertical'
    118         Widget:
    119             size_hint: 1, 0.05
    120         Label:
    121             size_hint: 0.70, None
    122             font_size: '20dp'
    123             text: root.message
    124             text_size: self.width, None
    125         Widget:
    126             size_hint: 1, 0.05
    127         Label:
    128             id: label_pin
    129             size_hint_y: 0.05
    130             font_size: '50dp'
    131             text: '*'*len(kb.password) + '-'*(6-len(kb.password))
    132             size: self.texture_size
    133         Widget:
    134             size_hint: 1, 0.05
    135         GridLayout:
    136             id: kb
    137             size_hint: 1, None
    138             height: self.minimum_height
    139             update_amount: popup.update_password
    140             password: ''
    141             on_password: popup.on_password(self.password)
    142             spacing: '2dp'
    143             cols: 3
    144             KButton:
    145                 text: '1'
    146             KButton:
    147                 text: '2'
    148             KButton:
    149                 text: '3'
    150             KButton:
    151                 text: '4'
    152             KButton:
    153                 text: '5'
    154             KButton:
    155                 text: '6'
    156             KButton:
    157                 text: '7'
    158             KButton:
    159                 text: '8'
    160             KButton:
    161                 text: '9'
    162             KButton:
    163                 text: 'Clear'
    164             KButton:
    165                 text: '0'
    166             KButton:
    167                 text: '<'
    168 ''')
    169 
    170 
    171 class AbstractPasswordDialog(Factory.Popup):
    172 
    173     def __init__(self, app: 'ElectrumWindow', *,
    174              check_password = None,
    175              on_success: Callable = None, on_failure: Callable = None,
    176              is_change: bool = False,
    177              is_password: bool = True,  # whether this is for a generic password or for a numeric PIN
    178              has_password: bool = False,
    179              message: str = '',
    180              basename:str=''):
    181         Factory.Popup.__init__(self)
    182         self.app = app
    183         self.pw_check = check_password
    184         self.message = message
    185         self.on_success = on_success
    186         self.on_failure = on_failure
    187         self.success = False
    188         self.is_change = is_change
    189         self.pw = None
    190         self.new_password = None
    191         self.title = 'Electrum'
    192         self.level = 1 if is_change and not has_password else 0
    193         self.basename = basename
    194         self.update_screen()
    195 
    196     def update_screen(self):
    197         self.clear_password()
    198         if self.level == 0 and self.message == '':
    199             self.message = self.enter_pw_message
    200         elif self.level == 1:
    201             self.message = self.enter_new_pw_message
    202         elif self.level == 2:
    203             self.message = self.confirm_new_pw_message
    204 
    205     def check_password(self, password):
    206         if self.level > 0:
    207             return True
    208         try:
    209             self.pw_check(password)
    210             return True
    211         except InvalidPassword as e:
    212             return False
    213 
    214     def on_dismiss(self):
    215         if self.level == 1 and self.allow_disable and self.on_success:
    216             self.on_success(self.pw, None)
    217             return False
    218         if not self.success:
    219             if self.on_failure:
    220                 self.on_failure()
    221             else:
    222                 # keep dialog open
    223                 return True
    224         else:
    225             if self.on_success:
    226                 args = (self.pw, self.new_password) if self.is_change else (self.pw,)
    227                 Clock.schedule_once(lambda dt: self.on_success(*args), 0.1)
    228 
    229     def update_password(self, c):
    230         kb = self.ids.kb
    231         text = kb.password
    232         if c == '<':
    233             text = text[:-1]
    234         elif c == 'Clear':
    235             text = ''
    236         else:
    237             text += c
    238         kb.password = text
    239 
    240 
    241     def do_check(self, pw):
    242         if self.check_password(pw):
    243             if self.is_change is False:
    244                 self.success = True
    245                 self.pw = pw
    246                 self.message = _('Please wait...')
    247                 self.dismiss()
    248             elif self.level == 0:
    249                 self.level = 1
    250                 self.pw = pw
    251                 self.update_screen()
    252             elif self.level == 1:
    253                 self.level = 2
    254                 self.new_password = pw
    255                 self.update_screen()
    256             elif self.level == 2:
    257                 self.success = pw == self.new_password
    258                 self.dismiss()
    259         else:
    260             self.app.show_error(self.wrong_password_message)
    261             self.clear_password()
    262 
    263 
    264 class PasswordDialog(AbstractPasswordDialog):
    265     enter_pw_message = _('Enter your password')
    266     enter_new_pw_message = _('Enter new password')
    267     confirm_new_pw_message = _('Confirm new password')
    268     wrong_password_message = _('Wrong password')
    269     allow_disable = False
    270 
    271     def __init__(self, app, **kwargs):
    272         AbstractPasswordDialog.__init__(self, app, **kwargs)
    273         self.hide_wallet_label = app._use_single_password
    274 
    275     def clear_password(self):
    276         self.ids.textinput_generic_password.text = ''
    277 
    278     def on_password(self, pw: str):
    279         #
    280         if not self.require_password:
    281             self.success = True
    282             self.message = _('Please wait...')
    283             self.dismiss()
    284             return
    285         # if setting new generic password, enforce min length
    286         if self.level > 0:
    287             if len(pw) < 6:
    288                 self.app.show_error(_('Password is too short (min {} characters)').format(6))
    289                 return
    290         # don't enforce minimum length on existing
    291         self.do_check(pw)
    292 
    293 
    294 
    295 class PincodeDialog(AbstractPasswordDialog):
    296     enter_pw_message = _('Enter your PIN')
    297     enter_new_pw_message = _('Enter new PIN')
    298     confirm_new_pw_message = _('Confirm new PIN')
    299     wrong_password_message = _('Wrong PIN')
    300     allow_disable = True
    301 
    302     def __init__(self, app, **kwargs):
    303         AbstractPasswordDialog.__init__(self, app, **kwargs)
    304 
    305     def clear_password(self):
    306         self.ids.kb.password = ''
    307 
    308     def on_password(self, pw: str):
    309         # PIN codes are exactly 6 chars
    310         if len(pw) >= 6:
    311             self.do_check(pw)
    312 
    313 
    314 class ChangePasswordDialog(PasswordDialog):
    315 
    316     def __init__(self, app, wallet, on_success, on_failure):
    317         PasswordDialog.__init__(self, app,
    318             basename = wallet.basename(),
    319             check_password = wallet.check_password,
    320             on_success=on_success,
    321             on_failure=on_failure,
    322             is_change=True,
    323             has_password=wallet.has_password())
    324 
    325 
    326 class OpenWalletDialog(PasswordDialog):
    327     """This dialog will let the user choose another wallet file if they don't remember their the password"""
    328 
    329     def __init__(self, app, path, callback):
    330         self.app = app
    331         self.callback = callback
    332         PasswordDialog.__init__(self, app,
    333             on_success=lambda pw: self.callback(pw, self.storage),
    334             on_failure=self.app.stop)
    335         self.init_storage_from_path(path)
    336 
    337     def select_file(self):
    338         dirname = os.path.dirname(self.app.electrum_config.get_wallet_path())
    339         d = WalletDialog(dirname, self.init_storage_from_path, self.app.is_wallet_creation_disabled())
    340         d.open()
    341 
    342     def init_storage_from_path(self, path):
    343         self.storage = WalletStorage(path)
    344         self.basename = self.storage.basename()
    345         if not self.storage.file_exists():
    346             self.require_password = False
    347             self.message = _('Press Next to create')
    348         elif self.storage.is_encrypted():
    349             if not self.storage.is_encrypted_with_user_pw():
    350                 raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
    351             self.pw_check = self.storage.check_password
    352             if self.app.password and self.check_password(self.app.password):
    353                 self.pw = self.app.password # must be set so that it is returned in callback
    354                 self.require_password = False
    355                 self.message = _('Press Next to open')
    356             else:
    357                 self.require_password = True
    358                 self.message = self.enter_pw_message
    359         else:
    360             # it is a bit wasteful load the wallet here and load it again in main_window,
    361             # but that is fine, because we are progressively enforcing storage encryption.
    362             db = WalletDB(self.storage.read(), manual_upgrades=False)
    363             wallet = Wallet(db, self.storage, config=self.app.electrum_config)
    364             self.require_password = wallet.has_password()
    365             self.pw_check = wallet.check_password
    366             self.message = self.enter_pw_message if self.require_password else _('Wallet not encrypted')