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
This commit is contained in:
Philipp Hörist 2018-11-27 18:15:02 +01:00
parent b8863e82be
commit 6e30d3af64
6 changed files with 66 additions and 169 deletions

View file

@ -391,9 +391,6 @@ class Config:
'subscription_request_msg': [opt_str, '', _('Message that is sent to contacts you want to add')], '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.')], '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.')], '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.')], '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, ''], 'recent_groupchats': [opt_str, ''],
'httpupload_verify': [opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')], 'httpupload_verify': [opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')],

View file

@ -87,7 +87,7 @@ class CommonConnection:
self.connected = 0 self.connected = 0
self.connection = None # xmpppy ClientCommon instance self.connection = None # xmpppy ClientCommon instance
self.is_zeroconf = False self.is_zeroconf = False
self.password = '' self.password = None
self.server_resource = self._compute_resource() self.server_resource = self._compute_resource()
self.gpg = None self.gpg = None
self.USE_GPG = False self.USE_GPG = False
@ -500,7 +500,6 @@ class Connection(CommonConnection, ConnectionHandlers):
self.new_account_info = None self.new_account_info = None
self.new_account_form = None self.new_account_form = None
self.last_sent = [] self.last_sent = []
self.password = passwords.get_password(name)
self._unregister_account = False self._unregister_account = False
self._unregister_account_cb = None self._unregister_account_cb = None
@ -510,7 +509,6 @@ class Connection(CommonConnection, ConnectionHandlers):
self.register_supported = False self.register_supported = False
# Do we auto accept insecure connection # Do we auto accept insecure connection
self.connection_auto_accepted = False self.connection_auto_accepted = False
self.pasword_callback = None
self.on_connect_success = None self.on_connect_success = None
self.on_connect_failure = None self.on_connect_failure = None
@ -1273,25 +1271,45 @@ class Connection(CommonConnection, ConnectionHandlers):
return return
log.info('SSL Cert accepted') log.info('SSL Cert accepted')
name = None self._auth()
if not app.config.get_per('accounts', self.name, 'anonymous_auth'):
name = app.config.get_per('accounts', self.name, 'name')
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) self._register_handlers(self.connection, self._current_type)
if app.config.get_per('accounts', self.name, 'anonymous_auth'):
name = None
auth_mechs = {'ANONYMOUS'}
else:
name = app.config.get_per('accounts', self.name, 'name')
auth_mechs = app.config.get_per( auth_mechs = app.config.get_per(
'accounts', self.name, 'authentication_mechanisms').split() 'accounts', self.name, 'authentication_mechanisms').split()
for mech in auth_mechs: auth_mechs = set(auth_mechs) if auth_mechs else None
if mech not in nbxmpp.auth_nb.SASL_AUTHENTICATION_MECHANISMS:
log.warning('Unknown authentication mechanisms %s', mech) self.connection.auth(name,
if not auth_mechs: get_password=self._get_password,
auth_mechs = None
else:
auth_mechs = set(auth_mechs)
self.connection.auth(user=name,
password=self.password,
resource=self.server_resource, resource=self.server_resource,
sasl=True,
auth_mechs=auth_mechs) auth_mechs=auth_mechs)
def _register_handlers(self, con, con_type): def _register_handlers(self, con, con_type):
@ -1795,66 +1813,6 @@ class Connection(CommonConnection, ConnectionHandlers):
caps=ptype != 'unavailable', caps=ptype != 'unavailable',
idle_time=idle_time) 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): def unregister_account(self, on_remove_success):
self._unregister_account = True self._unregister_account = True
self._unregister_account_cb = on_remove_success self._unregister_account_cb = on_remove_success

View file

@ -681,9 +681,6 @@ class PEPReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
class PlainConnectionEvent(nec.NetworkIncomingEvent): class PlainConnectionEvent(nec.NetworkIncomingEvent):
name = 'plain-connection' name = 'plain-connection'
class InsecurePasswordEvent(nec.NetworkIncomingEvent):
name = 'insecure-password'
class InsecureSSLConnectionEvent(nec.NetworkIncomingEvent): class InsecureSSLConnectionEvent(nec.NetworkIncomingEvent):
name = 'insecure-ssl-connection' name = 'insecure-ssl-connection'
@ -723,9 +720,6 @@ class ZeroconfNameConflictEvent(nec.NetworkIncomingEvent):
class PasswordRequiredEvent(nec.NetworkIncomingEvent): class PasswordRequiredEvent(nec.NetworkIncomingEvent):
name = 'password-required' name = 'password-required'
class Oauth2CredentialsRequiredEvent(nec.NetworkIncomingEvent):
name = 'oauth2-credentials-required'
class SignedInEvent(nec.NetworkIncomingEvent): class SignedInEvent(nec.NetworkIncomingEvent):
name = 'signed-in' name = 'signed-in'

View file

@ -43,10 +43,14 @@ class PasswordStorage:
def get_password(self, account_name): def get_password(self, account_name):
"""Return the password for account_name, or None if not found.""" """Return the password for account_name, or None if not found."""
raise NotImplementedError raise NotImplementedError
def save_password(self, account_name, password): def save_password(self, account_name, password):
"""Save password for account_name. Return a bool indicating success.""" """Save password for account_name. Return a bool indicating success."""
raise NotImplementedError raise NotImplementedError
def delete_password(self, account_name):
"""Delete password for account_name. Return a bool indicating success."""
raise NotImplementedError
class SecretPasswordStorage(PasswordStorage): class SecretPasswordStorage(PasswordStorage):
""" Store password using Keyring """ """ Store password using Keyring """
@ -69,6 +73,10 @@ class SecretPasswordStorage(PasswordStorage):
log.debug('getting password') log.debug('getting password')
return self.keyring.get_password('gajim', account_name) 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): class PasswordStorageManager(PasswordStorage):
"""Access all the implemented password storage backends, knowing which ones """Access all the implemented password storage backends, knowing which ones
are available and which we prefer to use. are available and which we prefer to use.
@ -118,17 +126,27 @@ class PasswordStorageManager(PasswordStorage):
return pw return pw
def save_password(self, account_name, password): 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:
if self.preferred_backend.save_password(account_name, password): if self.preferred_backend.save_password(account_name, password):
app.config.set_per('accounts', account_name, 'password', app.config.set_per('accounts', account_name, 'password',
self.preferred_backend.identifier) self.preferred_backend.identifier)
if account_name in app.connections: else:
app.connections[account_name].password = password app.config.set_per('accounts', account_name, 'password', password)
return True return True
app.config.set_per('accounts', account_name, 'password', password) def delete_password(self, account_name):
if account_name in app.connections: 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 return True
def set_preferred_backend(self): def set_preferred_backend(self):
@ -148,7 +166,8 @@ def get_storage():
def get_password(account_name): def get_password(account_name):
return get_storage().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): 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) return get_storage().save_password(account_name, password)

View file

@ -810,4 +810,4 @@ class LoginDialog(OptionsDialog):
def on_destroy(self, *args): def on_destroy(self, *args):
savepass = app.config.get_per('accounts', self.account, 'savepass') savepass = app.config.get_per('accounts', self.account, 'savepass')
if not savepass: if not savepass:
passwords.save_password(self.account, '') passwords.delete_password(self.account)

View file

@ -107,7 +107,6 @@ from gajim.gtk.dialogs import WarningDialog
from gajim.gtk.dialogs import InformationDialog from gajim.gtk.dialogs import InformationDialog
from gajim.gtk.dialogs import InputDialog from gajim.gtk.dialogs import InputDialog
from gajim.gtk.dialogs import YesNoDialog from gajim.gtk.dialogs import YesNoDialog
from gajim.gtk.dialogs import InputTextDialog
from gajim.gtk.dialogs import PlainConnectionDialog from gajim.gtk.dialogs import PlainConnectionDialog
from gajim.gtk.dialogs import SSLErrorDialog from gajim.gtk.dialogs import SSLErrorDialog
from gajim.gtk.dialogs import ConfirmationDialogDoubleCheck from gajim.gtk.dialogs import ConfirmationDialogDoubleCheck
@ -713,45 +712,19 @@ class Interface:
text = _('Enter your password for account %s') % account text = _('Enter your password for account %s') % account
def on_ok(passphrase, save): def on_ok(passphrase, save):
if save: app.config.set_per('accounts', account, 'savepass', save)
app.config.set_per('accounts', account, 'savepass', True)
passwords.save_password(account, passphrase) passwords.save_password(account, passphrase)
obj.conn.set_password(passphrase) obj.on_password(passphrase)
del self.pass_dialog[account] del self.pass_dialog[account]
def on_cancel(): def on_cancel():
self.roster.set_state(account, 'offline') obj.conn.disconnect(reconnect=False, immediately=True)
self.roster.update_status_combobox()
del self.pass_dialog[account] del self.pass_dialog[account]
self.pass_dialog[account] = dialogs.PassphraseDialog( self.pass_dialog[account] = dialogs.PassphraseDialog(
_('Password Required'), text, _('Save password'), ok_handler=on_ok, _('Password Required'), text, _('Save password'), ok_handler=on_ok,
cancel_handler=on_cancel) 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): def handle_event_roster_info(self, obj):
#('ROSTER_INFO', account, (jid, name, sub, ask, groups)) #('ROSTER_INFO', account, (jid, name, sub, ask, groups))
account = obj.conn.name account = obj.conn.name
@ -1454,48 +1427,6 @@ class Interface:
checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel, checktext2, on_response_ok=on_ok, on_response_cancel=on_cancel,
is_modal=False) 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): def create_core_handlers_list(self):
self.handlers = { self.handlers = {
'DB_ERROR': [self.handle_event_db_error], 'DB_ERROR': [self.handle_event_db_error],
@ -1515,7 +1446,6 @@ class Interface:
'gpg-trust-key': [self.handle_event_gpg_trust_key], 'gpg-trust-key': [self.handle_event_gpg_trust_key],
'http-auth-received': [self.handle_event_http_auth], 'http-auth-received': [self.handle_event_http_auth],
'information': [self.handle_event_information], 'information': [self.handle_event_information],
'insecure-password': [self.handle_event_insecure_password],
'insecure-ssl-connection': \ 'insecure-ssl-connection': \
[self.handle_event_insecure_ssl_connection], [self.handle_event_insecure_ssl_connection],
'iq-error-received': [self.handle_event_iq_error], 'iq-error-received': [self.handle_event_iq_error],
@ -1530,7 +1460,6 @@ class Interface:
'message-sent': [self.handle_event_msgsent], 'message-sent': [self.handle_event_msgsent],
'metacontacts-received': [self.handle_event_metacontacts], 'metacontacts-received': [self.handle_event_metacontacts],
'muc-owner-received': [self.handle_event_gc_config], 'muc-owner-received': [self.handle_event_gc_config],
'oauth2-credentials-required': [self.handle_oauth2_credentials],
'our-show': [self.handle_event_status], 'our-show': [self.handle_event_status],
'password-required': [self.handle_event_password_required], 'password-required': [self.handle_event_password_required],
'plain-connection': [self.handle_event_plain_connection], 'plain-connection': [self.handle_event_plain_connection],