/
Windows.py
170 lines (144 loc) · 5.67 KB
/
Windows.py
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
169
170
import logging
from .._compat import properties
from ..backend import KeyringBackend
from ..credentials import SimpleCredential
from ..errors import PasswordDeleteError, ExceptionRaisedContext
with ExceptionRaisedContext() as missing_deps:
try:
# prefer pywin32-ctypes
from win32ctypes.pywin32 import pywintypes
from win32ctypes.pywin32 import win32cred
# force demand import to raise ImportError
win32cred.__name__
except ImportError:
# fallback to pywin32
import pywintypes
import win32cred
# force demand import to raise ImportError
win32cred.__name__
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)
setattr(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 an 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):
"""
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):
# first attempt to get the password under the service name
res = self._get_password(service)
if not res or res['UserName'] != username:
# It wasn't found so attempt to get it with the compound name
res = self._get_password(self._compound_name(username, service))
if not res:
return None
return res.value
def _get_password(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._get_password(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._get_password(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 = None
# get the credentials associated with the provided username
if username:
res = self._get_password(self._compound_name(username, service))
# get any first password under the service name
if not res:
res = self._get_password(service)
if not res:
return None
return SimpleCredential(res['UserName'], res.value)