From 6e30d3af6473d41de42f1d60cd0363215b6ce321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Tue, 27 Nov 2018 18:15:02 +0100 Subject: [PATCH] Adapt to nbxmpp API changes - Remove support for OAUTH2 - Remove insecure password dialog, its impossible now that a user can connect plain by mistake - Refactor requesting the password - Add possibility to delete passwords --- gajim/common/config.py | 3 - gajim/common/connection.py | 112 +++++++-------------- gajim/common/connection_handlers_events.py | 6 -- gajim/common/passwords.py | 33 ++++-- gajim/gtk/accounts.py | 2 +- gajim/gui_interface.py | 79 +-------------- 6 files changed, 66 insertions(+), 169 deletions(-) diff --git a/gajim/common/config.py b/gajim/common/config.py index 88001fd50..5fa4a8bc3 100644 --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -391,9 +391,6 @@ class Config: 'subscription_request_msg': [opt_str, '', _('Message that is sent to contacts you want to add')], 'enable_message_carbons': [opt_bool, True, _('If enabled and if server supports this feature, Gajim will receive messages sent and received by other resources.')], 'ft_send_local_ips': [opt_bool, True, _('If enabled, Gajim will send your local IPs so your contact can connect to your machine to transfer files.')], - 'oauth2_refresh_token': [opt_str, '', _('Latest token for OAuth 2.0 authentication.')], - 'oauth2_client_id': [opt_str, '0000000044077801', _('client_id for OAuth 2.0 authentication.')], - 'oauth2_redirect_url': [opt_str, 'https%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for OAuth 2.0 authentication.')], 'opened_chat_controls': [opt_str, '', _('Space separated list of JIDs for which chat window will be re-opened on next startup.')], 'recent_groupchats': [opt_str, ''], 'httpupload_verify': [opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')], diff --git a/gajim/common/connection.py b/gajim/common/connection.py index c04374b76..fa9b15d70 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -87,7 +87,7 @@ class CommonConnection: self.connected = 0 self.connection = None # xmpppy ClientCommon instance self.is_zeroconf = False - self.password = '' + self.password = None self.server_resource = self._compute_resource() self.gpg = None self.USE_GPG = False @@ -500,7 +500,6 @@ class Connection(CommonConnection, ConnectionHandlers): self.new_account_info = None self.new_account_form = None self.last_sent = [] - self.password = passwords.get_password(name) self._unregister_account = False self._unregister_account_cb = None @@ -510,7 +509,6 @@ class Connection(CommonConnection, ConnectionHandlers): self.register_supported = False # Do we auto accept insecure connection self.connection_auto_accepted = False - self.pasword_callback = None self.on_connect_success = None self.on_connect_failure = None @@ -1273,25 +1271,45 @@ class Connection(CommonConnection, ConnectionHandlers): return log.info('SSL Cert accepted') - name = None - if not app.config.get_per('accounts', self.name, 'anonymous_auth'): - name = app.config.get_per('accounts', self.name, 'name') + self._auth() + def _get_password(self, mechanism, on_password): + if not mechanism.startswith('SCRAM') and not mechanism == 'PLAIN': + log.error('No password method for %s known', mechanism) + return + + if self.password is not None: + # Passord already known + on_password(self.password) + return + + pass_saved = app.config.get_per('accounts', self.name, 'savepass') + if pass_saved: + # Request password from keyring only if the user chose to save + # his password + self.password = passwords.get_password(self.name) + + if self.password is not None: + on_password(self.password) + else: + app.nec.push_incoming_event(PasswordRequiredEvent( + None, conn=self, on_password=on_password)) + + def _auth(self): self._register_handlers(self.connection, self._current_type) - auth_mechs = app.config.get_per( - 'accounts', self.name, 'authentication_mechanisms').split() - for mech in auth_mechs: - if mech not in nbxmpp.auth_nb.SASL_AUTHENTICATION_MECHANISMS: - log.warning('Unknown authentication mechanisms %s', mech) - if not auth_mechs: - auth_mechs = None + if app.config.get_per('accounts', self.name, 'anonymous_auth'): + name = None + auth_mechs = {'ANONYMOUS'} else: - auth_mechs = set(auth_mechs) - self.connection.auth(user=name, - password=self.password, + name = app.config.get_per('accounts', self.name, 'name') + auth_mechs = app.config.get_per( + 'accounts', self.name, 'authentication_mechanisms').split() + auth_mechs = set(auth_mechs) if auth_mechs else None + + self.connection.auth(name, + get_password=self._get_password, resource=self.server_resource, - sasl=True, auth_mechs=auth_mechs) def _register_handlers(self, con, con_type): @@ -1795,66 +1813,6 @@ class Connection(CommonConnection, ConnectionHandlers): caps=ptype != 'unavailable', idle_time=idle_time) - def get_password(self, callback, type_): - if app.config.get_per('accounts', self.name, 'anonymous_auth') and \ - type_ != 'ANONYMOUS': - app.nec.push_incoming_event( - NonAnonymousServerErrorEvent(None, conn=self)) - self.disconnect(reconnect=False) - return - self.pasword_callback = (callback, type_) - if type_ == 'X-MESSENGER-OAUTH2': - client_id = app.config.get_per('accounts', self.name, - 'oauth2_client_id') - refresh_token = app.config.get_per('accounts', self.name, - 'oauth2_refresh_token') - if refresh_token: - renew_url = ( - 'https://oauth.live.com/token?client_id=' - '%s&redirect_uri=https%%3A%%2F%%2Foauth.live.' - 'com%%2Fdesktop&grant_type=refresh_token&' - 'refresh_token=%s') % (client_id, refresh_token) - result = helpers.download_image(self.name, {'src': renew_url})[0] - if result: - dict_ = json.loads(result) - if 'access_token' in dict_: - self.set_password(dict_['access_token']) - return - script_url = app.config.get_per('accounts', self.name, - 'oauth2_redirect_url') - token_url = ( - 'https://oauth.live.com/authorize?client_id=' - '%s&scope=wl.messenger%%20wl.offline_access&' - 'response_type=code&redirect_uri=%s') % (client_id, script_url) - helpers.launch_browser_mailer('url', token_url) - self.disconnect(reconnect=False) - app.nec.push_incoming_event( - Oauth2CredentialsRequiredEvent(None, conn=self)) - return - if self.password: - self.set_password(self.password) - return - app.nec.push_incoming_event(PasswordRequiredEvent(None, conn=self)) - - def set_password(self, password): - self.password = password - if self.pasword_callback: - callback, type_ = self.pasword_callback - if self._current_type == 'plain' and type_ == 'PLAIN' and \ - app.config.get_per('accounts', self.name, - 'warn_when_insecure_password'): - app.nec.push_incoming_event(InsecurePasswordEvent(None, - conn=self)) - return - callback(password) - self.pasword_callback = None - - def accept_insecure_password(self): - if self.pasword_callback: - callback = self.pasword_callback[0] - callback(self.password) - self.pasword_callback = None - def unregister_account(self, on_remove_success): self._unregister_account = True self._unregister_account_cb = on_remove_success diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index ac2265564..1a45a3186 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -681,9 +681,6 @@ class PEPReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): class PlainConnectionEvent(nec.NetworkIncomingEvent): name = 'plain-connection' -class InsecurePasswordEvent(nec.NetworkIncomingEvent): - name = 'insecure-password' - class InsecureSSLConnectionEvent(nec.NetworkIncomingEvent): name = 'insecure-ssl-connection' @@ -723,9 +720,6 @@ class ZeroconfNameConflictEvent(nec.NetworkIncomingEvent): class PasswordRequiredEvent(nec.NetworkIncomingEvent): name = 'password-required' -class Oauth2CredentialsRequiredEvent(nec.NetworkIncomingEvent): - name = 'oauth2-credentials-required' - class SignedInEvent(nec.NetworkIncomingEvent): name = 'signed-in' diff --git a/gajim/common/passwords.py b/gajim/common/passwords.py index b5805b2ac..dacd8a458 100644 --- a/gajim/common/passwords.py +++ b/gajim/common/passwords.py @@ -43,10 +43,14 @@ class PasswordStorage: def get_password(self, account_name): """Return the password for account_name, or None if not found.""" raise NotImplementedError + def save_password(self, account_name, password): """Save password for account_name. Return a bool indicating success.""" raise NotImplementedError + def delete_password(self, account_name): + """Delete password for account_name. Return a bool indicating success.""" + raise NotImplementedError class SecretPasswordStorage(PasswordStorage): """ Store password using Keyring """ @@ -69,6 +73,10 @@ class SecretPasswordStorage(PasswordStorage): log.debug('getting password') return self.keyring.get_password('gajim', account_name) + def delete_password(self, account_name): + return self.keyring.delete_password('gajim', account_name) + + class PasswordStorageManager(PasswordStorage): """Access all the implemented password storage backends, knowing which ones are available and which we prefer to use. @@ -118,17 +126,27 @@ class PasswordStorageManager(PasswordStorage): return pw def save_password(self, account_name, password): + if account_name in app.connections: + app.connections[account_name].password = password + if not app.config.get_per('accounts', account_name, 'savepass'): + return True + if self.preferred_backend: if self.preferred_backend.save_password(account_name, password): app.config.set_per('accounts', account_name, 'password', self.preferred_backend.identifier) - if account_name in app.connections: - app.connections[account_name].password = password - return True + else: + app.config.set_per('accounts', account_name, 'password', password) + return True - app.config.set_per('accounts', account_name, 'password', password) + def delete_password(self, account_name): if account_name in app.connections: - app.connections[account_name].password = password + app.connections[account_name].password = None + + if not self.preferred_backend: + self.preferred_backend.delete_password(account_name) + else: + app.config.set_per('accounts', account_name, 'password', None) return True def set_preferred_backend(self): @@ -148,7 +166,8 @@ def get_storage(): def get_password(account_name): return get_storage().get_password(account_name) +def delete_password(account_name): + return get_storage().delete_password(account_name) + def save_password(account_name, password): - if account_name in app.connections: - app.connections[account_name].set_password(password) return get_storage().save_password(account_name, password) diff --git a/gajim/gtk/accounts.py b/gajim/gtk/accounts.py index 366d8f37b..aab4bb4f8 100644 --- a/gajim/gtk/accounts.py +++ b/gajim/gtk/accounts.py @@ -810,4 +810,4 @@ class LoginDialog(OptionsDialog): def on_destroy(self, *args): savepass = app.config.get_per('accounts', self.account, 'savepass') if not savepass: - passwords.save_password(self.account, '') + passwords.delete_password(self.account) diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index 9def3183b..8f0f8d40a 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -107,7 +107,6 @@ from gajim.gtk.dialogs import WarningDialog from gajim.gtk.dialogs import InformationDialog from gajim.gtk.dialogs import InputDialog from gajim.gtk.dialogs import YesNoDialog -from gajim.gtk.dialogs import InputTextDialog from gajim.gtk.dialogs import PlainConnectionDialog from gajim.gtk.dialogs import SSLErrorDialog from gajim.gtk.dialogs import ConfirmationDialogDoubleCheck @@ -713,45 +712,19 @@ class Interface: text = _('Enter your password for account %s') % account def on_ok(passphrase, save): - if save: - app.config.set_per('accounts', account, 'savepass', True) - passwords.save_password(account, passphrase) - obj.conn.set_password(passphrase) + app.config.set_per('accounts', account, 'savepass', save) + passwords.save_password(account, passphrase) + obj.on_password(passphrase) del self.pass_dialog[account] def on_cancel(): - self.roster.set_state(account, 'offline') - self.roster.update_status_combobox() + obj.conn.disconnect(reconnect=False, immediately=True) del self.pass_dialog[account] self.pass_dialog[account] = dialogs.PassphraseDialog( _('Password Required'), text, _('Save password'), ok_handler=on_ok, cancel_handler=on_cancel) - def handle_oauth2_credentials(self, obj): - account = obj.conn.name - def on_ok(refresh): - app.config.set_per('accounts', account, 'oauth2_refresh_token', - refresh) - st = app.config.get_per('accounts', account, 'last_status') - msg = helpers.from_one_line(app.config.get_per('accounts', - account, 'last_status_msg')) - app.interface.roster.send_status(account, st, msg) - del self.pass_dialog[account] - - def on_cancel(): - app.config.set_per('accounts', account, 'oauth2_refresh_token', - '') - self.roster.set_state(account, 'offline') - self.roster.update_status_combobox() - del self.pass_dialog[account] - - instruction = _('Please copy / paste the refresh token from the website' - ' that has just been opened.') - self.pass_dialog[account] = InputTextDialog( - _('Oauth2 Credentials'), instruction, is_modal=False, - ok_handler=on_ok, cancel_handler=on_cancel) - def handle_event_roster_info(self, obj): #('ROSTER_INFO', account, (jid, name, sub, ask, groups)) account = obj.conn.name @@ -1454,48 +1427,6 @@ class Interface: checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel, is_modal=False) - def handle_event_insecure_password(self, obj): - # ('INSECURE_PASSWORD', account, ()) - def on_ok(is_checked): - if not is_checked[0]: - on_cancel() - return - del self.instances[obj.conn.name]['online_dialog']\ - ['insecure_password'] - if is_checked[1]: - app.config.set_per('accounts', obj.conn.name, - 'warn_when_insecure_password', False) - if obj.conn.connected == 0: - # We have been disconnecting (too long time since window is - # opened) - # re-connect with auto-accept - obj.conn.connection_auto_accepted = True - show, msg = obj.conn.continue_connect_info[:2] - self.roster.send_status(obj.conn.name, show, msg) - return - obj.conn.accept_insecure_password() - - def on_cancel(): - del self.instances[obj.conn.name]['online_dialog']\ - ['insecure_password'] - obj.conn.disconnect(reconnect=False) - app.nec.push_incoming_event(OurShowEvent(None, conn=obj.conn, - show='offline')) - - pritext = _('Insecure connection') - sectext = _('You are about to send your password unencrypted on an ' - 'insecure connection. Are you sure you want to do that?') - checktext1 = _('Yes, I really want to connect insecurely') - checktext2 = _('_Do not ask me again') - if 'insecure_password' in self.instances[obj.conn.name]\ - ['online_dialog']: - self.instances[obj.conn.name]['online_dialog']\ - ['insecure_password'].destroy() - self.instances[obj.conn.name]['online_dialog']['insecure_password'] = \ - ConfirmationDialogDoubleCheck(pritext, sectext, checktext1, - checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel, - is_modal=False) - def create_core_handlers_list(self): self.handlers = { 'DB_ERROR': [self.handle_event_db_error], @@ -1515,7 +1446,6 @@ class Interface: 'gpg-trust-key': [self.handle_event_gpg_trust_key], 'http-auth-received': [self.handle_event_http_auth], 'information': [self.handle_event_information], - 'insecure-password': [self.handle_event_insecure_password], 'insecure-ssl-connection': \ [self.handle_event_insecure_ssl_connection], 'iq-error-received': [self.handle_event_iq_error], @@ -1530,7 +1460,6 @@ class Interface: 'message-sent': [self.handle_event_msgsent], 'metacontacts-received': [self.handle_event_metacontacts], 'muc-owner-received': [self.handle_event_gc_config], - 'oauth2-credentials-required': [self.handle_oauth2_credentials], 'our-show': [self.handle_event_status], 'password-required': [self.handle_event_password_required], 'plain-connection': [self.handle_event_plain_connection],