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