File: Windows.py

package info (click to toggle)
python-keyring 25.7.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 552 kB
  • sloc: python: 1,923; sh: 17; makefile: 17
file content (168 lines) | stat: -rw-r--r-- 5,727 bytes parent folder | download | duplicates (2)
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)