Yann Leboulanger pushed to branch master at gajim / gajim

Commits:

1 changed file:

Changes:

  • gajim/common/passwords.py
    ... ... @@ -33,6 +33,7 @@ __all__ = ['get_password', 'save_password']
    33 33
     
    
    34 34
     log = logging.getLogger('gajim.password')
    
    35 35
     
    
    36
    +keyring = None
    
    36 37
     if os.name == 'nt':
    
    37 38
         try:
    
    38 39
             import keyring
    
    ... ... @@ -40,77 +41,52 @@ if os.name == 'nt':
    40 41
             log.debug('python-keyring missing, falling back to plaintext storage')
    
    41 42
     
    
    42 43
     
    
    43
    -Secret = None
    
    44
    -
    
    45 44
     class PasswordStorage(object):
    
    45
    +    """Interface for password stores"""
    
    46 46
         def get_password(self, account_name):
    
    47
    +        """Return the password for account_name, or None if not found."""
    
    47 48
             raise NotImplementedError
    
    48 49
         def save_password(self, account_name, password):
    
    50
    +        """Save password for account_name. Return a bool indicating success."""
    
    49 51
             raise NotImplementedError
    
    50 52
     
    
    51 53
     
    
    52
    -class SimplePasswordStorage(PasswordStorage):
    
    53
    -    def get_password(self, account_name):
    
    54
    -        passwd = gajim.config.get_per('accounts', account_name, 'password')
    
    55
    -        if passwd and (passwd.startswith('libsecret:') or passwd.startswith('winvault:')):
    
    56
    -            # this is not a real password, it’s stored through libsecret.
    
    57
    -            return None
    
    58
    -        else:
    
    59
    -            return passwd
    
    60
    -
    
    61
    -    def save_password(self, account_name, password):
    
    62
    -        gajim.config.set_per('accounts', account_name, 'password', password)
    
    63
    -        if account_name in gajim.connections:
    
    64
    -            gajim.connections[account_name].password = password
    
    65
    -
    
    66
    -
    
    67
    -class SecretPasswordStorage(PasswordStorage):
    
    54
    +class LibSecretPasswordStorage(PasswordStorage):
    
    55
    +    """Store password using libsecret"""
    
    56
    +    identifier = 'libsecret:'
    
    68 57
         def __init__(self):
    
    69
    -        self.GAJIM_SCHEMA = Secret.Schema.new("org.gnome.keyring.NetworkPassword",
    
    70
    -            Secret.SchemaFlags.NONE,
    
    58
    +        gi.require_version('Secret', '1')
    
    59
    +        gir = __import__('gi.repository', globals(), locals(), ['Secret'], 0)
    
    60
    +        self.Secret = gir.Secret
    
    61
    +        self.GAJIM_SCHEMA = self.Secret.Schema.new(
    
    62
    +            "org.gnome.keyring.NetworkPassword",
    
    63
    +            self.Secret.SchemaFlags.NONE,
    
    71 64
                 {
    
    72
    -                'user': Secret.SchemaAttributeType.STRING,
    
    73
    -                'server':  Secret.SchemaAttributeType.STRING,
    
    74
    -                'protocol': Secret.SchemaAttributeType.STRING,
    
    65
    +                'user': self.Secret.SchemaAttributeType.STRING,
    
    66
    +                'server':  self.Secret.SchemaAttributeType.STRING,
    
    67
    +                'protocol': self.Secret.SchemaAttributeType.STRING,
    
    75 68
                 }
    
    76 69
             )
    
    77 70
     
    
    78 71
         def get_password(self, account_name):
    
    79
    -        conf = gajim.config.get_per('accounts', account_name, 'password')
    
    80
    -        if conf is None:
    
    81
    -            return None
    
    82
    -        if not conf.startswith('libsecret:'):
    
    83
    -            password = conf
    
    84
    -            ## migrate the password over to keyring
    
    85
    -            try:
    
    86
    -                self.save_password(account_name, password, update=False)
    
    87
    -            except Exception:
    
    88
    -                ## no keyring daemon: in the future, stop using it
    
    89
    -                set_storage(SimplePasswordStorage())
    
    90
    -            return password
    
    91 72
             server = gajim.config.get_per('accounts', account_name, 'hostname')
    
    92 73
             user = gajim.config.get_per('accounts', account_name, 'name')
    
    93
    -        password = Secret.password_lookup_sync(self.GAJIM_SCHEMA, {'user': user,
    
    94
    -            'server': server, 'protocol': 'xmpp'}, None)
    
    74
    +        password = self.Secret.password_lookup_sync(self.GAJIM_SCHEMA,
    
    75
    +            {'user': user, 'server': server, 'protocol': 'xmpp'}, None)
    
    95 76
             return password
    
    96 77
     
    
    97 78
         def save_password(self, account_name, password, update=True):
    
    98 79
             server = gajim.config.get_per('accounts', account_name, 'hostname')
    
    99 80
             user = gajim.config.get_per('accounts', account_name, 'name')
    
    100 81
             display_name = _('XMPP account %s@%s') % (user, server)
    
    101
    -        if password is None:
    
    102
    -            password = str()
    
    103 82
             attributes = {'user': user, 'server': server, 'protocol': 'xmpp'}
    
    104
    -        Secret.password_store_sync(self.GAJIM_SCHEMA, attributes,
    
    105
    -            Secret.COLLECTION_DEFAULT, display_name, password, None)
    
    106
    -        gajim.config.set_per('accounts', account_name, 'password',
    
    107
    -            'libsecret:')
    
    108
    -        if account_name in gajim.connections:
    
    109
    -            gajim.connections[account_name].password = password
    
    83
    +        return self.Secret.password_store_sync(self.GAJIM_SCHEMA, attributes,
    
    84
    +            self.Secret.COLLECTION_DEFAULT, display_name, password or '', None)
    
    110 85
     
    
    111 86
     
    
    112 87
     class SecretWindowsPasswordStorage(PasswordStorage):
    
    113 88
         """ Windows Keyring """
    
    89
    +    identifier = 'winvault:'
    
    114 90
     
    
    115 91
         def __init__(self):
    
    116 92
             self.win_keyring = keyring.get_keyring()
    
    ... ... @@ -118,53 +94,99 @@ class SecretWindowsPasswordStorage(PasswordStorage):
    118 94
         def save_password(self, account_name, password):
    
    119 95
             try:
    
    120 96
                 self.win_keyring.set_password('gajim', account_name, password)
    
    121
    -            gajim.config.set_per(
    
    122
    -                'accounts', account_name, 'password', 'winvault:')
    
    97
    +            return True
    
    123 98
             except:
    
    124 99
                 log.exception('error:')
    
    125
    -            set_storage(SimplePasswordStorage())
    
    126
    -            storage.save_password(account_name, password)
    
    100
    +            return False
    
    127 101
     
    
    128 102
         def get_password(self, account_name):
    
    129 103
             log.debug('getting password')
    
    130
    -        conf = gajim.config.get_per('accounts', account_name, 'password')
    
    131
    -        if conf is None:
    
    132
    -            return None
    
    133
    -        if not conf.startswith('winvault:'):
    
    134
    -            password = conf
    
    135
    -            # migrate the password over to keyring
    
    136
    -            self.save_password(account_name, password)
    
    137
    -            return password
    
    138 104
             return self.win_keyring.get_password('gajim', account_name)
    
    139 105
     
    
    106
    +class PasswordStorageManager(PasswordStorage):
    
    107
    +    """Access all the implemented password storage backends, knowing which ones
    
    108
    +    are available and which we prefer to use.
    
    109
    +    Also implements storing directly in gajim config (former
    
    110
    +    SimplePasswordStorage class)."""
    
    111
    +
    
    112
    +    def __init__(self):
    
    113
    +        self.preferred_backend = None
    
    114
    +
    
    115
    +        self.libsecret = None
    
    116
    +        self.winsecret = None
    
    117
    +
    
    118
    +        self.connect_backends()
    
    119
    +        self.set_preferred_backend()
    
    120
    +
    
    121
    +    def connect_backends(self):
    
    122
    +        """Initialize backend connections, determining which ones are available.
    
    123
    +        """
    
    124
    +        # TODO: handle disappearing backends
    
    140 125
     
    
    141
    -storage = None
    
    142
    -def get_storage():
    
    143
    -    global storage
    
    144
    -    if storage is None: # None is only in first time get_storage is called
    
    145
    -        global Secret
    
    146 126
             if gajim.config.get('use_keyring'):
    
    147
    -            try:
    
    148
    -                gi.require_version('Secret', '1')
    
    149
    -                gir = __import__('gi.repository', globals(), locals(),
    
    150
    -                    ['Secret'], 0)
    
    151
    -                Secret = gir.Secret
    
    152
    -            except (ValueError, AttributeError):
    
    153
    -                pass
    
    154
    -            try:
    
    155
    -                if os.name != 'nt':
    
    156
    -                    storage = SecretPasswordStorage()
    
    157
    -                else:
    
    158
    -                    storage = SecretWindowsPasswordStorage()
    
    159
    -            except Exception:
    
    160
    -                storage = SimplePasswordStorage()
    
    127
    +            if os.name == 'nt' and keyring:
    
    128
    +                self.winsecret = SecretWindowsPasswordStorage()
    
    129
    +            else:
    
    130
    +                try:
    
    131
    +                    self.libsecret = LibSecretPasswordStorage()
    
    132
    +                except (ValueError, AttributeError) as e:
    
    133
    +                    log.debug("Could not connect to libsecret: %s" % e)
    
    134
    +
    
    135
    +    def get_password(self, account_name):
    
    136
    +        pw = gajim.config.get_per('accounts', account_name, 'password')
    
    137
    +        if not pw:
    
    138
    +            return pw
    
    139
    +        if pw.startswith(LibSecretPasswordStorage.identifier) and \
    
    140
    +        self.libsecret:
    
    141
    +            backend = self.libsecret
    
    142
    +        elif pw.startswith(SecretWindowsPasswordStorage.identifier) and \
    
    143
    +        self.winsecret:
    
    144
    +            backend = self.winsecret
    
    145
    +        else:
    
    146
    +            backend = None
    
    147
    +
    
    148
    +        if backend:
    
    149
    +            pw = backend.get_password(account_name)
    
    150
    +        if backend != self.preferred_backend:
    
    151
    +            # migrate password to preferred_backend
    
    152
    +            self.preferred_backend.save_password(account_name, pw)
    
    153
    +            # TODO: remove from old backend
    
    154
    +        return pw
    
    155
    +
    
    156
    +    def save_password(self, account_name, password):
    
    157
    +        if self.preferred_backend:
    
    158
    +            if self.preferred_backend.save_password(account_name, password):
    
    159
    +                gajim.config.set_per('accounts', account_name, 'password',
    
    160
    +                    self.preferred_backend.identifier)
    
    161
    +                if account_name in gajim.connections:
    
    162
    +                    gajim.connections[account_name].password = password
    
    163
    +                return True
    
    164
    +
    
    165
    +        gajim.config.set_per('accounts', account_name, 'password', password)
    
    166
    +        if account_name in gajim.connections:
    
    167
    +            gajim.connections[account_name].password = password
    
    168
    +        return True
    
    169
    +
    
    170
    +    def set_preferred_backend(self):
    
    171
    +        if self.libsecret:
    
    172
    +            self.preferred_backend = self.libsecret
    
    173
    +        elif self.winsecret:
    
    174
    +            self.preferred_backend = self.winsecret
    
    161 175
             else:
    
    162
    -            storage = SimplePasswordStorage()
    
    163
    -    return storage
    
    176
    +            self.preferred_backend = None
    
    177
    +
    
    178
    +    def has_keyring(self):
    
    179
    +        """Is there a real password storage backend? Else, passwords are stored
    
    180
    +        plain in gajim config"""
    
    181
    +        return bool(self.preferred_backend)
    
    164 182
     
    
    165
    -def set_storage(storage_):
    
    166
    -    global storage
    
    167
    -    storage = storage_
    
    183
    +passwordStorageManager = None
    
    184
    +
    
    185
    +def get_storage():
    
    186
    +    global passwordStorageManager
    
    187
    +    if not passwordStorageManager:
    
    188
    +        passwordStorageManager = PasswordStorageManager()
    
    189
    +    return passwordStorageManager
    
    168 190
     
    
    169 191
     def get_password(account_name):
    
    170 192
         return get_storage().get_password(account_name)