You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
12 KiB
366 lines
12 KiB
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, Wallet |
|
from electrum.gui.kivy.i18n import _ |
|
from electrum.wallet_db import WalletDB |
|
|
|
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(''' |
|
#:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH |
|
|
|
<PasswordDialog@Popup> |
|
id: popup |
|
title: 'Electrum' |
|
message: '' |
|
basename:'' |
|
is_change: False |
|
hide_wallet_label: 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 |
|
disabled: root.hide_wallet_label |
|
opacity: 0 if root.hide_wallet_label else 1 |
|
IconButton: |
|
size_hint: 0.15, None |
|
height: '40dp' |
|
icon: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/btn_create_account' |
|
on_release: root.select_file() |
|
disabled: root.hide_wallet_label or root.is_change |
|
opacity: 0 if root.hide_wallet_label or 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: f'atlas://{KIVY_GUI_PATH}/theming/atlas/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) |
|
self.hide_wallet_label = app._use_single_password |
|
|
|
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): |
|
"""This dialog will let the user choose another wallet file if they don't remember their the password""" |
|
|
|
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, self.app.is_wallet_creation_disabled()) |
|
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.pw_check = self.storage.check_password |
|
if self.app.password and self.check_password(self.app.password): |
|
self.pw = self.app.password # must be set so that it is returned in callback |
|
self.require_password = False |
|
self.message = _('Press Next to open') |
|
else: |
|
self.require_password = True |
|
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. |
|
db = WalletDB(self.storage.read(), storage=self.storage, manual_upgrades=False) |
|
wallet = Wallet(db, config=self.app.electrum_config) |
|
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')
|
|
|