1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
|
from __future__ import annotations
import logging
from jaraco.context import ExceptionTrap
from ..backend import KeyringBackend
from ..compat import properties
from ..credentials import SimpleCredential
from ..errors import PasswordDeleteError
with ExceptionTrap() as missing_deps:
try:
# prefer pywin32-ctypes
from win32ctypes.pywin32 import pywintypes, win32cred
# force demand import to raise ImportError
win32cred.__name__ # noqa: B018
except ImportError:
# fallback to pywin32
import pywintypes
import win32cred
# force demand import to raise ImportError
win32cred.__name__ # noqa: B018
log = logging.getLogger(__name__)
class Persistence:
def __get__(self, keyring, type=None):
return getattr(keyring, '_persist', win32cred.CRED_PERSIST_ENTERPRISE)
def __set__(self, keyring, value):
"""
Set the persistence value on the Keyring. Value may be
one of the win32cred.CRED_PERSIST_* constants or a
string representing one of those constants. For example,
'local machine' or 'session'.
"""
if isinstance(value, str):
attr = 'CRED_PERSIST_' + value.replace(' ', '_').upper()
value = getattr(win32cred, attr)
keyring._persist = value
class DecodingCredential(dict):
@property
def value(self):
"""
Attempt to decode the credential blob as UTF-16 then UTF-8.
"""
cred = self['CredentialBlob']
try:
return cred.decode('utf-16')
except UnicodeDecodeError:
decoded_cred_utf8 = cred.decode('utf-8')
log.warning(
"Retrieved a UTF-8 encoded credential. Please be aware that "
"this library only writes credentials in UTF-16."
)
return decoded_cred_utf8
class WinVaultKeyring(KeyringBackend):
"""
WinVaultKeyring stores encrypted passwords using the Windows Credential
Manager.
Requires pywin32
This backend does some gymnastics to simulate multi-user support,
which WinVault doesn't support natively. See
https://github.com/jaraco/keyring/issues/47#issuecomment-75763152
for details on the implementation, but here's the gist:
Passwords are stored under the service name unless there is a collision
(another password with the same service name but different user name),
in which case the previous password is moved into a compound name:
{username}@{service}
"""
persist = Persistence()
@properties.classproperty
def priority(cls) -> float:
"""
If available, the preferred backend on Windows.
"""
if missing_deps:
raise RuntimeError("Requires Windows and pywin32")
return 5
@staticmethod
def _compound_name(username, service):
return f'{username}@{service}'
def get_password(self, service, username):
res = self._resolve_credential(service, username)
return res and res.value
def _resolve_credential(
self, service: str, username: str | None
) -> DecodingCredential | None:
# first attempt to get the password under the service name
res = self._read_credential(service)
if not res or username and res['UserName'] != username:
# It wasn't found so attempt to get it with the compound name
res = self._read_credential(self._compound_name(username, service))
return res
def _read_credential(self, target):
try:
res = win32cred.CredRead(
Type=win32cred.CRED_TYPE_GENERIC, TargetName=target
)
except pywintypes.error as e:
if e.winerror == 1168 and e.funcname == 'CredRead': # not found
return None
raise
return DecodingCredential(res)
def set_password(self, service, username, password):
existing_pw = self._read_credential(service)
if existing_pw:
# resave the existing password using a compound target
existing_username = existing_pw['UserName']
target = self._compound_name(existing_username, service)
self._set_password(
target,
existing_username,
existing_pw.value,
)
self._set_password(service, username, str(password))
def _set_password(self, target, username, password):
credential = dict(
Type=win32cred.CRED_TYPE_GENERIC,
TargetName=target,
UserName=username,
CredentialBlob=password,
Comment="Stored using python-keyring",
Persist=self.persist,
)
win32cred.CredWrite(credential, 0)
def delete_password(self, service, username):
compound = self._compound_name(username, service)
deleted = False
for target in service, compound:
existing_pw = self._read_credential(target)
if existing_pw and existing_pw['UserName'] == username:
deleted = True
self._delete_password(target)
if not deleted:
raise PasswordDeleteError(service)
def _delete_password(self, target):
try:
win32cred.CredDelete(Type=win32cred.CRED_TYPE_GENERIC, TargetName=target)
except pywintypes.error as e:
if e.winerror == 1168 and e.funcname == 'CredDelete': # not found
return
raise
def get_credential(self, service, username):
res = self._resolve_credential(service, username)
return res and SimpleCredential(res['UserName'], res.value)
|