LBRY-Vault/electrum/gui/kivy/uix/dialogs/password_dialog.py
ThomasV 88bb5309c4 Fix issue #6201:
- Pass a proper callback to WalletDialog
   (we used to call load_wallet_by_name recursively)
 - Do not cache PasswordDialog instances
2020-06-04 19:17:58 +02:00

353 lines
11 KiB
Python

from typing import Callable, TYPE_CHECKING, Optional, Union
import os
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from decimal import Decimal
from kivy.clock import Clock
from electrum.util import InvalidPassword
from electrum.wallet import WalletStorage
from electrum.gui.kivy.i18n import _
from .wallets import WalletDialog
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
from electrum.wallet import Abstract_Wallet
from electrum.storage import WalletStorage
Builder.load_string('''
<PasswordDialog@Popup>
id: popup
title: 'Electrum'
message: ''
basename:''
is_change: False
require_password: True
BoxLayout:
size_hint: 1, 1
orientation: 'vertical'
spacing: '12dp'
padding: '12dp'
BoxLayout:
size_hint: 1, None
orientation: 'horizontal'
height: '40dp'
Label:
size_hint: 0.85, None
height: '40dp'
font_size: '20dp'
text: _('Wallet') + ': ' + root.basename
text_size: self.width, None
IconButton:
size_hint: 0.15, None
height: '40dp'
icon: 'atlas://electrum/gui/kivy/theming/light/btn_create_account'
on_release: root.select_file()
disabled: root.is_change
opacity: 0 if root.is_change else 1
Widget:
size_hint: 1, 0.05
Label:
size_hint: 0.70, None
font_size: '20dp'
text: root.message
text_size: self.width, None
Widget:
size_hint: 1, 0.05
BoxLayout:
orientation: 'horizontal'
id: box_generic_password
disabled: not root.require_password
opacity: int(root.require_password)
size_hint_y: 0.05
height: '40dp'
TextInput:
height: '40dp'
id: textinput_generic_password
valign: 'center'
multiline: False
on_text_validate:
popup.on_password(self.text)
password: True
size_hint: 0.85, None
unfocus_on_touch: False
focus: True
IconButton:
height: '40dp'
size_hint: 0.15, None
icon: 'atlas://electrum/gui/kivy/theming/light/eye1'
icon_size: '40dp'
on_release:
textinput_generic_password.password = False if textinput_generic_password.password else True
Widget:
size_hint: 1, 1
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Button:
text: 'Cancel'
size_hint: 0.5, None
height: '48dp'
on_release: popup.dismiss()
Button:
text: 'Next'
size_hint: 0.5, None
height: '48dp'
on_release:
popup.on_password(textinput_generic_password.text)
<PincodeDialog@Popup>
id: popup
title: 'Electrum'
message: ''
basename:''
BoxLayout:
size_hint: 1, 1
orientation: 'vertical'
Widget:
size_hint: 1, 0.05
Label:
size_hint: 0.70, None
font_size: '20dp'
text: root.message
text_size: self.width, None
Widget:
size_hint: 1, 0.05
Label:
id: label_pin
size_hint_y: 0.05
font_size: '50dp'
text: '*'*len(kb.password) + '-'*(6-len(kb.password))
size: self.texture_size
Widget:
size_hint: 1, 0.05
GridLayout:
id: kb
size_hint: 1, None
height: self.minimum_height
update_amount: popup.update_password
password: ''
on_password: popup.on_password(self.password)
spacing: '2dp'
cols: 3
KButton:
text: '1'
KButton:
text: '2'
KButton:
text: '3'
KButton:
text: '4'
KButton:
text: '5'
KButton:
text: '6'
KButton:
text: '7'
KButton:
text: '8'
KButton:
text: '9'
KButton:
text: 'Clear'
KButton:
text: '0'
KButton:
text: '<'
''')
class AbstractPasswordDialog(Factory.Popup):
def __init__(self, app: 'ElectrumWindow', *,
check_password = None,
on_success: Callable = None, on_failure: Callable = None,
is_change: bool = False,
is_password: bool = True, # whether this is for a generic password or for a numeric PIN
has_password: bool = False,
message: str = '',
basename:str=''):
Factory.Popup.__init__(self)
self.app = app
self.pw_check = check_password
self.message = message
self.on_success = on_success
self.on_failure = on_failure
self.success = False
self.is_change = is_change
self.pw = None
self.new_password = None
self.title = 'Electrum'
self.level = 1 if is_change and not has_password else 0
self.basename = basename
self.update_screen()
def update_screen(self):
self.clear_password()
if self.level == 0 and self.message == '':
self.message = self.enter_pw_message
elif self.level == 1:
self.message = self.enter_new_pw_message
elif self.level == 2:
self.message = self.confirm_new_pw_message
def check_password(self, password):
if self.level > 0:
return True
try:
self.pw_check(password)
return True
except InvalidPassword as e:
return False
def on_dismiss(self):
if self.level == 1 and self.allow_disable and self.on_success:
self.on_success(self.pw, None)
return False
if not self.success:
if self.on_failure:
self.on_failure()
else:
# keep dialog open
return True
else:
if self.on_success:
args = (self.pw, self.new_password) if self.is_change else (self.pw,)
Clock.schedule_once(lambda dt: self.on_success(*args), 0.1)
def update_password(self, c):
kb = self.ids.kb
text = kb.password
if c == '<':
text = text[:-1]
elif c == 'Clear':
text = ''
else:
text += c
kb.password = text
def do_check(self, pw):
if self.check_password(pw):
if self.is_change is False:
self.success = True
self.pw = pw
self.message = _('Please wait...')
self.dismiss()
elif self.level == 0:
self.level = 1
self.pw = pw
self.update_screen()
elif self.level == 1:
self.level = 2
self.new_password = pw
self.update_screen()
elif self.level == 2:
self.success = pw == self.new_password
self.dismiss()
else:
self.app.show_error(self.wrong_password_message)
self.clear_password()
class PasswordDialog(AbstractPasswordDialog):
enter_pw_message = _('Enter your password')
enter_new_pw_message = _('Enter new password')
confirm_new_pw_message = _('Confirm new password')
wrong_password_message = _('Wrong password')
allow_disable = False
def __init__(self, app, **kwargs):
AbstractPasswordDialog.__init__(self, app, **kwargs)
def clear_password(self):
self.ids.textinput_generic_password.text = ''
def on_password(self, pw: str):
#
if not self.require_password:
self.success = True
self.message = _('Please wait...')
self.dismiss()
return
# if setting new generic password, enforce min length
if self.level > 0:
if len(pw) < 6:
self.app.show_error(_('Password is too short (min {} characters)').format(6))
return
# don't enforce minimum length on existing
self.do_check(pw)
class PincodeDialog(AbstractPasswordDialog):
enter_pw_message = _('Enter your PIN')
enter_new_pw_message = _('Enter new PIN')
confirm_new_pw_message = _('Confirm new PIN')
wrong_password_message = _('Wrong PIN')
allow_disable = True
def __init__(self, app, **kwargs):
AbstractPasswordDialog.__init__(self, app, **kwargs)
def clear_password(self):
self.ids.kb.password = ''
def on_password(self, pw: str):
# PIN codes are exactly 6 chars
if len(pw) >= 6:
self.do_check(pw)
class ChangePasswordDialog(PasswordDialog):
def __init__(self, app, wallet, on_success, on_failure):
PasswordDialog.__init__(self, app,
basename = wallet.basename(),
check_password = wallet.check_password,
on_success=on_success,
on_failure=on_failure,
is_change=True,
has_password=wallet.has_password())
class OpenWalletDialog(PasswordDialog):
def __init__(self, app, path, callback):
self.app = app
self.callback = callback
PasswordDialog.__init__(self, app,
on_success=lambda pw: self.callback(pw, self.storage),
on_failure=self.app.stop)
self.init_storage_from_path(path)
def select_file(self):
dirname = os.path.dirname(self.app.electrum_config.get_wallet_path())
d = WalletDialog(dirname, self.init_storage_from_path)
d.open()
def init_storage_from_path(self, path):
self.storage = WalletStorage(path)
self.basename = self.storage.basename()
if not self.storage.file_exists():
self.require_password = False
self.message = _('Press Next to create')
elif self.storage.is_encrypted():
if not self.storage.is_encrypted_with_user_pw():
raise Exception("Kivy GUI does not support this type of encrypted wallet files.")
self.require_password = True
self.pw_check = self.storage.check_password
self.message = self.enter_pw_message
else:
# it is a bit wasteful load the wallet here and load it again in main_window,
# but that is fine, because we are progressively enforcing storage encryption.
wallet = self.app.daemon.load_wallet(path, None)
self.require_password = wallet.has_password()
self.pw_check = wallet.check_password
self.message = self.enter_pw_message if self.require_password else _('Wallet not encrypted')