~ubuntu-branches/ubuntu/precise/python-keyring/precise-updates

« back to all changes in this revision

Viewing changes to .pc/fix_unlock.patch/keyring/backend.py

  • Committer: Package Import Robot
  • Author(s): Marc Deslauriers
  • Date: 2012-11-19 12:50:49 UTC
  • mfrom: (8.1.1 sid)
  • Revision ID: package-import@ubuntu.com-20121119125049-mryxwft3qi7fz3pg
Tags: 0.9.2-0ubuntu0.12.04.2
* SECURITY UPDATE: CryptedFileKeyring format is insecure (LP: #1004845)
  - Rebuild python-keyring 0.9.2 from Ubuntu 12.10 as a security update
    for Ubuntu 12.04.
  - debian/patches/crypto_compat.patch: include PBKDF2() directly to be
    compatible with the older version of python-crypto in Ubuntu 12.04.
  - CVE-2012-4571
* SECURITY UPDATE: insecure default file permissions (LP: #1031465)
  - debian/patches/file_permissions.patch: set appropriate permissions on
    database directory.
  - CVE number pending
* debian/patches/fix_migration.patch: fix migration code so old
  databases get upgraded when a key is read. (LP: #1042754)
* debian/patches/fix_unlock.patch: fix unlocking an existing keyring.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
2
backend.py
 
3
 
 
4
Keyring Backend implementations
 
5
"""
 
6
 
 
7
import getpass
 
8
import os
 
9
import stat
 
10
import sys
 
11
import ConfigParser
 
12
import base64
 
13
import StringIO
 
14
 
 
15
from keyring.util.escape import escape as escape_for_ini
 
16
import keyring.util.escape
 
17
from keyring.util import properties
 
18
import keyring.util.platform
 
19
import keyring.util.loc_compat
 
20
import keyring.py25compat
 
21
 
 
22
# use abstract base classes from the compat module
 
23
abc = keyring.py25compat.abc
 
24
# use json from the compat module
 
25
json = keyring.py25compat.json
 
26
 
 
27
class PasswordSetError(Exception):
 
28
    """Raised when the password can't be set.
 
29
    """
 
30
 
 
31
class KeyringBackend(object):
 
32
    """The abstract base class of the keyring, every backend must implement
 
33
    this interface.
 
34
    """
 
35
    __metaclass__ = abc.ABCMeta
 
36
 
 
37
    @abc.abstractmethod
 
38
    def supported(self):
 
39
        """Return if this keyring supports current environment:
 
40
        -1: not applicable
 
41
         0: suitable
 
42
         1: recommended
 
43
        """
 
44
        return -1
 
45
 
 
46
    @abc.abstractmethod
 
47
    def get_password(self, service, username):
 
48
        """Get password of the username for the service
 
49
        """
 
50
        return None
 
51
 
 
52
    @abc.abstractmethod
 
53
    def set_password(self, service, username, password):
 
54
        """Set password for the username of the service
 
55
        """
 
56
        raise PasswordSetError("reason")
 
57
 
 
58
class _ExtensionKeyring(KeyringBackend):
 
59
    """_ExtensionKeyring is a adaptor class for the platform related keyring
 
60
    backends.
 
61
    """
 
62
    def __init__(self):
 
63
        try:
 
64
            self.keyring_impl = self._init_backend()
 
65
        except ImportError:
 
66
            # keyring is not installed properly
 
67
            self.keyring_impl = None
 
68
 
 
69
    def _init_backend(self):
 
70
        """Return the keyring implementation handler
 
71
        """
 
72
        return None
 
73
 
 
74
    def _recommend(self):
 
75
        """If this keyring is recommended on current environment.
 
76
        """
 
77
        return False
 
78
 
 
79
    def supported(self):
 
80
        """Override the supported() in KeyringBackend.
 
81
        """
 
82
        if self.keyring_impl is None:
 
83
            return -1
 
84
        elif self._recommend():
 
85
            return 1
 
86
        return 0
 
87
 
 
88
    def get_password(self, service, username):
 
89
        """Override the get_password() in KeyringBackend.
 
90
        """
 
91
        try:
 
92
            password = self.keyring_impl.password_get(service, username)
 
93
        except OSError:
 
94
            password = None
 
95
        return password
 
96
 
 
97
    def set_password(self, service, username, password):
 
98
        """Override the set_password() in KeyringBackend.
 
99
        """
 
100
        try:
 
101
            self.keyring_impl.password_set(service, username, password)
 
102
        except OSError, e:
 
103
            raise PasswordSetError(e.message)
 
104
 
 
105
class OSXKeychain(_ExtensionKeyring):
 
106
    """Mac OS X Keychain"""
 
107
    def _init_backend(self):
 
108
        """Return the handler: osx_keychain
 
109
        """
 
110
        from backends import osx_keychain
 
111
        return osx_keychain
 
112
 
 
113
    def _recommend(self):
 
114
        """Recommended for all OSX environment.
 
115
        """
 
116
        return sys.platform == 'darwin'
 
117
 
 
118
class GnomeKeyring(KeyringBackend):
 
119
    """Gnome Keyring"""
 
120
 
 
121
    # Name of the keyring to store the passwords in.
 
122
    # Use None for the default keyring.
 
123
    KEYRING_NAME = None
 
124
 
 
125
    def supported(self):
 
126
        try:
 
127
            __import__('gnomekeyring')
 
128
        except ImportError:
 
129
            return -1
 
130
        else:
 
131
            if ("GNOME_KEYRING_CONTROL" in os.environ and
 
132
                "DISPLAY" in os.environ and
 
133
                "DBUS_SESSION_BUS_ADDRESS" in os.environ):
 
134
                return 1
 
135
            else:
 
136
                return 0
 
137
 
 
138
    def get_password(self, service, username):
 
139
        """Get password of the username for the service
 
140
        """
 
141
        import gnomekeyring
 
142
 
 
143
        try:
 
144
            items = gnomekeyring.find_network_password_sync(username, service)
 
145
        except gnomekeyring.NoMatchError:
 
146
            return None
 
147
        except gnomekeyring.CancelledError:
 
148
            # The user pressed "Cancel" when prompted to unlock their keyring.
 
149
            return None
 
150
 
 
151
        assert len(items) == 1, 'no more than one entry should ever match'
 
152
        return items[0]['password']
 
153
 
 
154
    def set_password(self, service, username, password):
 
155
        """Set password for the username of the service
 
156
        """
 
157
        import gnomekeyring
 
158
        try:
 
159
            gnomekeyring.item_create_sync(
 
160
                self.KEYRING_NAME, gnomekeyring.ITEM_NETWORK_PASSWORD,
 
161
                "Password for '%s' on '%s'" % (username, service),
 
162
                {'user': username, 'domain': service},
 
163
                password, True)
 
164
        except gnomekeyring.CancelledError:
 
165
            # The user pressed "Cancel" when prompted to unlock their keyring.
 
166
            raise PasswordSetError("cancelled by user")
 
167
 
 
168
 
 
169
class SecretServiceKeyring(KeyringBackend):
 
170
    """Secret Service Keyring"""
 
171
 
 
172
    def supported(self):
 
173
        try:
 
174
            import dbus
 
175
        except ImportError:
 
176
            return -1
 
177
        try:
 
178
            bus = dbus.SessionBus()
 
179
            bus.get_object('org.freedesktop.secrets',
 
180
                '/org/freedesktop/secrets')
 
181
        except dbus.exceptions.DBusException:
 
182
            return -1
 
183
        else:
 
184
            return 1
 
185
 
 
186
    def get_password(self, service, username):
 
187
        """Get password of the username for the service
 
188
        """
 
189
        import dbus
 
190
        bus = dbus.SessionBus()
 
191
        service_obj = bus.get_object('org.freedesktop.secrets',
 
192
            '/org/freedesktop/secrets')
 
193
        service_iface = dbus.Interface(service_obj,
 
194
            'org.freedesktop.Secret.Service')
 
195
        unlocked, locked = service_iface.SearchItems(
 
196
            {"username": username, "service": service})
 
197
        _, session = service_iface.OpenSession("plain", "")
 
198
        no_longer_locked, prompt = service_iface.Unlock(locked)
 
199
        assert prompt == "/"
 
200
        secrets = service_iface.GetSecrets(unlocked + locked, session)
 
201
        for item_path, secret in secrets.iteritems():
 
202
            return "".join([str(x) for x in secret[2]])
 
203
        return None
 
204
 
 
205
    def set_password(self, service, username, password):
 
206
        """Set password for the username of the service
 
207
        """
 
208
        import dbus
 
209
        bus = dbus.SessionBus()
 
210
        service_obj = bus.get_object('org.freedesktop.secrets',
 
211
            '/org/freedesktop/secrets')
 
212
        service_iface = dbus.Interface(service_obj,
 
213
            'org.freedesktop.Secret.Service')
 
214
        collection_obj = bus.get_object(
 
215
            'org.freedesktop.secrets',
 
216
            '/org/freedesktop/secrets/aliases/default')
 
217
        collection = dbus.Interface(collection_obj,
 
218
            'org.freedesktop.Secret.Collection')
 
219
        attributes = {
 
220
            "service": service,
 
221
            "username": username
 
222
            }
 
223
        _, session = service_iface.OpenSession("plain", "")
 
224
 
 
225
        if isinstance(password, unicode):
 
226
            password = password.encode('utf-8')
 
227
        secret = dbus.Struct(
 
228
            (session, "", dbus.ByteArray(password), "text/plain"))
 
229
        properties = {
 
230
            "org.freedesktop.Secret.Item.Label": "%s @ %s" % (
 
231
                username, service),
 
232
            "org.freedesktop.Secret.Item.Attributes": attributes}
 
233
        (item, prompt) = collection.CreateItem(properties, secret,
 
234
            True)
 
235
        assert prompt == "/"
 
236
 
 
237
 
 
238
kwallet = None
 
239
 
 
240
def open_kwallet(kwallet_module=None, qt_module=None):
 
241
 
 
242
    # If we specified the kwallet_module and/or qt_module, surely we won't need
 
243
    # the cached kwallet object...
 
244
    if kwallet_module is None and qt_module is None:
 
245
        global kwallet
 
246
        if not kwallet is None:
 
247
            return kwallet
 
248
 
 
249
    # Allow for the injection of module-like objects for testing purposes.
 
250
    if kwallet_module is None:
 
251
        kwallet_module = KWallet.Wallet
 
252
    if qt_module is None:
 
253
        qt_module = QtGui
 
254
 
 
255
    # KDE wants us to instantiate an application object.
 
256
    app = None
 
257
    if qt_module.qApp.instance() == None:
 
258
        app = qt_module.QApplication([])
 
259
    try:
 
260
        window = qt_module.QWidget()
 
261
        kwallet = kwallet_module.openWallet(
 
262
            kwallet_module.NetworkWallet(),
 
263
            window.winId(),
 
264
            kwallet_module.Synchronous)
 
265
        if kwallet is not None:
 
266
            if not kwallet.hasFolder('Python'):
 
267
                kwallet.createFolder('Python')
 
268
            kwallet.setFolder('Python')
 
269
            return kwallet
 
270
    finally:
 
271
        if app:
 
272
            app.exit()
 
273
 
 
274
 
 
275
try:
 
276
    from PyKDE4.kdeui import KWallet
 
277
    from PyQt4 import QtGui
 
278
except ImportError:
 
279
    kwallet_support = False
 
280
else:
 
281
    kwallet_support = True
 
282
 
 
283
 
 
284
class KDEKWallet(KeyringBackend):
 
285
    """KDE KWallet"""
 
286
 
 
287
    def supported(self):
 
288
        if kwallet_support and 'KDE_SESSION_UID' in os.environ:
 
289
            return 1
 
290
        elif kwallet_support:
 
291
            return 0
 
292
        else:
 
293
            return -1
 
294
 
 
295
    def get_password(self, service, username):
 
296
        """Get password of the username for the service
 
297
        """
 
298
        key = username + '@' + service
 
299
        network = KWallet.Wallet.NetworkWallet()
 
300
        wallet = open_kwallet()
 
301
        if wallet.keyDoesNotExist(network, 'Python', key):
 
302
            return None
 
303
 
 
304
        result = wallet.readPassword(key)[1]
 
305
        # The string will be a PyQt4.QtCore.QString, so turn it into a unicode
 
306
        # object.
 
307
        return unicode(result)
 
308
 
 
309
    def set_password(self, service, username, password):
 
310
        """Set password for the username of the service
 
311
        """
 
312
        wallet = open_kwallet()
 
313
        wallet.writePassword(username+'@'+service, password)
 
314
 
 
315
class BasicFileKeyring(KeyringBackend):
 
316
    """
 
317
    BasicFileKeyring is a file-based implementation of keyring.
 
318
 
 
319
    This keyring stores the password directly in the file and provides methods
 
320
    which may be overridden by subclasses to support
 
321
    encryption and decryption. The encrypted payload is stored in base64
 
322
    format.
 
323
    """
 
324
 
 
325
    @properties.NonDataProperty
 
326
    def file_path(self):
 
327
        """
 
328
        The path to the file where passwords are stored. This property
 
329
        may be overridden by the subclass or at the instance level.
 
330
        """
 
331
        return os.path.join(keyring.util.platform.data_root(), self.filename)
 
332
 
 
333
    @abc.abstractproperty
 
334
    def filename(self):
 
335
        """The filename used to store the passwords.
 
336
        """
 
337
        pass
 
338
 
 
339
    @abc.abstractmethod
 
340
    def encrypt(self, password):
 
341
        """Encrypt the password.
 
342
        """
 
343
        pass
 
344
 
 
345
    @abc.abstractmethod
 
346
    def decrypt(self, password_encrypted):
 
347
        """Decrypt the password.
 
348
        """
 
349
        pass
 
350
 
 
351
    def _migrate(self, keyring_password=None):
 
352
        """Convert older keyrings to the current format.
 
353
        """
 
354
        pass
 
355
 
 
356
    def _relocate_file(self):
 
357
        old_location = os.path.join(os.path.expanduser('~'), self.filename)
 
358
        new_location = self.file_path
 
359
        keyring.util.loc_compat.relocate_file(old_location, new_location)
 
360
        # disable this function - it only needs to be run once
 
361
        self._relocate_file = lambda: None
 
362
 
 
363
    def get_password(self, service, username):
 
364
        """Read the password from the file.
 
365
        """
 
366
        self._relocate_file()
 
367
        service = escape_for_ini(service)
 
368
        username = escape_for_ini(username)
 
369
 
 
370
        # load the passwords from the file
 
371
        config = ConfigParser.RawConfigParser()
 
372
        if os.path.exists(self.file_path):
 
373
            self._migrate()
 
374
            config.read(self.file_path)
 
375
 
 
376
        # fetch the password
 
377
        try:
 
378
            password_base64 = config.get(service, username).encode()
 
379
            # decode with base64
 
380
            password_encrypted = base64.decodestring(password_base64)
 
381
            # decrypted the password
 
382
            password = self.decrypt(password_encrypted).decode('utf-8')
 
383
        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
 
384
            password = None
 
385
        return password
 
386
 
 
387
    def set_password(self, service, username, password):
 
388
        """Write the password in the file.
 
389
        """
 
390
        self._relocate_file()
 
391
        service = escape_for_ini(service)
 
392
        username = escape_for_ini(username)
 
393
 
 
394
        # encrypt the password
 
395
        password_encrypted = self.encrypt(password.encode('utf-8'))
 
396
        # load the password from the disk
 
397
        config = ConfigParser.RawConfigParser()
 
398
        if os.path.exists(self.file_path):
 
399
            config.read(self.file_path)
 
400
 
 
401
        # encode with base64
 
402
        password_base64 = base64.encodestring(password_encrypted).decode()
 
403
        # write the modification
 
404
        if not config.has_section(service):
 
405
            config.add_section(service)
 
406
        config.set(service, username, password_base64)
 
407
        self._ensure_file_path()
 
408
        config_file = open(self.file_path,'w')
 
409
        config.write(config_file)
 
410
 
 
411
    def _ensure_file_path(self):
 
412
        """ensure the storage path exists"""
 
413
        storage_root = os.path.dirname(self.file_path)
 
414
        if storage_root and not os.path.isdir(storage_root):
 
415
            os.makedirs(storage_root)
 
416
        os.chmod(storage_root, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
 
417
 
 
418
 
 
419
class UncryptedFileKeyring(BasicFileKeyring):
 
420
    """Uncrypted File Keyring"""
 
421
 
 
422
    filename = 'keyring_pass.cfg'
 
423
 
 
424
    def encrypt(self, password):
 
425
        """Directly return the password itself.
 
426
        """
 
427
        return password
 
428
 
 
429
    def decrypt(self, password_encrypted):
 
430
        """Directly return encrypted password.
 
431
        """
 
432
        return password_encrypted
 
433
 
 
434
    def supported(self):
 
435
        """Applicable for all platforms, but do not recommend.
 
436
        """
 
437
        return 0
 
438
 
 
439
class CryptedFileKeyring(BasicFileKeyring):
 
440
    """PyCrypto File Keyring"""
 
441
 
 
442
    # a couple constants
 
443
    block_size = 32
 
444
    pad_char = '0'
 
445
 
 
446
    filename = 'crypted_pass.cfg'
 
447
 
 
448
    def supported(self):
 
449
        """Applicable for all platforms, but not recommend"
 
450
        """
 
451
        try:
 
452
            __import__('Crypto.Cipher.AES')
 
453
            __import__('Crypto.Protocol.KDF')
 
454
            __import__('Crypto.Random')
 
455
            if not json:
 
456
                raise AssertionError("JSON implementation needed (install "
 
457
                    "simplejson)")
 
458
            status = 0
 
459
        except (ImportError, AssertionError):
 
460
            status = -1
 
461
        return status
 
462
 
 
463
    @properties.NonDataProperty
 
464
    def keyring_key(self):
 
465
        # _unlock or _init_file will set the key or raise an exception
 
466
        self._check_file() and self._unlock() or self._init_file()
 
467
        return self.keyring_key
 
468
 
 
469
    def _get_new_password(self):
 
470
        while True:
 
471
            password = getpass.getpass(
 
472
                "Please set a password for your new keyring: ")
 
473
            confirm = getpass.getpass('Please confirm the password: ')
 
474
            if password != confirm:
 
475
                sys.stderr.write("Error: Your passwords didn't match\n")
 
476
                continue
 
477
            if '' == password.strip():
 
478
                # forbid the blank password
 
479
                sys.stderr.write("Error: blank passwords aren't allowed.\n")
 
480
                continue
 
481
            return password
 
482
 
 
483
    def _init_file(self):
 
484
        """
 
485
        Initialize a new password file and set the reference password.
 
486
        """
 
487
        self.keyring_key = self._get_new_password()
 
488
        # set a reference password, used to check that the password provided
 
489
        #  matches for subsequent checks.
 
490
        self.set_password('keyring-setting', 'password reference',
 
491
            'password reference value')
 
492
 
 
493
    def _check_file(self):
 
494
        """
 
495
        Check if the file exists and has the expected password reference.
 
496
        """
 
497
        if not os.path.exists(self.file_path):
 
498
            return False
 
499
        self._migrate()
 
500
        config = ConfigParser.RawConfigParser()
 
501
        config.read(self.file_path)
 
502
        try:
 
503
            config.get(
 
504
                escape_for_ini('keyring-setting'),
 
505
                escape_for_ini('password reference'),
 
506
            )
 
507
        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
 
508
            return False
 
509
        return True
 
510
 
 
511
    def _unlock(self):
 
512
        """
 
513
        Unlock this keyring by getting the password for the keyring from the
 
514
        user.
 
515
        """
 
516
        self.keyring_key = getpass.getpass(
 
517
            'Please enter password for encrypted keyring: ')
 
518
        try:
 
519
            ref_pw = self.get_password('keyring-setting', 'password reference')
 
520
            assert ref_pw == 'password reference value'
 
521
        except AssertionError:
 
522
            self._lock()
 
523
            raise ValueError("Incorrect Password")
 
524
 
 
525
    def _lock(self):
 
526
        """
 
527
        Remove the keyring key from this instance.
 
528
        """
 
529
        del self.keyring_key
 
530
 
 
531
    def _create_cipher(self, password, salt, IV):
 
532
        """
 
533
        Create the cipher object to encrypt or decrypt a payload.
 
534
        """
 
535
 
 
536
        def PBKDF2(password, salt, dkLen=16, count=1000, prf=None):
 
537
 
 
538
            import sys
 
539
            import struct
 
540
            from Crypto.Util.py3compat import b
 
541
            from Crypto.Hash import SHA as SHA1, HMAC
 
542
            from Crypto.Util.strxor import strxor
 
543
 
 
544
            if sys.version_info[0] == 2:
 
545
                def tobytes(s):
 
546
                    if isinstance(s, unicode):
 
547
                        return s.encode("latin-1")
 
548
                    else:
 
549
                        return ''.join(s)
 
550
            else:
 
551
                def tobytes(s):
 
552
                    if isinstance(s,bytes):
 
553
                        return s
 
554
                    else:
 
555
                        if isinstance(s,str):
 
556
                            return s.encode("latin-1")
 
557
                        else:
 
558
                            return bytes(s)
 
559
 
 
560
            password = tobytes(password)
 
561
            if prf is None:
 
562
                prf = lambda p,s: HMAC.new(p,s,SHA1).digest()
 
563
            key = b('')
 
564
            i = 1
 
565
            while len(key)<dkLen:
 
566
                U = previousU = prf(password,salt+struct.pack(">I", i))
 
567
                for j in xrange(count-1):
 
568
                    previousU = t = prf(password,previousU)
 
569
                    U = strxor(U,t)
 
570
                key += U
 
571
                i = i + 1
 
572
            return key[:dkLen]
 
573
 
 
574
        from Crypto.Cipher import AES
 
575
        pw = PBKDF2(password, salt, dkLen=self.block_size)
 
576
        return AES.new(pw[:self.block_size], AES.MODE_CFB, IV)
 
577
 
 
578
    def encrypt(self, password):
 
579
        from Crypto.Random import get_random_bytes
 
580
        salt = get_random_bytes(self.block_size)
 
581
        from Crypto.Cipher import AES
 
582
        IV = get_random_bytes(AES.block_size)
 
583
        cipher = self._create_cipher(self.keyring_key, salt, IV)
 
584
        password_encrypted = cipher.encrypt('pw:' + password)
 
585
        # Serialize the salt, IV, and encrypted password in a secure format
 
586
        data = dict(
 
587
            salt=salt, IV=IV, password_encrypted=password_encrypted,
 
588
        )
 
589
        for key in data:
 
590
            data[key] = data[key].encode('base64')
 
591
        return json.dumps(data)
 
592
 
 
593
    def decrypt(self, password_encrypted):
 
594
        # unpack the encrypted payload
 
595
        data = json.loads(password_encrypted)
 
596
        for key in data:
 
597
            data[key] = data[key].decode('base64')
 
598
        cipher = self._create_cipher(self.keyring_key, data['salt'],
 
599
            data['IV'])
 
600
        plaintext = cipher.decrypt(data['password_encrypted'])
 
601
        assert plaintext.startswith('pw:')
 
602
        return plaintext[3:]
 
603
 
 
604
    def _migrate(self, keyring_password=None):
 
605
        """
 
606
        Convert older keyrings to the current format.
 
607
        """
 
608
        self.__convert_0_9_0(keyring_password)
 
609
        self.__convert_0_9_1(keyring_password)
 
610
 
 
611
    def __convert_0_9_1(self, keyring_password):
 
612
        """
 
613
        Convert keyring from the 0.9.1 format to the current format.
 
614
        """
 
615
        with open(self.file_path) as f:
 
616
            encoded_lines = list(f)
 
617
        try:
 
618
            head, data = [line.decode('base64') for line in encoded_lines]
 
619
        except Exception:
 
620
            # not an 0.9.1 formatted file
 
621
            return
 
622
 
 
623
        print("Keyring from 0.9.1 detected. Upgrading...")
 
624
 
 
625
        salt = head[:self.block_size]
 
626
        IV = head[self.block_size:]
 
627
 
 
628
        if keyring_password is None:
 
629
            keyring_password = getpass.getpass(
 
630
                "Please input your password for the keyring: ")
 
631
 
 
632
        cipher = self._create_cipher(keyring_password, salt, IV)
 
633
 
 
634
        config_file = StringIO.StringIO(cipher.decrypt(data))
 
635
        config = ConfigParser.RawConfigParser()
 
636
        try:
 
637
            config.readfp(config_file)
 
638
        except ConfigParser.Error:
 
639
            sys.stderr.write("Wrong password for the keyring.\n")
 
640
            raise ValueError("Wrong password")
 
641
 
 
642
        self.keyring_key = keyring_password
 
643
 
 
644
        # wipe the existing file
 
645
        os.remove(self.file_path)
 
646
 
 
647
        self.set_password('keyring-setting', 'password reference',
 
648
            'password reference value')
 
649
 
 
650
        for service in config.sections():
 
651
            for user in config.options(service):
 
652
                password = config.get(service, user).decode('utf-8')
 
653
                service = keyring.util.escape.unescape(service)
 
654
                user = keyring.util.escape.unescape(user)
 
655
                self.set_password(service, user, password)
 
656
 
 
657
        print("File upgraded successfully")
 
658
 
 
659
    def __convert_0_9_0(self, keyring_password):
 
660
        """
 
661
        Convert keyring from the 0.9.0 and earlier format to the current
 
662
        format.
 
663
        """
 
664
        KEYRING_SETTING = 'keyring-setting'
 
665
        CRYPTED_PASSWORD = 'crypted-password'
 
666
 
 
667
        try:
 
668
            config = ConfigParser.RawConfigParser()
 
669
            config.read(self.file_path)
 
670
            config.get(KEYRING_SETTING, CRYPTED_PASSWORD)
 
671
        except Exception:
 
672
            return
 
673
 
 
674
        print("Keyring from 0.9.0 or earlier detected. Upgrading...")
 
675
 
 
676
        import crypt
 
677
 
 
678
        if keyring_password is None:
 
679
            keyring_password = getpass.getpass(
 
680
                "Please input your password for the keyring: ")
 
681
 
 
682
        hashed = crypt.crypt(keyring_password, keyring_password)
 
683
        if config.get(KEYRING_SETTING, CRYPTED_PASSWORD) != hashed:
 
684
            sys.stderr.write("Wrong password for the keyring.\n")
 
685
            raise ValueError("Wrong password")
 
686
 
 
687
        self.keyring_key = keyring_password
 
688
        config.remove_option(KEYRING_SETTING, CRYPTED_PASSWORD)
 
689
        with open(self.file_path, 'w') as f:
 
690
            config.write(f)
 
691
        self.set_password('keyring-setting', 'password reference',
 
692
            'password reference value')
 
693
 
 
694
        from Crypto.Cipher import AES
 
695
        password = keyring_password + (
 
696
            self.block_size - len(keyring_password) % self.block_size
 
697
            ) * self.pad_char
 
698
 
 
699
        for service in config.sections():
 
700
            for user in config.options(service):
 
701
                cipher = AES.new(password, AES.MODE_CFB,
 
702
                    '\0' * AES.block_size)
 
703
                password_c = config.get(service, user).decode('base64')
 
704
                service = keyring.util.escape.unescape(service)
 
705
                user = keyring.util.escape.unescape(user)
 
706
                password_p = cipher.decrypt(password_c)
 
707
                self.set_password(service, user, password_p)
 
708
 
 
709
        print("File upgraded successfully")
 
710
 
 
711
 
 
712
class Win32CryptoKeyring(BasicFileKeyring):
 
713
    """Win32 Cryptography Keyring"""
 
714
 
 
715
    filename = 'wincrypto_pass.cfg'
 
716
 
 
717
    def __init__(self):
 
718
        super(Win32CryptoKeyring, self).__init__()
 
719
 
 
720
        try:
 
721
            from backends import win32_crypto
 
722
            self.crypt_handler = win32_crypto
 
723
        except ImportError:
 
724
            self.crypt_handler = None
 
725
 
 
726
    def supported(self):
 
727
        """Recommended when other Windows backends are unavailable
 
728
        """
 
729
        recommended = select_windows_backend()
 
730
        if recommended == None:
 
731
            return -1
 
732
        elif recommended == 'file':
 
733
            return 1
 
734
        else:
 
735
            return 0
 
736
 
 
737
    def encrypt(self, password):
 
738
        """Encrypt the password using the CryptAPI.
 
739
        """
 
740
        return self.crypt_handler.encrypt(password)
 
741
 
 
742
    def decrypt(self, password_encrypted):
 
743
        """Decrypt the password using the CryptAPI.
 
744
        """
 
745
        return self.crypt_handler.decrypt(password_encrypted)
 
746
 
 
747
 
 
748
class WinVaultKeyring(KeyringBackend):
 
749
    """
 
750
    WinVaultKeyring stores encrypted passwords using the Windows Credential
 
751
    Manager.
 
752
 
 
753
    Requires pywin32
 
754
 
 
755
    This backend does some gymnastics to simulate multi-user support,
 
756
    which WinVault doesn't support natively. See
 
757
    https://bitbucket.org/kang/python-keyring-lib/issue/47/winvaultkeyring-only-ever-returns-last#comment-731977
 
758
    for details on the implementation, but here's the gist:
 
759
 
 
760
    Passwords are stored under the service name unless there is a collision
 
761
    (another password with the same service name but different user name),
 
762
    in which case the previous password is moved into a compound name:
 
763
    {username}@{service}
 
764
    """
 
765
    def __init__(self):
 
766
        super(WinVaultKeyring, self).__init__()
 
767
        try:
 
768
            import pywintypes
 
769
            import win32cred
 
770
            self.win32cred = win32cred
 
771
            self.pywintypes = pywintypes
 
772
        except ImportError:
 
773
            self.win32cred = None
 
774
 
 
775
    def supported(self):
 
776
        '''Default Windows backend, when it is available
 
777
        '''
 
778
        recommended = select_windows_backend()
 
779
        if recommended == None:
 
780
            return -1
 
781
        elif recommended == 'cred':
 
782
            return 1
 
783
        else:
 
784
            return 0
 
785
 
 
786
    @staticmethod
 
787
    def _compound_name(username, service):
 
788
        return u'%(username)s@%(service)s' % vars()
 
789
 
 
790
    def get_password(self, service, username):
 
791
        # first attempt to get the password under the service name
 
792
        res = self._get_password(service)
 
793
        if not res or res['UserName'] != username:
 
794
            # It wasn't found so attempt to get it with the compound name
 
795
            res = self._get_password(self._compound_name(username, service))
 
796
        if not res:
 
797
            return None
 
798
        blob = res['CredentialBlob']
 
799
        return blob.decode('utf-16')
 
800
 
 
801
    def _get_password(self, target):
 
802
        try:
 
803
            res = self.win32cred.CredRead(
 
804
                Type=self.win32cred.CRED_TYPE_GENERIC,
 
805
                TargetName=target,
 
806
            )
 
807
        except self.pywintypes.error, e:
 
808
            if e.winerror == 1168 and e.funcname == 'CredRead': # not found
 
809
                return None
 
810
            raise
 
811
        return res
 
812
 
 
813
    def set_password(self, service, username, password):
 
814
        existing_pw = self._get_password(service)
 
815
        if existing_pw:
 
816
            # resave the existing password using a compound target
 
817
            existing_username = existing_pw['UserName']
 
818
            target = self._compound_name(existing_username, service)
 
819
            self._set_password(target, existing_username,
 
820
                existing_pw['CredentialBlob'].decode('utf-16'))
 
821
        self._set_password(service, username, unicode(password))
 
822
 
 
823
    def _set_password(self, target, username, password):
 
824
        credential = dict(Type=self.win32cred.CRED_TYPE_GENERIC,
 
825
                          TargetName=target,
 
826
                          UserName=username,
 
827
                          CredentialBlob=password,
 
828
                          Comment="Stored using python-keyring",
 
829
                          Persist=self.win32cred.CRED_PERSIST_ENTERPRISE)
 
830
        self.win32cred.CredWrite(credential, 0)
 
831
 
 
832
    def delete_password(self, service, username):
 
833
        compound = self._compound_name(username, service)
 
834
        for target in service, compound:
 
835
            existing_pw = self._get_password(target)
 
836
            if existing_pw and existing_pw['UserName'] == username:
 
837
                self._delete_password(target)
 
838
 
 
839
    def _delete_password(self, target):
 
840
        self.win32cred.CredDelete(
 
841
            Type=self.win32cred.CRED_TYPE_GENERIC,
 
842
            TargetName=target,
 
843
        )
 
844
 
 
845
class Win32CryptoRegistry(KeyringBackend):
 
846
    """Win32CryptoRegistry is a keyring which use Windows CryptAPI to encrypt
 
847
    the user's passwords and store them under registry keys
 
848
    """
 
849
    def __init__(self):
 
850
        super(Win32CryptoRegistry, self).__init__()
 
851
 
 
852
        try:
 
853
            from backends import win32_crypto
 
854
            __import__('_winreg')
 
855
            self.crypt_handler = win32_crypto
 
856
        except ImportError:
 
857
            self.crypt_handler = None
 
858
 
 
859
    def supported(self):
 
860
        """Return if this keyring supports current enviroment.
 
861
        -1: not applicable
 
862
         0: suitable
 
863
         1: recommended
 
864
        """
 
865
        recommended = select_windows_backend()
 
866
        if recommended == None:
 
867
            return -1
 
868
        elif recommended == 'reg':
 
869
            return 1
 
870
        else:
 
871
            return 0
 
872
 
 
873
    def get_password(self, service, username):
 
874
        """Get password of the username for the service
 
875
        """
 
876
        from _winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx
 
877
        try:
 
878
            # fetch the password
 
879
            key = r'Software\%s\Keyring' % service
 
880
            hkey = OpenKey(HKEY_CURRENT_USER, key)
 
881
            password_base64 = QueryValueEx(hkey, username)[0]
 
882
            # decode with base64
 
883
            password_encrypted = base64.decodestring(password_base64)
 
884
            # decrypted the password
 
885
            password = self.crypt_handler.decrypt(password_encrypted)
 
886
        except EnvironmentError:
 
887
            password = None
 
888
        return password
 
889
 
 
890
    def set_password(self, service, username, password):
 
891
        """Write the password to the registry
 
892
        """
 
893
        # encrypt the password
 
894
        password_encrypted = self.crypt_handler.encrypt(password)
 
895
        # encode with base64
 
896
        password_base64 = base64.encodestring(password_encrypted)
 
897
 
 
898
        # store the password
 
899
        from _winreg import HKEY_CURRENT_USER, CreateKey, SetValueEx, REG_SZ
 
900
        hkey = CreateKey(HKEY_CURRENT_USER, r'Software\%s\Keyring' % service)
 
901
        SetValueEx(hkey, username, 0, REG_SZ, password_base64)
 
902
 
 
903
def select_windows_backend():
 
904
    if os.name != 'nt':
 
905
        return None
 
906
    major, minor, build, platform, text = sys.getwindowsversion()
 
907
    try:
 
908
        __import__('pywintypes')
 
909
        __import__('win32cred')
 
910
        if (major, minor) >= (5, 1):
 
911
            # recommend for windows xp+
 
912
            return 'cred'
 
913
    except ImportError:
 
914
        pass
 
915
    try:
 
916
        __import__('keyring.backends.win32_crypto')
 
917
        __import__('_winreg')
 
918
        if (major, minor) >= (5, 0):
 
919
            # recommend for windows 2k+
 
920
            return 'reg'
 
921
    except ImportError:
 
922
        pass
 
923
    try:
 
924
        __import__('keyring.backends.win32_crypto')
 
925
        return 'file'
 
926
    except ImportError:
 
927
        pass
 
928
    return None
 
929
 
 
930
 
 
931
_all_keyring = None
 
932
 
 
933
def get_all_keyring():
 
934
    """Return the list of all keyrings in the lib
 
935
    """
 
936
    global _all_keyring
 
937
    if _all_keyring is None:
 
938
        _all_keyring = [OSXKeychain(), GnomeKeyring(), KDEKWallet(),
 
939
                        CryptedFileKeyring(), UncryptedFileKeyring(),
 
940
                        Win32CryptoKeyring(), Win32CryptoRegistry(),
 
941
                        WinVaultKeyring(), SecretServiceKeyring()]
 
942
    return _all_keyring