initial commit

This commit is contained in:
Davidson Gomes
2024-10-30 11:19:09 -03:00
commit 8654a31a4d
3744 changed files with 585542 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
import functools
import os
import platform
import warnings
from ...backend import KeyringBackend
from ...compat import properties
from ...errors import KeyringError, KeyringLocked, PasswordDeleteError, PasswordSetError
try:
from . import api
except Exception:
pass
def warn_keychain(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
if self.keychain:
warnings.warn("Specified keychain is ignored. See #623", stacklevel=2)
return func(self, *args, **kwargs)
return wrapper
class Keyring(KeyringBackend):
"""macOS Keychain"""
keychain = os.environ.get('KEYCHAIN_PATH')
"Path to keychain file, overriding default"
@properties.classproperty
def priority(cls):
"""
Preferred for all macOS environments.
"""
if platform.system() != 'Darwin':
raise RuntimeError("macOS required")
if 'api' not in globals():
raise RuntimeError("Security API unavailable")
return 5
@warn_keychain
def set_password(self, service, username, password):
if username is None:
username = ''
try:
api.set_generic_password(self.keychain, service, username, password)
except api.KeychainDenied as e:
raise KeyringLocked(f"Can't store password on keychain: {e}") from e
except api.Error as e:
raise PasswordSetError(f"Can't store password on keychain: {e}") from e
@warn_keychain
def get_password(self, service, username):
if username is None:
username = ''
try:
return api.find_generic_password(self.keychain, service, username)
except api.NotFound:
pass
except api.KeychainDenied as e:
raise KeyringLocked(f"Can't get password from keychain: {e}") from e
except api.Error as e:
raise KeyringError(f"Can't get password from keychain: {e}") from e
@warn_keychain
def delete_password(self, service, username):
if username is None:
username = ''
try:
return api.delete_generic_password(self.keychain, service, username)
except api.Error as e:
raise PasswordDeleteError(f"Can't delete password in keychain: {e}") from e
def with_keychain(self, keychain):
warnings.warn(
"macOS.Keyring.with_keychain is deprecated. Use with_properties instead.",
DeprecationWarning,
stacklevel=2,
)
return self.with_properties(keychain=keychain)

View File

@@ -0,0 +1,184 @@
from __future__ import annotations
import contextlib
import ctypes
import functools
from ctypes import (
byref,
c_int32,
c_uint32,
c_void_p,
)
from ctypes.util import find_library
OS_status = c_int32
class error:
item_not_found = -25300
keychain_denied = -128
sec_auth_failed = -25293
plist_missing = -67030
sec_interaction_not_allowed = -25308
_sec = ctypes.CDLL(find_library('Security'))
_core = ctypes.CDLL(find_library('CoreServices'))
_found = ctypes.CDLL(find_library('Foundation'))
CFDictionaryCreate = _found.CFDictionaryCreate
CFDictionaryCreate.restype = c_void_p
CFDictionaryCreate.argtypes = (
c_void_p,
c_void_p,
c_void_p,
c_int32,
c_void_p,
c_void_p,
)
CFStringCreateWithCString = _found.CFStringCreateWithCString
CFStringCreateWithCString.restype = c_void_p
CFStringCreateWithCString.argtypes = [c_void_p, c_void_p, c_uint32]
CFNumberCreate = _found.CFNumberCreate
CFNumberCreate.restype = c_void_p
CFNumberCreate.argtypes = [c_void_p, c_uint32, ctypes.c_void_p]
SecItemAdd = _sec.SecItemAdd
SecItemAdd.restype = OS_status
SecItemAdd.argtypes = (c_void_p, c_void_p)
SecItemCopyMatching = _sec.SecItemCopyMatching
SecItemCopyMatching.restype = OS_status
SecItemCopyMatching.argtypes = (c_void_p, c_void_p)
SecItemDelete = _sec.SecItemDelete
SecItemDelete.restype = OS_status
SecItemDelete.argtypes = (c_void_p,)
CFDataGetBytePtr = _found.CFDataGetBytePtr
CFDataGetBytePtr.restype = c_void_p
CFDataGetBytePtr.argtypes = (c_void_p,)
CFDataGetLength = _found.CFDataGetLength
CFDataGetLength.restype = c_int32
CFDataGetLength.argtypes = (c_void_p,)
def k_(s):
return c_void_p.in_dll(_sec, s)
@functools.singledispatch
def create_cf(ob):
return ob
# explicit bool and int required for Python 3.10 compatibility
@create_cf.register(bool)
@create_cf.register(int)
def _(val: bool | int):
if val.bit_length() > 31:
raise OverflowError(val)
int32 = 0x9
return CFNumberCreate(None, int32, ctypes.byref(c_int32(val)))
@create_cf.register
def _(s: str):
kCFStringEncodingUTF8 = 0x08000100
return CFStringCreateWithCString(None, s.encode('utf8'), kCFStringEncodingUTF8)
def create_query(**kwargs):
return CFDictionaryCreate(
None,
(c_void_p * len(kwargs))(*map(k_, kwargs.keys())),
(c_void_p * len(kwargs))(*map(create_cf, kwargs.values())),
len(kwargs),
_found.kCFTypeDictionaryKeyCallBacks,
_found.kCFTypeDictionaryValueCallBacks,
)
def cfstr_to_str(data):
return ctypes.string_at(CFDataGetBytePtr(data), CFDataGetLength(data)).decode(
'utf-8'
)
class Error(Exception):
@classmethod
def raise_for_status(cls, status):
if status == 0:
return
if status == error.item_not_found:
raise NotFound(status, "Item not found")
if status == error.keychain_denied:
raise KeychainDenied(status, "Keychain Access Denied")
if status == error.sec_auth_failed or status == error.plist_missing:
raise SecAuthFailure(
status,
"Security Auth Failure: make sure "
"executable is signed with codesign util",
)
raise cls(status, "Unknown Error")
class NotFound(Error):
pass
class KeychainDenied(Error):
pass
class SecAuthFailure(Error):
pass
def find_generic_password(kc_name, service, username, not_found_ok=False):
q = create_query(
kSecClass=k_('kSecClassGenericPassword'),
kSecMatchLimit=k_('kSecMatchLimitOne'),
kSecAttrService=service,
kSecAttrAccount=username,
kSecReturnData=True,
)
data = c_void_p()
status = SecItemCopyMatching(q, byref(data))
if status == error.item_not_found and not_found_ok:
return
Error.raise_for_status(status)
return cfstr_to_str(data)
def set_generic_password(name, service, username, password):
with contextlib.suppress(NotFound):
delete_generic_password(name, service, username)
q = create_query(
kSecClass=k_('kSecClassGenericPassword'),
kSecAttrService=service,
kSecAttrAccount=username,
kSecValueData=password,
)
status = SecItemAdd(q, None)
Error.raise_for_status(status)
def delete_generic_password(name, service, username):
q = create_query(
kSecClass=k_('kSecClassGenericPassword'),
kSecAttrService=service,
kSecAttrAccount=username,
)
status = SecItemDelete(q)
Error.raise_for_status(status)