LBRY-Vault/electrum/gui/kivy/uix/dialogs/password_dialog.py

273 lines
8.1 KiB
Python

from typing import Callable, TYPE_CHECKING, Optional, Union
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.gui.kivy.i18n import _
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
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
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
<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:
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=''):
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, Factory.Popup):
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 clear_password(self):
self.ids.textinput_generic_password.text = ''
def on_password(self, pw: str):
# 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)
def select_file(self):
self.app.wallets_dialog()
class PincodeDialog(AbstractPasswordDialog, Factory.Popup):
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 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)