| ... |
... |
@@ -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)
|