URI: 
       tpassword_dialog.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tpassword_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')