1#!/usr/local/bin/python3.8 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 26import re 27import math 28from functools import partial 29 30from PyQt5.QtCore import Qt 31from PyQt5.QtGui import QPixmap 32from PyQt5.QtWidgets import QLineEdit, QLabel, QGridLayout, QVBoxLayout, QCheckBox 33 34from electrum.i18n import _ 35from electrum.plugin import run_hook 36 37from .util import (icon_path, WindowModalDialog, OkButton, CancelButton, Buttons, 38 PasswordLineEdit) 39 40 41def 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 58PW_NEW, PW_CHANGE, PW_PASSPHRASE = range(0, 3) 59 60 61class 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 if kind == PW_PASSPHRASE: 128 self.encrypt_cb.setVisible(False) 129 130 def enable_OK(): 131 ok = self.new_pw.text() == self.conf_pw.text() 132 OK_button.setEnabled(ok) 133 self.encrypt_cb.setEnabled(ok and bool(self.new_pw.text()) 134 and not force_disable_encrypt_cb) 135 self.new_pw.textChanged.connect(enable_OK) 136 self.conf_pw.textChanged.connect(enable_OK) 137 138 self.vbox = vbox 139 140 def title(self): 141 return self.titles[self.kind] 142 143 def layout(self): 144 return self.vbox 145 146 def pw_changed(self): 147 password = self.new_pw.text() 148 if password: 149 colors = {"Weak":"Red", "Medium":"Blue", "Strong":"Green", 150 "Very Strong":"Green"} 151 strength = check_password_strength(password) 152 label = (_("Password Strength") + ": " + "<font color=" 153 + colors[strength] + ">" + strength + "</font>") 154 else: 155 label = "" 156 self.pw_strength.setText(label) 157 158 def old_password(self): 159 if self.kind == PW_CHANGE: 160 return self.pw.text() or None 161 return None 162 163 def new_password(self): 164 pw = self.new_pw.text() 165 # Empty passphrases are fine and returned empty. 166 if pw == "" and self.kind != PW_PASSPHRASE: 167 pw = None 168 return pw 169 170 def clear_password_fields(self): 171 for field in [self.pw, self.new_pw, self.conf_pw]: 172 field.clear() 173 174 175class PasswordLayoutForHW(object): 176 177 def __init__(self, msg, wallet=None): 178 self.wallet = wallet 179 180 vbox = QVBoxLayout() 181 label = QLabel(msg + "\n") 182 label.setWordWrap(True) 183 184 grid = QGridLayout() 185 grid.setSpacing(8) 186 grid.setColumnMinimumWidth(0, 150) 187 grid.setColumnMinimumWidth(1, 100) 188 grid.setColumnStretch(1,1) 189 190 logo_grid = QGridLayout() 191 logo_grid.setSpacing(8) 192 logo_grid.setColumnMinimumWidth(0, 70) 193 logo_grid.setColumnStretch(1,1) 194 195 logo = QLabel() 196 logo.setAlignment(Qt.AlignCenter) 197 198 logo_grid.addWidget(logo, 0, 0) 199 logo_grid.addWidget(label, 0, 1, 1, 2) 200 vbox.addLayout(logo_grid) 201 202 if wallet and wallet.has_storage_encryption(): 203 lockfile = "lock.png" 204 else: 205 lockfile = "unlock.png" 206 logo.setPixmap(QPixmap(icon_path(lockfile)) 207 .scaledToWidth(36, mode=Qt.SmoothTransformation)) 208 209 vbox.addLayout(grid) 210 211 self.encrypt_cb = QCheckBox(_('Encrypt wallet file')) 212 grid.addWidget(self.encrypt_cb, 1, 0, 1, 2) 213 214 self.vbox = vbox 215 216 def title(self): 217 return _("Toggle Encryption") 218 219 def layout(self): 220 return self.vbox 221 222 223class ChangePasswordDialogBase(WindowModalDialog): 224 225 def __init__(self, parent, wallet): 226 WindowModalDialog.__init__(self, parent) 227 is_encrypted = wallet.has_storage_encryption() 228 OK_button = OkButton(self) 229 230 self.create_password_layout(wallet, is_encrypted, OK_button) 231 232 self.setWindowTitle(self.playout.title()) 233 vbox = QVBoxLayout(self) 234 vbox.addLayout(self.playout.layout()) 235 vbox.addStretch(1) 236 vbox.addLayout(Buttons(CancelButton(self), OK_button)) 237 self.playout.encrypt_cb.setChecked(is_encrypted) 238 239 def create_password_layout(self, wallet, is_encrypted, OK_button): 240 raise NotImplementedError() 241 242 243class ChangePasswordDialogForSW(ChangePasswordDialogBase): 244 245 def __init__(self, parent, wallet): 246 ChangePasswordDialogBase.__init__(self, parent, wallet) 247 if not wallet.has_password(): 248 self.playout.encrypt_cb.setChecked(True) 249 250 def create_password_layout(self, wallet, is_encrypted, OK_button): 251 if not wallet.has_password(): 252 msg = _('Your wallet is not protected.') 253 msg += ' ' + _('Use this dialog to add a password to your wallet.') 254 else: 255 if not is_encrypted: 256 msg = _('Your bitcoins are password protected. However, your wallet file is not encrypted.') 257 else: 258 msg = _('Your wallet is password protected and encrypted.') 259 msg += ' ' + _('Use this dialog to change your password.') 260 self.playout = PasswordLayout(msg=msg, 261 kind=PW_CHANGE, 262 OK_button=OK_button, 263 wallet=wallet, 264 force_disable_encrypt_cb=not wallet.can_have_keystore_encryption()) 265 266 def run(self): 267 try: 268 if not self.exec_(): 269 return False, None, None, None 270 return True, self.playout.old_password(), self.playout.new_password(), self.playout.encrypt_cb.isChecked() 271 finally: 272 self.playout.clear_password_fields() 273 274 275class ChangePasswordDialogForHW(ChangePasswordDialogBase): 276 277 def __init__(self, parent, wallet): 278 ChangePasswordDialogBase.__init__(self, parent, wallet) 279 280 def create_password_layout(self, wallet, is_encrypted, OK_button): 281 if not is_encrypted: 282 msg = _('Your wallet file is NOT encrypted.') 283 else: 284 msg = _('Your wallet file is encrypted.') 285 msg += '\n' + _('Note: If you enable this setting, you will need your hardware device to open your wallet.') 286 msg += '\n' + _('Use this dialog to toggle encryption.') 287 self.playout = PasswordLayoutForHW(msg) 288 289 def run(self): 290 if not self.exec_(): 291 return False, None 292 return True, self.playout.encrypt_cb.isChecked() 293 294 295class PasswordDialog(WindowModalDialog): 296 297 def __init__(self, parent=None, msg=None): 298 msg = msg or _('Please enter your password') 299 WindowModalDialog.__init__(self, parent, _("Enter Password")) 300 self.pw = pw = PasswordLineEdit() 301 vbox = QVBoxLayout() 302 vbox.addWidget(QLabel(msg)) 303 grid = QGridLayout() 304 grid.setSpacing(8) 305 grid.addWidget(QLabel(_('Password')), 1, 0) 306 grid.addWidget(pw, 1, 1) 307 vbox.addLayout(grid) 308 vbox.addLayout(Buttons(CancelButton(self), OkButton(self))) 309 self.setLayout(vbox) 310 run_hook('password_dialog', pw, grid, 1) 311 312 def run(self): 313 try: 314 if not self.exec_(): 315 return 316 return self.pw.text() 317 finally: 318 self.pw.clear() 319