rework password interface. Fixes #8347

This commit is contained in:
Yann Leboulanger 2017-08-12 17:00:16 +02:00
parent baa54f5010
commit 89d3c3af14
1 changed files with 104 additions and 82 deletions

View File

@ -33,6 +33,7 @@ __all__ = ['get_password', 'save_password']
log = logging.getLogger('gajim.password') log = logging.getLogger('gajim.password')
keyring = None
if os.name == 'nt': if os.name == 'nt':
try: try:
import keyring import keyring
@ -40,77 +41,52 @@ if os.name == 'nt':
log.debug('python-keyring missing, falling back to plaintext storage') log.debug('python-keyring missing, falling back to plaintext storage')
Secret = None
class PasswordStorage(object): class PasswordStorage(object):
"""Interface for password stores"""
def get_password(self, account_name): def get_password(self, account_name):
"""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."""
raise NotImplementedError raise NotImplementedError
class SimplePasswordStorage(PasswordStorage): class LibSecretPasswordStorage(PasswordStorage):
def get_password(self, account_name): """Store password using libsecret"""
passwd = gajim.config.get_per('accounts', account_name, 'password') identifier = 'libsecret:'
if passwd and (passwd.startswith('libsecret:') or passwd.startswith('winvault:')):
# this is not a real password, its stored through libsecret.
return None
else:
return passwd
def save_password(self, account_name, password):
gajim.config.set_per('accounts', account_name, 'password', password)
if account_name in gajim.connections:
gajim.connections[account_name].password = password
class SecretPasswordStorage(PasswordStorage):
def __init__(self): def __init__(self):
self.GAJIM_SCHEMA = Secret.Schema.new("org.gnome.keyring.NetworkPassword", gi.require_version('Secret', '1')
Secret.SchemaFlags.NONE, gir = __import__('gi.repository', globals(), locals(), ['Secret'], 0)
self.Secret = gir.Secret
self.GAJIM_SCHEMA = self.Secret.Schema.new(
"org.gnome.keyring.NetworkPassword",
self.Secret.SchemaFlags.NONE,
{ {
'user': Secret.SchemaAttributeType.STRING, 'user': self.Secret.SchemaAttributeType.STRING,
'server': Secret.SchemaAttributeType.STRING, 'server': self.Secret.SchemaAttributeType.STRING,
'protocol': Secret.SchemaAttributeType.STRING, 'protocol': self.Secret.SchemaAttributeType.STRING,
} }
) )
def get_password(self, account_name): def get_password(self, account_name):
conf = gajim.config.get_per('accounts', account_name, 'password')
if conf is None:
return None
if not conf.startswith('libsecret:'):
password = conf
## migrate the password over to keyring
try:
self.save_password(account_name, password, update=False)
except Exception:
## no keyring daemon: in the future, stop using it
set_storage(SimplePasswordStorage())
return password
server = gajim.config.get_per('accounts', account_name, 'hostname') server = gajim.config.get_per('accounts', account_name, 'hostname')
user = gajim.config.get_per('accounts', account_name, 'name') user = gajim.config.get_per('accounts', account_name, 'name')
password = Secret.password_lookup_sync(self.GAJIM_SCHEMA, {'user': user, password = self.Secret.password_lookup_sync(self.GAJIM_SCHEMA,
'server': server, 'protocol': 'xmpp'}, None) {'user': user, 'server': server, 'protocol': 'xmpp'}, None)
return password return password
def save_password(self, account_name, password, update=True): def save_password(self, account_name, password, update=True):
server = gajim.config.get_per('accounts', account_name, 'hostname') server = gajim.config.get_per('accounts', account_name, 'hostname')
user = gajim.config.get_per('accounts', account_name, 'name') user = gajim.config.get_per('accounts', account_name, 'name')
display_name = _('XMPP account %s@%s') % (user, server) display_name = _('XMPP account %s@%s') % (user, server)
if password is None:
password = str()
attributes = {'user': user, 'server': server, 'protocol': 'xmpp'} attributes = {'user': user, 'server': server, 'protocol': 'xmpp'}
Secret.password_store_sync(self.GAJIM_SCHEMA, attributes, return self.Secret.password_store_sync(self.GAJIM_SCHEMA, attributes,
Secret.COLLECTION_DEFAULT, display_name, password, None) self.Secret.COLLECTION_DEFAULT, display_name, password or '', None)
gajim.config.set_per('accounts', account_name, 'password',
'libsecret:')
if account_name in gajim.connections:
gajim.connections[account_name].password = password
class SecretWindowsPasswordStorage(PasswordStorage): class SecretWindowsPasswordStorage(PasswordStorage):
""" Windows Keyring """ """ Windows Keyring """
identifier = 'winvault:'
def __init__(self): def __init__(self):
self.win_keyring = keyring.get_keyring() self.win_keyring = keyring.get_keyring()
@ -118,53 +94,99 @@ class SecretWindowsPasswordStorage(PasswordStorage):
def save_password(self, account_name, password): def save_password(self, account_name, password):
try: try:
self.win_keyring.set_password('gajim', account_name, password) self.win_keyring.set_password('gajim', account_name, password)
gajim.config.set_per( return True
'accounts', account_name, 'password', 'winvault:')
except: except:
log.exception('error:') log.exception('error:')
set_storage(SimplePasswordStorage()) return False
storage.save_password(account_name, password)
def get_password(self, account_name): def get_password(self, account_name):
log.debug('getting password') log.debug('getting password')
conf = gajim.config.get_per('accounts', account_name, 'password')
if conf is None:
return None
if not conf.startswith('winvault:'):
password = conf
# migrate the password over to keyring
self.save_password(account_name, password)
return password
return self.win_keyring.get_password('gajim', account_name) return self.win_keyring.get_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.
Also implements storing directly in gajim config (former
SimplePasswordStorage class)."""
def __init__(self):
self.preferred_backend = None
self.libsecret = None
self.winsecret = None
self.connect_backends()
self.set_preferred_backend()
def connect_backends(self):
"""Initialize backend connections, determining which ones are available.
"""
# TODO: handle disappearing backends
storage = None
def get_storage():
global storage
if storage is None: # None is only in first time get_storage is called
global Secret
if gajim.config.get('use_keyring'): if gajim.config.get('use_keyring'):
try: if os.name == 'nt' and keyring:
gi.require_version('Secret', '1') self.winsecret = SecretWindowsPasswordStorage()
gir = __import__('gi.repository', globals(), locals(), else:
['Secret'], 0) try:
Secret = gir.Secret self.libsecret = LibSecretPasswordStorage()
except (ValueError, AttributeError): except (ValueError, AttributeError) as e:
pass log.debug("Could not connect to libsecret: %s" % e)
try:
if os.name != 'nt':
storage = SecretPasswordStorage()
else:
storage = SecretWindowsPasswordStorage()
except Exception:
storage = SimplePasswordStorage()
else:
storage = SimplePasswordStorage()
return storage
def set_storage(storage_): def get_password(self, account_name):
global storage pw = gajim.config.get_per('accounts', account_name, 'password')
storage = storage_ if not pw:
return pw
if pw.startswith(LibSecretPasswordStorage.identifier) and \
self.libsecret:
backend = self.libsecret
elif pw.startswith(SecretWindowsPasswordStorage.identifier) and \
self.winsecret:
backend = self.winsecret
else:
backend = None
if backend:
pw = backend.get_password(account_name)
if backend != self.preferred_backend:
# migrate password to preferred_backend
self.preferred_backend.save_password(account_name, pw)
# TODO: remove from old backend
return pw
def save_password(self, account_name, password):
if self.preferred_backend:
if self.preferred_backend.save_password(account_name, password):
gajim.config.set_per('accounts', account_name, 'password',
self.preferred_backend.identifier)
if account_name in gajim.connections:
gajim.connections[account_name].password = password
return True
gajim.config.set_per('accounts', account_name, 'password', password)
if account_name in gajim.connections:
gajim.connections[account_name].password = password
return True
def set_preferred_backend(self):
if self.libsecret:
self.preferred_backend = self.libsecret
elif self.winsecret:
self.preferred_backend = self.winsecret
else:
self.preferred_backend = None
def has_keyring(self):
"""Is there a real password storage backend? Else, passwords are stored
plain in gajim config"""
return bool(self.preferred_backend)
passwordStorageManager = None
def get_storage():
global passwordStorageManager
if not passwordStorageManager:
passwordStorageManager = PasswordStorageManager()
return passwordStorageManager
def get_password(account_name): def get_password(account_name):
return get_storage().get_password(account_name) return get_storage().get_password(account_name)