4
Keyring Backend implementations
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
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
27
class PasswordSetError(Exception):
28
"""Raised when the password can't be set.
31
class KeyringBackend(object):
32
"""The abstract base class of the keyring, every backend must implement
35
__metaclass__ = abc.ABCMeta
39
"""Return if this keyring supports current environment:
47
def get_password(self, service, username):
48
"""Get password of the username for the service
53
def set_password(self, service, username, password):
54
"""Set password for the username of the service
56
raise PasswordSetError("reason")
58
class _ExtensionKeyring(KeyringBackend):
59
"""_ExtensionKeyring is a adaptor class for the platform related keyring
64
self.keyring_impl = self._init_backend()
66
# keyring is not installed properly
67
self.keyring_impl = None
69
def _init_backend(self):
70
"""Return the keyring implementation handler
75
"""If this keyring is recommended on current environment.
80
"""Override the supported() in KeyringBackend.
82
if self.keyring_impl is None:
84
elif self._recommend():
88
def get_password(self, service, username):
89
"""Override the get_password() in KeyringBackend.
92
password = self.keyring_impl.password_get(service, username)
97
def set_password(self, service, username, password):
98
"""Override the set_password() in KeyringBackend.
101
self.keyring_impl.password_set(service, username, password)
103
raise PasswordSetError(e.message)
105
class OSXKeychain(_ExtensionKeyring):
106
"""Mac OS X Keychain"""
107
def _init_backend(self):
108
"""Return the handler: osx_keychain
110
from backends import osx_keychain
113
def _recommend(self):
114
"""Recommended for all OSX environment.
116
return sys.platform == 'darwin'
118
class GnomeKeyring(KeyringBackend):
121
# Name of the keyring to store the passwords in.
122
# Use None for the default keyring.
127
__import__('gnomekeyring')
131
if ("GNOME_KEYRING_CONTROL" in os.environ and
132
"DISPLAY" in os.environ and
133
"DBUS_SESSION_BUS_ADDRESS" in os.environ):
138
def get_password(self, service, username):
139
"""Get password of the username for the service
144
items = gnomekeyring.find_network_password_sync(username, service)
145
except gnomekeyring.NoMatchError:
147
except gnomekeyring.CancelledError:
148
# The user pressed "Cancel" when prompted to unlock their keyring.
151
assert len(items) == 1, 'no more than one entry should ever match'
152
return items[0]['password']
154
def set_password(self, service, username, password):
155
"""Set password for the username of the service
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},
164
except gnomekeyring.CancelledError:
165
# The user pressed "Cancel" when prompted to unlock their keyring.
166
raise PasswordSetError("cancelled by user")
169
class SecretServiceKeyring(KeyringBackend):
170
"""Secret Service Keyring"""
178
bus = dbus.SessionBus()
179
bus.get_object('org.freedesktop.secrets',
180
'/org/freedesktop/secrets')
181
except dbus.exceptions.DBusException:
186
def get_password(self, service, username):
187
"""Get password of the username for the service
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)
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]])
205
def set_password(self, service, username, password):
206
"""Set password for the username of the service
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')
223
_, session = service_iface.OpenSession("plain", "")
225
if isinstance(password, unicode):
226
password = password.encode('utf-8')
227
secret = dbus.Struct(
228
(session, "", dbus.ByteArray(password), "text/plain"))
230
"org.freedesktop.Secret.Item.Label": "%s @ %s" % (
232
"org.freedesktop.Secret.Item.Attributes": attributes}
233
(item, prompt) = collection.CreateItem(properties, secret,
240
def open_kwallet(kwallet_module=None, qt_module=None):
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:
246
if not kwallet is None:
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:
255
# KDE wants us to instantiate an application object.
257
if qt_module.qApp.instance() == None:
258
app = qt_module.QApplication([])
260
window = qt_module.QWidget()
261
kwallet = kwallet_module.openWallet(
262
kwallet_module.NetworkWallet(),
264
kwallet_module.Synchronous)
265
if kwallet is not None:
266
if not kwallet.hasFolder('Python'):
267
kwallet.createFolder('Python')
268
kwallet.setFolder('Python')
276
from PyKDE4.kdeui import KWallet
277
from PyQt4 import QtGui
279
kwallet_support = False
281
kwallet_support = True
284
class KDEKWallet(KeyringBackend):
288
if kwallet_support and 'KDE_SESSION_UID' in os.environ:
290
elif kwallet_support:
295
def get_password(self, service, username):
296
"""Get password of the username for the service
298
key = username + '@' + service
299
network = KWallet.Wallet.NetworkWallet()
300
wallet = open_kwallet()
301
if wallet.keyDoesNotExist(network, 'Python', key):
304
result = wallet.readPassword(key)[1]
305
# The string will be a PyQt4.QtCore.QString, so turn it into a unicode
307
return unicode(result)
309
def set_password(self, service, username, password):
310
"""Set password for the username of the service
312
wallet = open_kwallet()
313
wallet.writePassword(username+'@'+service, password)
315
class BasicFileKeyring(KeyringBackend):
317
BasicFileKeyring is a file-based implementation of keyring.
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
325
@properties.NonDataProperty
328
The path to the file where passwords are stored. This property
329
may be overridden by the subclass or at the instance level.
331
return os.path.join(keyring.util.platform.data_root(), self.filename)
333
@abc.abstractproperty
335
"""The filename used to store the passwords.
340
def encrypt(self, password):
341
"""Encrypt the password.
346
def decrypt(self, password_encrypted):
347
"""Decrypt the password.
351
def _migrate(self, keyring_password=None):
352
"""Convert older keyrings to the current format.
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
363
def get_password(self, service, username):
364
"""Read the password from the file.
366
self._relocate_file()
367
service = escape_for_ini(service)
368
username = escape_for_ini(username)
370
# load the passwords from the file
371
config = ConfigParser.RawConfigParser()
372
if os.path.exists(self.file_path):
374
config.read(self.file_path)
378
password_base64 = config.get(service, username).encode()
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):
387
def set_password(self, service, username, password):
388
"""Write the password in the file.
390
self._relocate_file()
391
service = escape_for_ini(service)
392
username = escape_for_ini(username)
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)
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)
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)
419
class UncryptedFileKeyring(BasicFileKeyring):
420
"""Uncrypted File Keyring"""
422
filename = 'keyring_pass.cfg'
424
def encrypt(self, password):
425
"""Directly return the password itself.
429
def decrypt(self, password_encrypted):
430
"""Directly return encrypted password.
432
return password_encrypted
435
"""Applicable for all platforms, but do not recommend.
439
class CryptedFileKeyring(BasicFileKeyring):
440
"""PyCrypto File Keyring"""
446
filename = 'crypted_pass.cfg'
449
"""Applicable for all platforms, but not recommend"
452
__import__('Crypto.Cipher.AES')
453
__import__('Crypto.Protocol.KDF')
454
__import__('Crypto.Random')
456
raise AssertionError("JSON implementation needed (install "
459
except (ImportError, AssertionError):
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
469
def _get_new_password(self):
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")
477
if '' == password.strip():
478
# forbid the blank password
479
sys.stderr.write("Error: blank passwords aren't allowed.\n")
483
def _init_file(self):
485
Initialize a new password file and set the reference password.
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')
493
def _check_file(self):
495
Check if the file exists and has the expected password reference.
497
if not os.path.exists(self.file_path):
500
config = ConfigParser.RawConfigParser()
501
config.read(self.file_path)
504
escape_for_ini('keyring-setting'),
505
escape_for_ini('password reference'),
507
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
513
Unlock this keyring by getting the password for the keyring from the
516
self.keyring_key = getpass.getpass(
517
'Please enter password for encrypted keyring: ')
519
ref_pw = self.get_password('keyring-setting', 'password reference')
520
assert ref_pw == 'password reference value'
521
except AssertionError:
523
raise ValueError("Incorrect Password")
527
Remove the keyring key from this instance.
531
def _create_cipher(self, password, salt, IV):
533
Create the cipher object to encrypt or decrypt a payload.
536
def PBKDF2(password, salt, dkLen=16, count=1000, prf=None):
540
from Crypto.Util.py3compat import b
541
from Crypto.Hash import SHA as SHA1, HMAC
542
from Crypto.Util.strxor import strxor
544
if sys.version_info[0] == 2:
546
if isinstance(s, unicode):
547
return s.encode("latin-1")
552
if isinstance(s,bytes):
555
if isinstance(s,str):
556
return s.encode("latin-1")
560
password = tobytes(password)
562
prf = lambda p,s: HMAC.new(p,s,SHA1).digest()
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)
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)
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
587
salt=salt, IV=IV, password_encrypted=password_encrypted,
590
data[key] = data[key].encode('base64')
591
return json.dumps(data)
593
def decrypt(self, password_encrypted):
594
# unpack the encrypted payload
595
data = json.loads(password_encrypted)
597
data[key] = data[key].decode('base64')
598
cipher = self._create_cipher(self.keyring_key, data['salt'],
600
plaintext = cipher.decrypt(data['password_encrypted'])
601
assert plaintext.startswith('pw:')
604
def _migrate(self, keyring_password=None):
606
Convert older keyrings to the current format.
608
self.__convert_0_9_0(keyring_password)
609
self.__convert_0_9_1(keyring_password)
611
def __convert_0_9_1(self, keyring_password):
613
Convert keyring from the 0.9.1 format to the current format.
615
with open(self.file_path) as f:
616
encoded_lines = list(f)
618
head, data = [line.decode('base64') for line in encoded_lines]
620
# not an 0.9.1 formatted file
623
print("Keyring from 0.9.1 detected. Upgrading...")
625
salt = head[:self.block_size]
626
IV = head[self.block_size:]
628
if keyring_password is None:
629
keyring_password = getpass.getpass(
630
"Please input your password for the keyring: ")
632
cipher = self._create_cipher(keyring_password, salt, IV)
634
config_file = StringIO.StringIO(cipher.decrypt(data))
635
config = ConfigParser.RawConfigParser()
637
config.readfp(config_file)
638
except ConfigParser.Error:
639
sys.stderr.write("Wrong password for the keyring.\n")
640
raise ValueError("Wrong password")
642
self.keyring_key = keyring_password
644
# wipe the existing file
645
os.remove(self.file_path)
647
self.set_password('keyring-setting', 'password reference',
648
'password reference value')
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)
657
print("File upgraded successfully")
659
def __convert_0_9_0(self, keyring_password):
661
Convert keyring from the 0.9.0 and earlier format to the current
664
KEYRING_SETTING = 'keyring-setting'
665
CRYPTED_PASSWORD = 'crypted-password'
668
config = ConfigParser.RawConfigParser()
669
config.read(self.file_path)
670
config.get(KEYRING_SETTING, CRYPTED_PASSWORD)
674
print("Keyring from 0.9.0 or earlier detected. Upgrading...")
678
if keyring_password is None:
679
keyring_password = getpass.getpass(
680
"Please input your password for the keyring: ")
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")
687
self.keyring_key = keyring_password
688
config.remove_option(KEYRING_SETTING, CRYPTED_PASSWORD)
689
with open(self.file_path, 'w') as f:
691
self.set_password('keyring-setting', 'password reference',
692
'password reference value')
694
from Crypto.Cipher import AES
695
password = keyring_password + (
696
self.block_size - len(keyring_password) % self.block_size
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)
709
print("File upgraded successfully")
712
class Win32CryptoKeyring(BasicFileKeyring):
713
"""Win32 Cryptography Keyring"""
715
filename = 'wincrypto_pass.cfg'
718
super(Win32CryptoKeyring, self).__init__()
721
from backends import win32_crypto
722
self.crypt_handler = win32_crypto
724
self.crypt_handler = None
727
"""Recommended when other Windows backends are unavailable
729
recommended = select_windows_backend()
730
if recommended == None:
732
elif recommended == 'file':
737
def encrypt(self, password):
738
"""Encrypt the password using the CryptAPI.
740
return self.crypt_handler.encrypt(password)
742
def decrypt(self, password_encrypted):
743
"""Decrypt the password using the CryptAPI.
745
return self.crypt_handler.decrypt(password_encrypted)
748
class WinVaultKeyring(KeyringBackend):
750
WinVaultKeyring stores encrypted passwords using the Windows Credential
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:
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:
766
super(WinVaultKeyring, self).__init__()
770
self.win32cred = win32cred
771
self.pywintypes = pywintypes
773
self.win32cred = None
776
'''Default Windows backend, when it is available
778
recommended = select_windows_backend()
779
if recommended == None:
781
elif recommended == 'cred':
787
def _compound_name(username, service):
788
return u'%(username)s@%(service)s' % vars()
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))
798
blob = res['CredentialBlob']
799
return blob.decode('utf-16')
801
def _get_password(self, target):
803
res = self.win32cred.CredRead(
804
Type=self.win32cred.CRED_TYPE_GENERIC,
807
except self.pywintypes.error, e:
808
if e.winerror == 1168 and e.funcname == 'CredRead': # not found
813
def set_password(self, service, username, password):
814
existing_pw = self._get_password(service)
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))
823
def _set_password(self, target, username, password):
824
credential = dict(Type=self.win32cred.CRED_TYPE_GENERIC,
827
CredentialBlob=password,
828
Comment="Stored using python-keyring",
829
Persist=self.win32cred.CRED_PERSIST_ENTERPRISE)
830
self.win32cred.CredWrite(credential, 0)
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)
839
def _delete_password(self, target):
840
self.win32cred.CredDelete(
841
Type=self.win32cred.CRED_TYPE_GENERIC,
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
850
super(Win32CryptoRegistry, self).__init__()
853
from backends import win32_crypto
854
__import__('_winreg')
855
self.crypt_handler = win32_crypto
857
self.crypt_handler = None
860
"""Return if this keyring supports current enviroment.
865
recommended = select_windows_backend()
866
if recommended == None:
868
elif recommended == 'reg':
873
def get_password(self, service, username):
874
"""Get password of the username for the service
876
from _winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx
879
key = r'Software\%s\Keyring' % service
880
hkey = OpenKey(HKEY_CURRENT_USER, key)
881
password_base64 = QueryValueEx(hkey, username)[0]
883
password_encrypted = base64.decodestring(password_base64)
884
# decrypted the password
885
password = self.crypt_handler.decrypt(password_encrypted)
886
except EnvironmentError:
890
def set_password(self, service, username, password):
891
"""Write the password to the registry
893
# encrypt the password
894
password_encrypted = self.crypt_handler.encrypt(password)
896
password_base64 = base64.encodestring(password_encrypted)
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)
903
def select_windows_backend():
906
major, minor, build, platform, text = sys.getwindowsversion()
908
__import__('pywintypes')
909
__import__('win32cred')
910
if (major, minor) >= (5, 1):
911
# recommend for windows xp+
916
__import__('keyring.backends.win32_crypto')
917
__import__('_winreg')
918
if (major, minor) >= (5, 0):
919
# recommend for windows 2k+
924
__import__('keyring.backends.win32_crypto')
933
def get_all_keyring():
934
"""Return the list of all keyrings in the lib
937
if _all_keyring is None:
938
_all_keyring = [OSXKeychain(), GnomeKeyring(), KDEKWallet(),
939
CryptedFileKeyring(), UncryptedFileKeyring(),
940
Win32CryptoKeyring(), Win32CryptoRegistry(),
941
WinVaultKeyring(), SecretServiceKeyring()]