password_dialog.py (11097B)
1 #!/usr/bin/env python 2 # 3 # Electrum - lightweight Bitcoin client 4 # Copyright (C) 2013 ecdsa@github 5 # 6 # Permission is hereby granted, free of charge, to any person 7 # obtaining a copy of this software and associated documentation files 8 # (the "Software"), to deal in the Software without restriction, 9 # including without limitation the rights to use, copy, modify, merge, 10 # publish, distribute, sublicense, and/or sell copies of the Software, 11 # and to permit persons to whom the Software is furnished to do so, 12 # subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be 15 # included in all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 # SOFTWARE. 25 26 import re 27 import math 28 from functools import partial 29 30 from PyQt5.QtCore import Qt 31 from PyQt5.QtGui import QPixmap 32 from PyQt5.QtWidgets import QLineEdit, QLabel, QGridLayout, QVBoxLayout, QCheckBox 33 34 from electrum.i18n import _ 35 from electrum.plugin import run_hook 36 37 from .util import (icon_path, WindowModalDialog, OkButton, CancelButton, Buttons, 38 PasswordLineEdit) 39 40 41 def check_password_strength(password): 42 43 ''' 44 Check the strength of the password entered by the user and return back the same 45 :param password: password entered by user in New Password 46 :return: password strength Weak or Medium or Strong 47 ''' 48 password = password 49 n = math.log(len(set(password))) 50 num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None 51 caps = password != password.upper() and password != password.lower() 52 extra = re.match("^[a-zA-Z0-9]*$", password) is None 53 score = len(password)*( n + caps + num + extra)/20 54 password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"} 55 return password_strength[min(3, int(score))] 56 57 58 PW_NEW, PW_CHANGE, PW_PASSPHRASE = range(0, 3) 59 60 61 class PasswordLayout(object): 62 63 titles = [_("Enter Password"), _("Change Password"), _("Enter Passphrase")] 64 65 def __init__(self, msg, kind, OK_button, wallet=None, force_disable_encrypt_cb=False): 66 self.wallet = wallet 67 68 self.pw = PasswordLineEdit() 69 self.new_pw = PasswordLineEdit() 70 self.conf_pw = PasswordLineEdit() 71 self.kind = kind 72 self.OK_button = OK_button 73 74 vbox = QVBoxLayout() 75 label = QLabel(msg + "\n") 76 label.setWordWrap(True) 77 78 grid = QGridLayout() 79 grid.setSpacing(8) 80 grid.setColumnMinimumWidth(0, 150) 81 grid.setColumnMinimumWidth(1, 100) 82 grid.setColumnStretch(1,1) 83 84 if kind == PW_PASSPHRASE: 85 vbox.addWidget(label) 86 msgs = [_('Passphrase:'), _('Confirm Passphrase:')] 87 else: 88 logo_grid = QGridLayout() 89 logo_grid.setSpacing(8) 90 logo_grid.setColumnMinimumWidth(0, 70) 91 logo_grid.setColumnStretch(1,1) 92 93 logo = QLabel() 94 logo.setAlignment(Qt.AlignCenter) 95 96 logo_grid.addWidget(logo, 0, 0) 97 logo_grid.addWidget(label, 0, 1, 1, 2) 98 vbox.addLayout(logo_grid) 99 100 m1 = _('New Password:') if kind == PW_CHANGE else _('Password:') 101 msgs = [m1, _('Confirm Password:')] 102 if wallet and wallet.has_password(): 103 grid.addWidget(QLabel(_('Current Password:')), 0, 0) 104 grid.addWidget(self.pw, 0, 1) 105 lockfile = "lock.png" 106 else: 107 lockfile = "unlock.png" 108 logo.setPixmap(QPixmap(icon_path(lockfile)) 109 .scaledToWidth(36, mode=Qt.SmoothTransformation)) 110 111 grid.addWidget(QLabel(msgs[0]), 1, 0) 112 grid.addWidget(self.new_pw, 1, 1) 113 114 grid.addWidget(QLabel(msgs[1]), 2, 0) 115 grid.addWidget(self.conf_pw, 2, 1) 116 vbox.addLayout(grid) 117 118 # Password Strength Label 119 if kind != PW_PASSPHRASE: 120 self.pw_strength = QLabel() 121 grid.addWidget(self.pw_strength, 3, 0, 1, 2) 122 self.new_pw.textChanged.connect(self.pw_changed) 123 124 self.encrypt_cb = QCheckBox(_('Encrypt wallet file')) 125 self.encrypt_cb.setEnabled(False) 126 grid.addWidget(self.encrypt_cb, 4, 0, 1, 2) 127 self.encrypt_cb.setVisible(kind != PW_PASSPHRASE) 128 129 def enable_OK(): 130 ok = self.new_pw.text() == self.conf_pw.text() 131 OK_button.setEnabled(ok) 132 self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()) 133 and not force_disable_encrypt_cb) 134 self.new_pw.textChanged.connect(enable_OK) 135 self.conf_pw.textChanged.connect(enable_OK) 136 137 self.vbox = vbox 138 139 def title(self): 140 return self.titles[self.kind] 141 142 def layout(self): 143 return self.vbox 144 145 def pw_changed(self): 146 password = self.new_pw.text() 147 if password: 148 colors = {"Weak":"Red", "Medium":"Blue", "Strong":"Green", 149 "Very Strong":"Green"} 150 strength = check_password_strength(password) 151 label = (_("Password Strength") + ": " + "<font color=" 152 + colors[strength] + ">" + strength + "</font>") 153 else: 154 label = "" 155 self.pw_strength.setText(label) 156 157 def old_password(self): 158 if self.kind == PW_CHANGE: 159 return self.pw.text() or None 160 return None 161 162 def new_password(self): 163 pw = self.new_pw.text() 164 # Empty passphrases are fine and returned empty. 165 if pw == "" and self.kind != PW_PASSPHRASE: 166 pw = None 167 return pw 168 169 def clear_password_fields(self): 170 for field in [self.pw, self.new_pw, self.conf_pw]: 171 field.clear() 172 173 174 class PasswordLayoutForHW(object): 175 176 def __init__(self, msg, wallet=None): 177 self.wallet = wallet 178 179 vbox = QVBoxLayout() 180 label = QLabel(msg + "\n") 181 label.setWordWrap(True) 182 183 grid = QGridLayout() 184 grid.setSpacing(8) 185 grid.setColumnMinimumWidth(0, 150) 186 grid.setColumnMinimumWidth(1, 100) 187 grid.setColumnStretch(1,1) 188 189 logo_grid = QGridLayout() 190 logo_grid.setSpacing(8) 191 logo_grid.setColumnMinimumWidth(0, 70) 192 logo_grid.setColumnStretch(1,1) 193 194 logo = QLabel() 195 logo.setAlignment(Qt.AlignCenter) 196 197 logo_grid.addWidget(logo, 0, 0) 198 logo_grid.addWidget(label, 0, 1, 1, 2) 199 vbox.addLayout(logo_grid) 200 201 if wallet and wallet.has_storage_encryption(): 202 lockfile = "lock.png" 203 else: 204 lockfile = "unlock.png" 205 logo.setPixmap(QPixmap(icon_path(lockfile)) 206 .scaledToWidth(36, mode=Qt.SmoothTransformation)) 207 208 vbox.addLayout(grid) 209 210 self.encrypt_cb = QCheckBox(_('Encrypt wallet file')) 211 grid.addWidget(self.encrypt_cb, 1, 0, 1, 2) 212 213 self.vbox = vbox 214 215 def title(self): 216 return _("Toggle Encryption") 217 218 def layout(self): 219 return self.vbox 220 221 222 class ChangePasswordDialogBase(WindowModalDialog): 223 224 def __init__(self, parent, wallet): 225 WindowModalDialog.__init__(self, parent) 226 is_encrypted = wallet.has_storage_encryption() 227 OK_button = OkButton(self) 228 229 self.create_password_layout(wallet, is_encrypted, OK_button) 230 231 self.setWindowTitle(self.playout.title()) 232 vbox = QVBoxLayout(self) 233 vbox.addLayout(self.playout.layout()) 234 vbox.addStretch(1) 235 vbox.addLayout(Buttons(CancelButton(self), OK_button)) 236 self.playout.encrypt_cb.setChecked(is_encrypted) 237 238 def create_password_layout(self, wallet, is_encrypted, OK_button): 239 raise NotImplementedError() 240 241 242 class ChangePasswordDialogForSW(ChangePasswordDialogBase): 243 244 def __init__(self, parent, wallet): 245 ChangePasswordDialogBase.__init__(self, parent, wallet) 246 if not wallet.has_password(): 247 self.playout.encrypt_cb.setChecked(True) 248 249 def create_password_layout(self, wallet, is_encrypted, OK_button): 250 if not wallet.has_password(): 251 msg = _('Your wallet is not protected.') 252 msg += ' ' + _('Use this dialog to add a password to your wallet.') 253 else: 254 if not is_encrypted: 255 msg = _('Your bitcoins are password protected. However, your wallet file is not encrypted.') 256 else: 257 msg = _('Your wallet is password protected and encrypted.') 258 msg += ' ' + _('Use this dialog to change your password.') 259 self.playout = PasswordLayout(msg=msg, 260 kind=PW_CHANGE, 261 OK_button=OK_button, 262 wallet=wallet, 263 force_disable_encrypt_cb=not wallet.can_have_keystore_encryption()) 264 265 def run(self): 266 try: 267 if not self.exec_(): 268 return False, None, None, None 269 return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked() 270 finally: 271 self.playout.clear_password_fields() 272 273 274 class ChangePasswordDialogForHW(ChangePasswordDialogBase): 275 276 def __init__(self, parent, wallet): 277 ChangePasswordDialogBase.__init__(self, parent, wallet) 278 279 def create_password_layout(self, wallet, is_encrypted, OK_button): 280 if not is_encrypted: 281 msg = _('Your wallet file is NOT encrypted.') 282 else: 283 msg = _('Your wallet file is encrypted.') 284 msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.') 285 msg += '\n' + _('Use this dialog to toggle encryption.') 286 self.playout = PasswordLayoutForHW(msg) 287 288 def run(self): 289 if not self.exec_(): 290 return False, None 291 return True, self.playout.encrypt_cb.isChecked() 292 293 294 class PasswordDialog(WindowModalDialog): 295 296 def __init__(self, parent=None, msg=None): 297 msg = msg or _('Please enter your password') 298 WindowModalDialog.__init__(self, parent, _("Enter Password")) 299 self.pw = pw = PasswordLineEdit() 300 vbox = QVBoxLayout() 301 vbox.addWidget(QLabel(msg)) 302 grid = QGridLayout() 303 grid.setSpacing(8) 304 grid.addWidget(QLabel(_('Password')), 1, 0) 305 grid.addWidget(pw, 1, 1) 306 vbox.addLayout(grid) 307 vbox.addLayout(Buttons(CancelButton(self), OkButton(self))) 308 self.setLayout(vbox) 309 run_hook('password_dialog', pw, grid, 1) 310 311 def run(self): 312 try: 313 if not self.exec_(): 314 return 315 return self.pw.text() 316 finally: 317 self.pw.clear()