diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | changes/feature-preferences-window-password-change | 1 | ||||
| -rw-r--r-- | src/leap/bitmask/crypto/srpauth.py | 82 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/mainwindow.py | 19 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/preferenceswindow.py | 196 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/ui/mainwindow.ui | 11 | ||||
| -rw-r--r-- | src/leap/bitmask/gui/ui/preferences.ui | 170 | 
7 files changed, 479 insertions, 2 deletions
| @@ -20,7 +20,7 @@ TRANSLAT_DIR = data/translations  PROJFILE = data/bitmask.pro  #UI files to compile -UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui statuspanel.ui +UI_FILES = loggerwindow.ui mainwindow.ui wizard.ui login.ui statuspanel.ui preferences.ui  #Qt resource files to compile  RESOURCES = locale.qrc loggerwindow.qrc mainwindow.qrc icons.qrc diff --git a/changes/feature-preferences-window-password-change b/changes/feature-preferences-window-password-change new file mode 100644 index 00000000..df255500 --- /dev/null +++ b/changes/feature-preferences-window-password-change @@ -0,0 +1 @@ +  o Add a preference panel that lets you change your password. Closes #3500 #2798 #3533. diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index 7b91205f..41ce130a 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -134,6 +134,8 @@ class SRPAuth(QtCore.QObject):          A_KEY = "A"          CLIENT_AUTH_KEY = "client_auth"          SESSION_ID_KEY = "_session_id" +        USER_VERIFIER_KEY = 'user[password_verifier]' +        USER_SALT_KEY = 'user[password_salt]'          def __init__(self, provider_config):              """ @@ -169,6 +171,10 @@ class SRPAuth(QtCore.QObject):              self._srp_user = None              self._srp_a = None +            # User credentials stored for password changing checks +            self._username = None +            self._password = None +          def _safe_unhexlify(self, val):              """              Rounds the val to a multiple of 2 and returns the @@ -438,6 +444,51 @@ class SRPAuth(QtCore.QObject):          def _threader(self, cb, res, *args, **kwargs):              return threads.deferToThread(cb, res, *args, **kwargs) +        def change_password(self, current_password, new_password): +            """ +            Changes the password for the currently logged user if the current +            password match. +            It requires to be authenticated. + +            Might raise: +                SRPAuthBadPassword +                requests.exceptions.HTTPError + +            :param current_password: the current password for the logged user. +            :type current_password: str +            :param new_password: the new password for the user +            :type new_password: str +            """ +            leap_assert(self.get_uid() is not None) + +            if current_password != self._password: +                raise SRPAuthBadPassword + +            url = "%s/%s/users/%s.json" % ( +                self._provider_config.get_api_uri(), +                self._provider_config.get_api_version(), +                self.get_uid()) + +            salt, verifier = self._srp.create_salted_verification_key( +                self._username, new_password, self._hashfun, self._ng) + +            cookies = {self.SESSION_ID_KEY: self.get_session_id()} +            user_data = { +                self.USER_VERIFIER_KEY: binascii.hexlify(verifier), +                self.USER_SALT_KEY: binascii.hexlify(salt) +            } + +            change_password = self._session.put( +                url, data=user_data, +                verify=self._provider_config.get_ca_cert_path(), +                cookies=cookies, +                timeout=REQUEST_TIMEOUT) + +            # In case of non 2xx it raises HTTPError +            change_password.raise_for_status() + +            self._password = new_password +          def authenticate(self, username, password):              """              Executes the whole authentication process for a user @@ -454,6 +505,10 @@ class SRPAuth(QtCore.QObject):              """              leap_assert(self.get_session_id() is None, "Already logged in") +            # User credentials stored for password changing checks +            self._username = username.lower() +            self._password = password +              d = threads.deferToThread(self._authentication_preprocessing,                                        username=username,                                        password=password) @@ -565,6 +620,33 @@ class SRPAuth(QtCore.QObject):          d.addErrback(self._errback)          return d +    def change_password(self, current_password, new_password): +        """ +        Changes the user's password. + +        :param current_password: the current password of the user. +        :type current_password: str +        :param new_password: the new password for the user. +        :type new_password: str + +        :returns: a defer to interact with. +        :rtype: twisted.internet.defer.Deferred +        """ +        d = threads.deferToThread( +            self.__instance.change_password, current_password, new_password) +        return d + +    def get_username(self): +        """ +        Returns the username of the currently authenticated user or None if +        no user is logged. + +        :rtype: str or None +        """ +        if self.get_uid() is None: +            return None +        return self.__instance._username +      def _gui_notify(self, _):          """          Callback that notifies the UI with the proper signal. diff --git a/src/leap/bitmask/gui/mainwindow.py b/src/leap/bitmask/gui/mainwindow.py index 17275983..c832887a 100644 --- a/src/leap/bitmask/gui/mainwindow.py +++ b/src/leap/bitmask/gui/mainwindow.py @@ -33,6 +33,7 @@ from leap.bitmask.config.leapsettings import LeapSettings  from leap.bitmask.config.providerconfig import ProviderConfig  from leap.bitmask.crypto.srpauth import SRPAuth  from leap.bitmask.gui.loggerwindow import LoggerWindow +from leap.bitmask.gui.preferenceswindow import PreferencesWindow  from leap.bitmask.gui.wizard import Wizard  from leap.bitmask.gui.login import LoginWidget  from leap.bitmask.gui.statuspanel import StatusPanelWidget @@ -162,6 +163,7 @@ class MainWindow(QtGui.QMainWindow):              self._launch_wizard)          self.ui.btnShowLog.clicked.connect(self._show_logger_window) +        self.ui.btnPreferences.clicked.connect(self._show_preferences)          self._status_panel = StatusPanelWidget(              self.ui.stackedWidget.widget(self.EIP_STATUS_INDEX)) @@ -286,12 +288,17 @@ class MainWindow(QtGui.QMainWindow):          ################################# end Qt Signals connection ######## +        # Enable the password change when soledad is ready +        self.soledad_ready.connect( +            partial(self.ui.btnPreferences.setEnabled, True)) +          init_platform()          self._wizard = None          self._wizard_firstrun = False          self._logger_window = None +        self._preferences_window = None          self._bypass_checks = bypass_checks @@ -403,6 +410,17 @@ class MainWindow(QtGui.QMainWindow):          self._logger_window.finished.connect(self._uncheck_logger_button) +    def _show_preferences(self): +        """ +        SLOT +        TRIGGERS: +          self.ui.action_show_preferences.triggered +          self.ui.btnPreferences.clicked + +        Displays the preferences window. +        """ +        PreferencesWindow(self, self._srp_auth, self._soledad).show() +      def _uncheck_logger_button(self):          """          SLOT @@ -1399,6 +1417,7 @@ class MainWindow(QtGui.QMainWindow):          self._login_widget.set_password("")          self._login_widget.set_enabled(True)          self._login_widget.set_status("") +        self.ui.btnPreferences.setEnabled(False)      def _intermediate_stage(self, data):          """ diff --git a/src/leap/bitmask/gui/preferenceswindow.py b/src/leap/bitmask/gui/preferenceswindow.py new file mode 100644 index 00000000..67448768 --- /dev/null +++ b/src/leap/bitmask/gui/preferenceswindow.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# preferenceswindow.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +""" +Preferences window +""" +import logging + +from functools import partial +from PySide import QtGui + +from leap.bitmask.gui.ui_preferences import Ui_Preferences +from leap.soledad.client import NoStorageSecret +from leap.bitmask.crypto.srpauth import SRPAuthBadPassword + +logger = logging.getLogger(__name__) + + +class PreferencesWindow(QtGui.QDialog): +    """ +    Window that displays the preferences. +    """ + +    WEAK_PASSWORDS = ("123456", "qweasd", "qwerty", "password") + +    def __init__(self, parent, srp_auth, soledad): +        """ +        :param parent: parent object of the PreferencesWindow. +        :parent type: QWidget +        :param srp_auth: SRPAuth object configured in the main app. +        :type srp_auth: SRPAuth +        :param soledad: Soledad object configured in the main app. +        :type soledad: Soledad +        """ +        QtGui.QDialog.__init__(self, parent) + +        self._srp_auth = srp_auth +        self._soledad = soledad + +        # Load UI +        self.ui = Ui_Preferences() +        self.ui.setupUi(self) +        self.ui.lblPasswordChangeStatus.setVisible(False) + +        # Connections +        self.ui.pbChangePassword.clicked.connect(self._change_password) + +    def _basic_password_checks(self, username, password, password2): +        """ +        Performs basic password checks to avoid really easy passwords. + +        :param username: username provided at the registrarion form +        :type username: str +        :param password: password from the registration form +        :type password: str +        :param password2: second password from the registration form +        :type password: str + +        :returns: True and empty message if all the checks pass, +                  False and an error message otherwise +        :rtype: tuple(bool, str) +        """ +        message = None + +        if message is None and password != password2: +            message = self.tr("Passwords don't match") + +        if message is None and len(password) < 6: +            message = self.tr("Password too short") + +        if message is None and password in self.WEAK_PASSWORDS: +            message = self.tr("Password too easy") + +        if message is None and username == password: +            message = self.tr("Password equal to username") + +        return message is None, message + +    def _set_password_change_status(self, status, error=False, success=False): +        """ +        Sets the status label for the password change. + +        :param status: status message to display, can be HTML +        :type status: str +        """ +        if error: +            status = "<font color='red'><b>%s</b></font>" % (status,) +        elif success: +            status = "<font color='green'><b>%s</b></font>" % (status,) + +        self.ui.lblPasswordChangeStatus.setVisible(True) +        self.ui.lblPasswordChangeStatus.setText(status) + +    def _set_changing_password(self, disable): +        """ +        Enables or disables the widgets in the password change group box. + +        :param disable: True if the widgets should be disabled and +                        it displays the status label that shows that is +                        changing the password. +                        False if they should be enabled. +        :type disable: bool +        """ +        if disable: +            self._set_password_change_disable(self.tr("Changing password...")) + +        self.ui.leCurrentPassword.setEnabled(not disable) +        self.ui.leNewPassword.setEnabled(not disable) +        self.ui.leNewPassword2.setEnabled(not disable) +        self.ui.pbChangePassword.setEnabled(not disable) + +    def _change_password(self): +        """ +        Changes the user's password if the inputboxes are correctly filled. +        """ +        username = self._srp_auth.get_username() +        current_password = self.ui.leCurrentPassword.text() +        new_password = self.ui.leNewPassword.text() +        new_password2 = self.ui.leNewPassword2.text() + +        ok, msg = self._basic_password_checks( +            username, new_password, new_password2) + +        if not ok: +            self._set_changing_password(False) +            self._set_password_change_status(msg, error=True) +            self.ui.leNewPassword.setFocus() +            return + +        self._set_changing_password(True) +        d = self._srp_auth.change_password(current_password, new_password) +        d.addCallback(partial(self._change_password_success, new_password)) +        d.addErrback(self._change_password_problem) + +    def _change_password_success(self, new_password, _): +        """ +        Callback used to display a successfully performed action. + +        :param new_password: the new password for the user. +        :type new_password: str. +        :param _: the returned data from self._srp_auth.change_password +                  Ignored +        """ +        logger.debug("SRP password changed successfully.") +        try: +            self._soledad.change_passphrase(str(new_password)) +            logger.debug("Soledad password changed successfully.") +        except NoStorageSecret: +            logger.debug( +                "No storage secret for password change in Soledad.") + +        self._set_password_change_status( +            self.tr("Password changed successfully."), success=True) +        self._clear_inputs() +        self._set_changing_password(False) + +    def _change_password_problem(self, failure): +        """ +        Errback called if there was a problem with the deferred. +        Also is used to display an error message. + +        :param failure: the cause of the method failed. +        :type failure: twisted.python.Failure +        """ +        logger.error("Error changing password: %s", (failure, )) +        problem = self.tr("There was a problem changing the password.") + +        if failure.check(SRPAuthBadPassword): +            problem = self.tr("You did not enter a correct current password.") + +        self._set_password_change_status(problem, error=True) + +        self._set_changing_password(False) +        failure.trap(Exception) + +    def _clear_inputs(self): +        """ +        Clear the contents of the inputs. +        """ +        self.ui.leCurrentPassword.setText("") +        self.ui.leNewPassword.setText("") +        self.ui.leNewPassword2.setText("") diff --git a/src/leap/bitmask/gui/ui/mainwindow.ui b/src/leap/bitmask/gui/ui/mainwindow.ui index 9c4e6ff0..834a562e 100644 --- a/src/leap/bitmask/gui/ui/mainwindow.ui +++ b/src/leap/bitmask/gui/ui/mainwindow.ui @@ -215,6 +215,16 @@      <item row="18" column="2">       <layout class="QHBoxLayout" name="horizontalLayout">        <item> +       <widget class="QPushButton" name="btnPreferences"> +        <property name="enabled"> +         <bool>false</bool> +        </property> +        <property name="text"> +         <string>Preferences</string> +        </property> +       </widget> +      </item> +      <item>         <spacer name="horizontalSpacer_10">          <property name="orientation">           <enum>Qt::Horizontal</enum> @@ -310,7 +320,6 @@   <resources>    <include location="../../../../../data/resources/mainwindow.qrc"/>    <include location="../../../../../data/resources/locale.qrc"/> -  <include location="../../../../data/resources/mainwindow.qrc"/>   </resources>   <connections/>  </ui> diff --git a/src/leap/bitmask/gui/ui/preferences.ui b/src/leap/bitmask/gui/ui/preferences.ui new file mode 100644 index 00000000..8c63ccad --- /dev/null +++ b/src/leap/bitmask/gui/ui/preferences.ui @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>Preferences</class> + <widget class="QDialog" name="Preferences"> +  <property name="geometry"> +   <rect> +    <x>0</x> +    <y>0</y> +    <width>451</width> +    <height>267</height> +   </rect> +  </property> +  <property name="windowTitle"> +   <string>Preferences</string> +  </property> +  <property name="windowIcon"> +   <iconset resource="../../../../../data/resources/mainwindow.qrc"> +    <normaloff>:/images/mask-icon.png</normaloff>:/images/mask-icon.png</iconset> +  </property> +  <layout class="QVBoxLayout" name="verticalLayout"> +   <item> +    <widget class="QGroupBox" name="gbPasswordChange"> +     <property name="title"> +      <string>Password Change</string> +     </property> +     <layout class="QFormLayout" name="formLayout"> +      <property name="fieldGrowthPolicy"> +       <enum>QFormLayout::ExpandingFieldsGrow</enum> +      </property> +      <item row="0" column="0"> +       <widget class="QLabel" name="lblCurrentPassword"> +        <property name="text"> +         <string>&Current password:</string> +        </property> +        <property name="buddy"> +         <cstring>leCurrentPassword</cstring> +        </property> +       </widget> +      </item> +      <item row="0" column="1"> +       <widget class="QLineEdit" name="leCurrentPassword"> +        <property name="echoMode"> +         <enum>QLineEdit::Password</enum> +        </property> +       </widget> +      </item> +      <item row="1" column="0"> +       <widget class="QLabel" name="lblNewPassword"> +        <property name="text"> +         <string>&New password:</string> +        </property> +        <property name="buddy"> +         <cstring>leNewPassword</cstring> +        </property> +       </widget> +      </item> +      <item row="1" column="1"> +       <widget class="QLineEdit" name="leNewPassword"> +        <property name="echoMode"> +         <enum>QLineEdit::Password</enum> +        </property> +       </widget> +      </item> +      <item row="2" column="0"> +       <widget class="QLabel" name="lblNewPassword2"> +        <property name="text"> +         <string>&Re-enter new password:</string> +        </property> +        <property name="buddy"> +         <cstring>leNewPassword2</cstring> +        </property> +       </widget> +      </item> +      <item row="2" column="1"> +       <widget class="QLineEdit" name="leNewPassword2"> +        <property name="echoMode"> +         <enum>QLineEdit::Password</enum> +        </property> +       </widget> +      </item> +      <item row="4" column="1"> +       <widget class="QPushButton" name="pbChangePassword"> +        <property name="text"> +         <string>Change</string> +        </property> +       </widget> +      </item> +      <item row="3" column="0" colspan="2"> +       <widget class="QLabel" name="lblPasswordChangeStatus"> +        <property name="text"> +         <string><Password change status></string> +        </property> +        <property name="alignment"> +         <set>Qt::AlignCenter</set> +        </property> +       </widget> +      </item> +     </layout> +    </widget> +   </item> +   <item> +    <widget class="QGroupBox" name="gbGatewaySelector"> +     <property name="enabled"> +      <bool>false</bool> +     </property> +     <property name="title"> +      <string>Select gateway for provider</string> +     </property> +     <property name="checkable"> +      <bool>false</bool> +     </property> +     <layout class="QGridLayout" name="gridLayout"> +      <item row="0" column="0"> +       <widget class="QLabel" name="lblSelectProvider"> +        <property name="text"> +         <string>&Select provider:</string> +        </property> +        <property name="buddy"> +         <cstring>cbProviders</cstring> +        </property> +       </widget> +      </item> +      <item row="0" column="1"> +       <widget class="QComboBox" name="cbProviders"> +        <item> +         <property name="text"> +          <string><Select provider></string> +         </property> +        </item> +       </widget> +      </item> +      <item row="1" column="0"> +       <widget class="QLabel" name="label"> +        <property name="text"> +         <string>Select gateway:</string> +        </property> +       </widget> +      </item> +      <item row="1" column="1"> +       <widget class="QComboBox" name="comboBox"> +        <item> +         <property name="text"> +          <string>Automatic</string> +         </property> +        </item> +       </widget> +      </item> +     </layout> +    </widget> +   </item> +   <item> +    <spacer name="verticalSpacer"> +     <property name="orientation"> +      <enum>Qt::Vertical</enum> +     </property> +     <property name="sizeHint" stdset="0"> +      <size> +       <width>20</width> +       <height>40</height> +      </size> +     </property> +    </spacer> +   </item> +  </layout> + </widget> + <resources> +  <include location="../../../../../data/resources/mainwindow.qrc"/> + </resources> + <connections/> +</ui> | 
