Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9ab3fa3
Add KeyChain Cache
marstr May 16, 2019
156ee52
Use **kwargs in OSXTokenCache.find
marstr May 22, 2019
19480d7
Fixing pylint errors
marstr May 24, 2019
901da7f
Use CrossPlatLock in OSX Cache
marstr May 29, 2019
0646254
Adding partially implemented cache MUX
marstr May 30, 2019
bc60103
Overload modify instead of more specific commands
marstr May 30, 2019
f8f61bd
Squash warnings about duplicate code
marstr May 30, 2019
b8f3f3f
Fix pylint error about unnecessary `elif` statements.
marstr May 30, 2019
8998773
Fixing line formatting err from pylint
marstr May 30, 2019
3b124dc
Add missing doc-strings as noted by pylint
marstr May 30, 2019
bfdfa1b
Revert "Squash warnings about duplicate code"
marstr May 31, 2019
6c630c8
Pull duplicated code into single unprotected SerializableTokenCache i…
marstr May 31, 2019
3ccd530
Move all token cache implementations into .token_cache
marstr May 31, 2019
602f5d0
Fixing formatting nit
marstr May 31, 2019
8c0b870
Mux on generic form of FileTokenCache as fallback plan
marstr May 31, 2019
5e19795
Removing redundant test
marstr May 31, 2019
2ce8774
Fix documentation typo
marstr Jun 3, 2019
d3059c3
Making directory creation call in _mkdir_p recursive.
marstr Jun 10, 2019
1dff899
Restructuring what occurs in _read/_write and the other modifiers
marstr Jun 10, 2019
0327141
Fix kwargs use in constructors
marstr Jun 10, 2019
9c59a72
Fix invalid/inaccurate file mode for agnostic backend
marstr Jun 10, 2019
9f459fb
Add test for plain file token cache.
marstr Jun 10, 2019
47e985c
Moving error handling out of _read and _write.
marstr Jun 10, 2019
db40f1b
Suppress pylint error caused by difference in parameters, which is do…
marstr Jun 10, 2019
e6009cd
Removing duplicated line
marstr Jun 21, 2019
2962bd8
Brining all Keychain Error constructors into the same shape.
marstr Jun 21, 2019
469da00
Don't defer failure of OS module loads
marstr Jun 21, 2019
3334e2c
Remove child error-types
marstr Jun 24, 2019
a68ba24
Do token cache switching at import time instead of having a separate …
marstr Jun 25, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions msal_extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
"""Provides auxiliary functionality to the `msal` package."""
__version__ = "0.0.1"

import sys

if sys.platform.startswith('win'):
from .token_cache import WindowsTokenCache as TokenCache
elif sys.platform.startswith('darwin'):
from .token_cache import OSXTokenCache as TokenCache
else:
from .token_cache import UnencryptedTokenCache as TokenCache
253 changes: 253 additions & 0 deletions msal_extensions/osx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# pylint: disable=duplicate-code

"""Implements a macOS specific TokenCache, and provides auxiliary helper types."""

import os
import ctypes as _ctypes

OS_RESULT = _ctypes.c_int32


class KeychainError(OSError):
"""The RuntimeError that will be run when a function interacting with Keychain fails."""

ACCESS_DENIED = -128
NO_SUCH_KEYCHAIN = -25294
NO_DEFAULT = -25307
ITEM_NOT_FOUND = -25300

def __init__(self, exit_status):
super(KeychainError, self).__init__()
self.exit_status = exit_status
# TODO: pylint: disable=fixme
# use SecCopyErrorMessageString to fetch the appropriate message here.
self.message = \
'{} ' \
'see https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/MacErrors.h'\
.format(self.exit_status)

def _get_native_location(name):
# type: (str) -> str
"""
Fetches the location of a native MacOS library.
:param name: The name of the library to be loaded.
:return: The location of the library on a MacOS filesystem.
"""
return '/System/Library/Frameworks/{0}.framework/{0}'.format(name)


# Load native MacOS libraries
_SECURITY = _ctypes.CDLL(_get_native_location('Security'))
_CORE = _ctypes.CDLL(_get_native_location('CoreFoundation'))


# Bind CFRelease from native MacOS libraries.
_CORE_RELEASE = _CORE.CFRelease
_CORE_RELEASE.argtypes = (
_ctypes.c_void_p,
)

# Bind SecCopyErrorMessageString from native MacOS libraries.
# https://developer.apple.com/documentation/security/1394686-seccopyerrormessagestring?language=objc
_SECURITY_COPY_ERROR_MESSAGE_STRING = _SECURITY.SecCopyErrorMessageString
_SECURITY_COPY_ERROR_MESSAGE_STRING.argtypes = (
OS_RESULT,
_ctypes.c_void_p
)
_SECURITY_COPY_ERROR_MESSAGE_STRING.restype = _ctypes.c_char_p

# Bind SecKeychainOpen from native MacOS libraries.
# https://developer.apple.com/documentation/security/1396431-seckeychainopen
_SECURITY_KEYCHAIN_OPEN = _SECURITY.SecKeychainOpen
_SECURITY_KEYCHAIN_OPEN.argtypes = (
_ctypes.c_char_p,
_ctypes.POINTER(_ctypes.c_void_p)
)
_SECURITY_KEYCHAIN_OPEN.restype = OS_RESULT

# Bind SecKeychainCopyDefault from native MacOS libraries.
# https://developer.apple.com/documentation/security/1400743-seckeychaincopydefault?language=objc
_SECURITY_KEYCHAIN_COPY_DEFAULT = _SECURITY.SecKeychainCopyDefault
_SECURITY_KEYCHAIN_COPY_DEFAULT.argtypes = (
_ctypes.POINTER(_ctypes.c_void_p),
)
_SECURITY_KEYCHAIN_COPY_DEFAULT.restype = OS_RESULT


# Bind SecKeychainItemFreeContent from native MacOS libraries.
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT = _SECURITY.SecKeychainItemFreeContent
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.argtypes = (
_ctypes.c_void_p,
_ctypes.c_void_p,
)
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT.restype = OS_RESULT

# Bind SecKeychainItemModifyAttributesAndData from native MacOS libraries.
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA = \
_SECURITY.SecKeychainItemModifyAttributesAndData
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.argtypes = (
_ctypes.c_void_p,
_ctypes.c_void_p,
_ctypes.c_uint32,
_ctypes.c_void_p,
)
_SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA.restype = OS_RESULT

# Bind SecKeychainFindGenericPassword from native MacOS libraries.
# https://developer.apple.com/documentation/security/1397301-seckeychainfindgenericpassword?language=objc
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD = _SECURITY.SecKeychainFindGenericPassword
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.argtypes = (
_ctypes.c_void_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.POINTER(_ctypes.c_uint32),
_ctypes.POINTER(_ctypes.c_void_p),
_ctypes.POINTER(_ctypes.c_void_p),
)
_SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD.restype = OS_RESULT
# Bind SecKeychainAddGenericPassword from native MacOS
# https://developer.apple.com/documentation/security/1398366-seckeychainaddgenericpassword?language=objc
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD = _SECURITY.SecKeychainAddGenericPassword
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.argtypes = (
_ctypes.c_void_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.c_uint32,
_ctypes.c_char_p,
_ctypes.POINTER(_ctypes.c_void_p),
)
_SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD.restype = OS_RESULT


class Keychain(object):
"""Encapsulates the interactions with a particular MacOS Keychain."""
def __init__(self, filename=None):
# type: (str) -> None
self._ref = _ctypes.c_void_p()

if filename:
filename = os.path.expanduser(filename)
self._filename = filename.encode('utf-8')
else:
self._filename = None

def __enter__(self):
if self._filename:
status = _SECURITY_KEYCHAIN_OPEN(self._filename, self._ref)
else:
status = _SECURITY_KEYCHAIN_COPY_DEFAULT(self._ref)

if status:
raise OSError(status)
return self

def __exit__(self, *args):
if self._ref:
_CORE_RELEASE(self._ref)

def get_generic_password(self, service, account_name):
# type: (str, str) -> str
"""Fetch the password associated with a particular service and account.

:param service: The service that this password is associated with.
:param account_name: The account that this password is associated with.
:return: The value of the password associated with the specified service and account.
"""
service = service.encode('utf-8')
account_name = account_name.encode('utf-8')

length = _ctypes.c_uint32()
contents = _ctypes.c_void_p()
exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD(
self._ref,
len(service),
service,
len(account_name),
account_name,
length,
contents,
None,
)

if exit_status:
raise KeychainError(exit_status=exit_status)

value = _ctypes.create_string_buffer(length.value)
_ctypes.memmove(value, contents.value, length.value)
_SECURITY_KEYCHAIN_ITEM_FREE_CONTENT(None, contents)
return value.raw.decode('utf-8')

def set_generic_password(self, service, account_name, value):
# type: (str, str, str) -> None
"""Associate a password with a given service and account.

:param service: The service to associate this password with.
:param account_name: The account to associate this password with.
:param value: The string that should be used as the password.
"""
service = service.encode('utf-8')
account_name = account_name.encode('utf-8')
value = value.encode('utf-8')

entry = _ctypes.c_void_p()
find_exit_status = _SECURITY_KEYCHAIN_FIND_GENERIC_PASSWORD(
self._ref,
len(service),
service,
len(account_name),
account_name,
None,
None,
entry,
)

if not find_exit_status:
modify_exit_status = _SECURITY_KEYCHAIN_ITEM_MODIFY_ATTRIBUTES_AND_DATA(
entry,
None,
len(value),
value,
)
if modify_exit_status:
raise KeychainError(exit_status=modify_exit_status)

elif find_exit_status == KeychainError.ITEM_NOT_FOUND:
add_exit_status = _SECURITY_KEYCHAIN_ADD_GENERIC_PASSWORD(
self._ref,
len(service),
service,
len(account_name),
account_name,
len(value),
value,
None
)

if add_exit_status:
raise KeychainError(exit_status=add_exit_status)
else:
raise KeychainError(exit_status=find_exit_status)

def get_internet_password(self, service, username):
# type: (str, str) -> str
""" Fetches a password associated with a domain and username.
NOTE: THIS IS NOT YET IMPLEMENTED
:param service: The website/service that this password is associated with.
:param username: The account that this password is associated with.
:return: The password that was associated with the given service and username.
"""
raise NotImplementedError()

def set_internet_password(self, service, username, value):
# type: (str, str, str) -> None
"""Sets a password associated with a domain and a username.
NOTE: THIS IS NOT YET IMPLEMENTED
:param service: The website/service that this password is associated with.
:param username: The account that this password is associated with.
:param value: The password that should be associated with the given service and username.
"""
raise NotImplementedError()
Loading