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