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')