Skip to content

Instantly share code, notes, and snippets.

@gudezhi
Forked from zrzka/security.py
Created July 20, 2018 09:23
Show Gist options
  • Save gudezhi/ace8631b8dcf4dc0d7e6ae0af211fae1 to your computer and use it in GitHub Desktop.
Save gudezhi/ace8631b8dcf4dc0d7e6ae0af211fae1 to your computer and use it in GitHub Desktop.
iOS Keychain for Pythonista (WIP)
#!python3
from ctypes import c_int, c_void_p, POINTER, byref, c_ulong
from objc_util import (
load_framework, c, ns, ObjCInstance, nsdata_to_bytes, NSString, NSData, NSNumber,
ObjCClass, NSArray, NSDictionary
)
from enum import Enum, IntFlag
from typing import Union
import datetime
from os.path import basename
__all__ = [
'get_password', 'set_password', 'delete_password', 'get_services',
'KeychainError', 'KeychainDuplicateItemError', 'KeychainItemNotFoundError',
'KeychainAuthFailedError', 'KeychainUserCanceledError', 'KeychainUserInteractionNotAllowedError',
'KeychainParamError', 'KeychainUnhandledError',
'ItemClass', 'AuthenticationPolicy', 'Accessibility', 'AuthenticationUI', 'AccessControl',
'GenericPassword', 'GenericPasswordAttributes'
]
#
# Core Foundation
#
# Memory management rules
# https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/CFMemoryMgmt.html
#
# Toll-free bridged types - we're not forced to play with CFDictionaryCreate - we can use ns(dict) -> NSDictionary directlyy
# https://developer.apple.com/library/content/documentation/CoreFoundation/Conceptual/CFDesignConcepts/Articles/tollFreeBridgedTypes.html
#
load_framework('Security')
NSDate = ObjCClass('NSDate')
def _from_nsstring(obj):
return obj.UTF8String().decode()
def _from_nsnumber(obj): # noqa: C901
ctype = obj.objCType()
if ctype == b'c':
return obj.charValue()
elif ctype == b's':
return obj.shortValue()
elif ctype == b'i':
return obj.intValue()
elif ctype == b'l':
return obj.longValue()
elif ctype == b'q':
return obj.longLongValue()
elif ctype == b'C':
return obj.unsignedCharValue()
elif ctype == b'S':
return obj.unsignedShortValue()
elif ctype == b'I':
return obj.unsignedIntValue()
elif ctype == b'L':
return obj.unsignedLongValue()
elif ctype == b'Q':
return obj.unsignedLongLongValue()
elif ctype == b'f':
return obj.floatValue()
elif ctype == b'd':
return obj.doubleValue()
elif ctype == b'B':
return obj.boolValue()
raise ValueError(f'Unsupported objCType value {ctype}')
def _from_nsdata(obj):
return nsdata_to_bytes(obj)
def _from_nsdate(obj):
return datetime.datetime.fromtimestamp(obj.timeIntervalSince1970())
def from_ns(obj):
if obj.isKindOfClass_(NSString):
return _from_nsstring(obj)
elif obj.isKindOfClass_(NSNumber):
return _from_nsnumber(obj)
elif obj.isKindOfClass_(NSData):
return _from_nsdata(obj)
elif obj.isKindOfClass_(NSDate):
return _from_nsdate(obj)
elif obj.isKindOfClass_(NSArray):
return [from_ns(obj.objectAtIndex_(i) for i in range(obj.count()))]
elif obj.isKindOfClass_(NSDictionary):
return {from_ns(k): from_ns(obj.objectForKey_(k)) for k in obj.allKeys()}
print(type(obj))
return obj
def _symbol_ptr(name):
return c_void_p.in_dll(c, name)
def _str_symbol(name):
return ObjCInstance(_symbol_ptr(name)).UTF8String().decode()
#
# kSec* constants
#
# [TODO] Check if there's a way how to get them via from_address, ... because in_dll works for these symbols
# [TODO] Add other constants
#
# https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_class_keys_and_values?language=objc
kSecClass = _str_symbol('kSecClass')
kSecClassGenericPassword = _str_symbol('kSecClassGenericPassword')
kSecClassInternetPassword = _str_symbol('kSecClassInternetPassword')
# https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_attribute_keys_and_values
# General Item Attribute Keys
kSecAttrAccessControl = _str_symbol('kSecAttrAccessControl')
kSecAttrAccessible = _str_symbol('kSecAttrAccessible')
kSecAttrAccessGroup = _str_symbol('kSecAttrAccessGroup')
kSecAttrSynchronizable = _str_symbol('kSecAttrSynchronizable')
kSecAttrCreationDate = _str_symbol('kSecAttrCreationDate')
kSecAttrModificationDate = _str_symbol('kSecAttrModificationDate')
kSecAttrDescription = _str_symbol('kSecAttrDescription')
kSecAttrComment = _str_symbol('kSecAttrComment')
kSecAttrCreator = _str_symbol('kSecAttrCreator')
kSecAttrType = _str_symbol('kSecAttrType')
kSecAttrLabel = _str_symbol('kSecAttrLabel')
kSecAttrIsInvisible = _str_symbol('kSecAttrIsInvisible')
kSecAttrIsNegative = _str_symbol('kSecAttrIsNegative')
kSecAttrSyncViewHint = _str_symbol('kSecAttrSyncViewHint')
# Password Attribute Keys (generic & internet password)
kSecAttrAccount = _str_symbol('kSecAttrAccount')
# Password Attribute Keys (generic password only)
kSecAttrService = _str_symbol('kSecAttrService')
kSecAttrGeneric = _str_symbol('kSecAttrGeneric')
# Password Attribute Keys (internet password only)
kSecAttrSecurityDomain = _str_symbol('kSecAttrSecurityDomain')
kSecAttrServer = _str_symbol('kSecAttrServer')
kSecAttrProtocol = _str_symbol('kSecAttrProtocol')
kSecAttrAuthenticationType = _str_symbol('kSecAttrAuthenticationType')
kSecAttrPort = _str_symbol('kSecAttrPort')
kSecAttrPath = _str_symbol('kSecAttrPath')
# kSecAttrProtocol values
kSecAttrProtocolFTP = _str_symbol('kSecAttrProtocolFTP')
kSecAttrProtocolFTPAccount = _str_symbol('kSecAttrProtocolFTPAccount')
kSecAttrProtocolHTTP = _str_symbol('kSecAttrProtocolHTTP')
kSecAttrProtocolIRC = _str_symbol('kSecAttrProtocolIRC')
kSecAttrProtocolNNTP = _str_symbol('kSecAttrProtocolNNTP')
kSecAttrProtocolPOP3 = _str_symbol('kSecAttrProtocolPOP3')
kSecAttrProtocolSMTP = _str_symbol('kSecAttrProtocolSMTP')
kSecAttrProtocolSOCKS = _str_symbol('kSecAttrProtocolSOCKS')
kSecAttrProtocolIMAP = _str_symbol('kSecAttrProtocolIMAP')
kSecAttrProtocolLDAP = _str_symbol('kSecAttrProtocolLDAP')
kSecAttrProtocolAppleTalk = _str_symbol('kSecAttrProtocolAppleTalk')
kSecAttrProtocolAFP = _str_symbol('kSecAttrProtocolAFP')
kSecAttrProtocolTelnet = _str_symbol('kSecAttrProtocolTelnet')
kSecAttrProtocolSSH = _str_symbol('kSecAttrProtocolSSH')
kSecAttrProtocolFTPS = _str_symbol('kSecAttrProtocolFTPS')
kSecAttrProtocolHTTPS = _str_symbol('kSecAttrProtocolHTTPS')
kSecAttrProtocolHTTPProxy = _str_symbol('kSecAttrProtocolHTTPProxy')
kSecAttrProtocolHTTPSProxy = _str_symbol('kSecAttrProtocolHTTPSProxy')
kSecAttrProtocolFTPProxy = _str_symbol('kSecAttrProtocolFTPProxy')
kSecAttrProtocolSMB = _str_symbol('kSecAttrProtocolSMB')
kSecAttrProtocolRTSP = _str_symbol('kSecAttrProtocolRTSP')
kSecAttrProtocolRTSPProxy = _str_symbol('kSecAttrProtocolRTSPProxy')
kSecAttrProtocolDAAP = _str_symbol('kSecAttrProtocolDAAP')
kSecAttrProtocolEPPC = _str_symbol('kSecAttrProtocolEPPC')
kSecAttrProtocolIPP = _str_symbol('kSecAttrProtocolIPP')
kSecAttrProtocolNNTPS = _str_symbol('kSecAttrProtocolNNTPS')
kSecAttrProtocolLDAPS = _str_symbol('kSecAttrProtocolLDAPS')
kSecAttrProtocolTelnetS = _str_symbol('kSecAttrProtocolTelnetS')
kSecAttrProtocolIMAPS = _str_symbol('kSecAttrProtocolIMAPS')
kSecAttrProtocolIRCS = _str_symbol('kSecAttrProtocolIRCS')
kSecAttrProtocolPOP3S = _str_symbol('kSecAttrProtocolPOP3S')
# kSecAttrAuthenticationType values
kSecAttrAuthenticationTypeNTLM = _str_symbol('kSecAttrAuthenticationTypeNTLM')
kSecAttrAuthenticationTypeMSN = _str_symbol('kSecAttrAuthenticationTypeMSN')
kSecAttrAuthenticationTypeDPA = _str_symbol('kSecAttrAuthenticationTypeDPA')
kSecAttrAuthenticationTypeRPA = _str_symbol('kSecAttrAuthenticationTypeRPA')
kSecAttrAuthenticationTypeHTTPBasic = _str_symbol('kSecAttrAuthenticationTypeHTTPBasic')
kSecAttrAuthenticationTypeHTTPDigest = _str_symbol('kSecAttrAuthenticationTypeHTTPDigest')
kSecAttrAuthenticationTypeHTMLForm = _str_symbol('kSecAttrAuthenticationTypeHTMLForm')
kSecAttrAuthenticationTypeDefault = _str_symbol('kSecAttrAuthenticationTypeDefault')
# https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_return_result_keys?language=objc
kSecReturnData = _str_symbol('kSecReturnData')
kSecReturnAttributes = _str_symbol('kSecReturnAttributes')
kSecReturnRef = _str_symbol('kSecReturnRef')
kSecReturnPersistentRef = _str_symbol('kSecReturnPersistentRef')
# https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_return_result_keys?language=objc
kSecValueData = _str_symbol('kSecValueData')
kSecValueRef = _str_symbol('kSecValueRef')
kSecValuePersistentRef = _str_symbol('kSecValuePersistentRef')
# https://developer.apple.com/documentation/security/keychain_services/keychain_items/search_attribute_keys_and_values?language=objc
kSecMatchLimit = _str_symbol('kSecMatchLimit')
kSecMatchLimitAll = _str_symbol('kSecMatchLimitAll')
kSecMatchLimitOne = _str_symbol('kSecMatchLimitOne')
kSecMatchCaseInsensitive = _str_symbol('kSecMatchCaseInsensitive')
# https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_attribute_keys_and_values#1679100?language=objc
kSecAttrAccessibleAlways = _str_symbol('kSecAttrAccessibleAlways')
kSecAttrAccessibleAlwaysThisDeviceOnly = _str_symbol('kSecAttrAccessibleAlwaysThisDeviceOnly')
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly = _str_symbol('kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly')
kSecAttrAccessibleAfterFirstUnlock = _str_symbol('kSecAttrAccessibleAfterFirstUnlock')
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly = _str_symbol('kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly')
kSecAttrAccessibleWhenUnlocked = _str_symbol('kSecAttrAccessibleWhenUnlocked')
kSecAttrAccessibleWhenUnlockedThisDeviceOnly = _str_symbol('kSecAttrAccessibleWhenUnlockedThisDeviceOnly')
# https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontroluserpresence
kSecAccessControlUserPresence = 1 << 0
kSecAccessControlTouchIDAny = 1 << 1
kSecAccessControlTouchIDCurrentSet = 1 << 3
kSecAccessControlDevicePasscode = 1 << 4
kSecAccessControlOr = 1 << 14
kSecAccessControlAnd = 1 << 15
kSecAccessControlPrivateKeyUsage = 1 << 30
kSecAccessControlApplicationPassword = 1 << 31
# https://developer.apple.com/documentation/security/ksecuseauthenticationuiallow?language=objc
kSecUseAuthenticationUI = _str_symbol('kSecUseAuthenticationUI')
kSecUseAuthenticationUIAllow = _str_symbol('kSecUseAuthenticationUIAllow')
kSecUseAuthenticationUIFail = _str_symbol('kSecUseAuthenticationUIFail')
kSecUseAuthenticationUISkip = _str_symbol('kSecUseAuthenticationUISkip')
kSecUseOperationPrompt = _str_symbol('kSecUseOperationPrompt')
#
# Security framework functions
#
CFTypeRef = c_void_p
CFDictionaryRef = c_void_p
SecAccessControlRef = c_void_p
CFErrorRef = c_void_p
CFAllocatorRef = c_void_p
# void CFRelease(CFTypeRef cf)
# https://developer.apple.com/documentation/corefoundation/1521153-cfrelease
CFRelease = c.CFRelease
CFRelease.restype = None
CFRelease.argtypes = [CFTypeRef]
# OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef _Nullable *result);
# https://developer.apple.com/documentation/security/1401659-secitemadd?language=objc
SecItemAdd = c.SecItemAdd
SecItemAdd.restype = c_int
SecItemAdd.argtypes = [CFDictionaryRef, POINTER(CFTypeRef)]
# OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
# https://developer.apple.com/documentation/security/1393617-secitemupdate?language=objc
SecItemUpdate = c.SecItemUpdate
SecItemUpdate.restype = c_int
SecItemUpdate.argtypes = [CFDictionaryRef, CFDictionaryRef]
# OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef _Nullable *result);
# https://developer.apple.com/documentation/security/1398306-secitemcopymatching?language=objc
SecItemCopyMatching = c.SecItemCopyMatching
SecItemCopyMatching.restype = c_int
SecItemCopyMatching.argtypes = [CFDictionaryRef, POINTER(CFTypeRef)]
# OSStatus SecItemDelete(CFDictionaryRef query);
# https://developer.apple.com/documentation/security/1395547-secitemdelete?language=objc
SecItemDelete = c.SecItemDelete
SecItemDelete.restype = c_int
SecItemDelete.argtypes = [CFDictionaryRef]
# SecAccessControlRef SecAccessControlCreateWithFlags(CFAllocatorRef allocator, CFTypeRef protection,
# SecAccessControlCreateFlags flags, CFErrorRef _Nullable *error);
# https://developer.apple.com/documentation/security/1394452-secaccesscontrolcreatewithflags?language=objc
SecAccessControlCreateWithFlags = c.SecAccessControlCreateWithFlags
SecAccessControlCreateWithFlags.restype = SecAccessControlRef
SecAccessControlCreateWithFlags.argtypes = [CFAllocatorRef, CFTypeRef, c_ulong, POINTER(CFErrorRef)]
#
# Keychain errors
#
_status_error_classes = {}
def register_status_error(status=None):
def decorator(cls):
_status_error_classes[status] = cls
return cls
return decorator
class KeychainError(Exception):
def __init__(self, *args, status=None):
super().__init__(*args)
self.status = status
@register_status_error(-25299)
class KeychainDuplicateItemError(KeychainError):
pass
@register_status_error(-25300)
class KeychainItemNotFoundError(KeychainError):
pass
@register_status_error(-25293)
class KeychainAuthFailedError(KeychainError):
pass
@register_status_error(-128)
class KeychainUserCanceledError(KeychainError):
pass
@register_status_error(-25308)
class KeychainUserInteractionNotAllowedError(KeychainError):
pass
@register_status_error(-50)
class KeychainParamError(KeychainError):
pass
@register_status_error()
class KeychainUnhandledError(KeychainError):
pass
def error_class_with_status(status):
return _status_error_classes.get(status, _status_error_classes[None])
def raise_status(status, *args):
if status:
raise error_class_with_status(status)(*args, status=status)
def sec_item_add(attributes: dict) -> None:
raise_status(
SecItemAdd(ns(attributes), None),
'Failed to add keychain item'
)
def sec_item_update(query_attributes, attributes_to_update) -> None:
raise_status(
SecItemUpdate(ns(query_attributes), ns(attributes_to_update)),
'Failed to update keychain item'
)
def sec_item_copy_matching(query_attributes) -> ObjCInstance:
ptr = CFTypeRef()
raise_status(
SecItemCopyMatching(ns(query_attributes), byref(ptr)),
'Failed to get keychain item'
)
assert(ptr.value is not None)
result = ObjCInstance(ptr)
CFRelease(ptr)
return result
def sec_item_copy_matching_data(query_attributes) -> bytes:
query = dict(query_attributes)
query[kSecReturnAttributes] = False
query[kSecReturnData] = True
return from_ns(sec_item_copy_matching(query))
def sec_item_copy_matching_attributes(query_attributes) -> dict:
query = dict(query_attributes)
query[kSecReturnAttributes] = True
query[kSecReturnData] = False
return from_ns(sec_item_copy_matching(query))
def sec_item_delete(query_attributes) -> None:
raise_status(
SecItemDelete(ns(query_attributes)),
'Failed to delete keychain item'
)
#
# Kind of human interface for security framework
#
class ItemClass(str, Enum):
GENERIC_PASSWORD = kSecClassGenericPassword
INTERNET_PASSWORD = kSecClassInternetPassword
class AuthenticationPolicy(IntFlag):
USER_PRESENCE = kSecAccessControlUserPresence
TOUCH_ID_ANY = kSecAccessControlTouchIDAny
TOUCH_ID_CURRENT_SET = kSecAccessControlTouchIDCurrentSet
DEVICE_PASSCODE = kSecAccessControlDevicePasscode
OR = kSecAccessControlOr
AND = kSecAccessControlAnd
PRIVATE_KEY_USAGE = kSecAccessControlPrivateKeyUsage
APPLICATION_PASSWORD = kSecAccessControlApplicationPassword
class Accessibility(str, Enum):
ALWAYS = kSecAttrAccessibleAlways
ALWAYS_THIS_DEVICE_ONLY = kSecAttrAccessibleAlwaysThisDeviceOnly
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
AFTER_FIRST_UNLOCK = kSecAttrAccessibleAfterFirstUnlock
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
WHEN_UNLOCKED = kSecAttrAccessibleWhenUnlocked
WHEN_UNLOCKED_THIS_DEVICE_ONLY = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
class AuthenticationUI(str, Enum):
ALLOW = kSecUseAuthenticationUIAllow
FAIL = kSecUseAuthenticationUIFail
SKIP = kSecUseAuthenticationUISkip
class AccessControl:
def __init__(self, accessibility: Accessibility, flags: AuthenticationPolicy):
self._accessibility = accessibility
self._flags = flags
self._sac = None
@property
def accessibility(self):
return self._accessibility
@property
def flags(self):
return self._flags
@property
def value(self):
if not self._sac:
sac = SecAccessControlCreateWithFlags(None, ns(self._accessibility.value), self._flags, None)
if sac is None:
raise KeychainError('Failed to create SecAccessControl object')
self._sac = ObjCInstance(sac)
CFRelease(sac)
return self._sac
class _SecItem:
_ITEM_CLASS = None
def __init__(self, **kwargs):
self.accessibility = kwargs.get('accessibility', None)
self.access_control = kwargs.get('access_control', None)
self.description = kwargs.get('description', None)
self.label = kwargs.get('label', None)
self.comment = kwargs.get('comment', None)
self.is_invisible = kwargs.get('is_invisible', None)
self.is_negative = kwargs.get('is_negative', None)
@property
def item_class(self):
return self._ITEM_CLASS
def _query_attributes(self):
return {
kSecClass: self.item_class
}
def _item_attributes(self):
attrs = {}
if self.accessibility is not None:
attrs[kSecAttrAccessible] = self.accessibility.value
if self.access_control:
attrs[kSecAttrAccessControl] = self.access_control.value
if self.description:
attrs[kSecAttrDescription] = self.description
if self.label:
attrs[kSecAttrLabel] = self.label
if self.comment:
attrs[kSecAttrComment] = self.comment
if self.is_invisible:
attrs[kSecAttrIsInvisible] = self.is_invisible
if self.is_negative:
attrs[kSecAttrIsNegative] = self.is_negative
return attrs
def _get_attributes(self, *, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
query = self._query_attributes()
query[kSecReturnAttributes] = True
query[kSecReturnData] = False
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecUseAuthenticationUI] = authentication_ui
if prompt:
query[kSecUseOperationPrompt] = prompt
return sec_item_copy_matching_attributes(query)
def get_data(self, *, prompt: Union[str, None] = None, authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
query = self._query_attributes()
query[kSecReturnAttributes] = False
query[kSecReturnData] = True
query[kSecMatchLimit] = kSecMatchLimitOne
query[kSecUseAuthenticationUI] = authentication_ui
if prompt:
query[kSecUseOperationPrompt] = prompt
return sec_item_copy_matching_data(query)
def delete(self):
try:
sec_item_delete(self._query_attributes())
except KeychainItemNotFoundError:
pass
def add(self, *, data: Union[bytes, None] = None, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
attrs = self._query_attributes()
attrs.update(self._item_attributes())
attrs[kSecUseAuthenticationUI] = authentication_ui
if data:
attrs[kSecValueData] = data
if prompt:
attrs[kSecUseOperationPrompt] = prompt
sec_item_add(attrs)
def update(self, *, data: Union[bytes, None] = None, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
query = self._query_attributes()
attrs = self._item_attributes()
query[kSecUseAuthenticationUI] = authentication_ui
if data:
attrs[kSecValueData] = data
if prompt:
query[kSecUseOperationPrompt] = prompt
sec_item_update(query, attrs)
def save(self, *, data: Union[bytes, None] = None, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
try:
self.add(data=data, prompt=prompt, authentication_ui=authentication_ui)
except KeychainDuplicateItemError:
self.update(data=data, prompt=prompt, authentication_ui=authentication_ui)
@classmethod
def _query_items(cls, attributes=None, *, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
query = {
kSecClass: cls._ITEM_CLASS,
kSecReturnData: False,
kSecReturnAttributes: True,
kSecMatchLimit: kSecMatchLimitAll,
kSecUseAuthenticationUI: authentication_ui
}
if prompt:
query[kSecUseOperationPrompt] = prompt
if attributes:
query.update(attributes)
result = sec_item_copy_matching(query)
return [
from_ns(result.objectAtIndex_(i))
for i in range(result.count())
]
class _SecItemAttributes:
def __init__(self, attrs):
self.modification_date = attrs.get(kSecAttrModificationDate, None)
self.creation_date = attrs.get(kSecAttrCreationDate, None)
self.description = attrs.get(kSecAttrDescription, None)
self.label = attrs.get(kSecAttrLabel, None)
self.comment = attrs.get(kSecAttrComment, None)
self.is_invisible = bool(attrs.get(kSecAttrIsInvisible, False))
self.is_negative = bool(attrs.get(kSecAttrIsNegative, False))
if kSecAttrAccessible in attrs:
self.accessibility = Accessibility(attrs[kSecAttrAccessible])
class GenericPasswordAttributes(_SecItemAttributes):
def __init__(self, attrs):
super().__init__(attrs)
self.item_class = ItemClass.GENERIC_PASSWORD
self.service = attrs.get(kSecAttrService, None)
self.account = attrs.get(kSecAttrAccount, None)
self.generic = attrs.get(kSecAttrGeneric, None)
class GenericPassword(_SecItem):
_ITEM_CLASS = ItemClass.GENERIC_PASSWORD
def __init__(self, service: str, account: str):
super().__init__()
self._service = service
self._account = account
self.generic = None
@property
def service(self):
return self._service
@property
def account(self):
return self._account
def _query_attributes(self):
query = super()._query_attributes()
query[kSecAttrService] = self.service
query[kSecAttrAccount] = self.account
return query
def _item_attributes(self):
attrs = super()._item_attributes()
attrs[kSecAttrService] = self._service
attrs[kSecAttrAccount] = self._account
if self.generic:
attrs[kSecAttrGeneric] = self.generic
return attrs
def get_attributes(self, *, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
return GenericPasswordAttributes(self._get_attributes(prompt=prompt, authentication_ui=authentication_ui))
def get_password(self, *, prompt: Union[str, None] = None, authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
return self.get_data(prompt=prompt, authentication_ui=authentication_ui).decode()
def set_password(self, password, *, prompt: Union[str, None] = None,
authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
self.save(data=password.encode(), prompt=prompt, authentication_ui=authentication_ui)
@classmethod
def query_items(cls, service: Union[str, None] = None,
prompt: Union[str, None] = None, authentication_ui: AuthenticationUI = AuthenticationUI.ALLOW):
attrs = {}
if service:
attrs[kSecAttrService] = service
return [
GenericPasswordAttributes(x)
for x in cls._query_items(attrs, prompt=prompt, authentication_ui=authentication_ui)
]
#
# Pythonista keychain compatibility layer
#
# - all functions mimicks Pythonista keychain module functions behavior
# - not sure what Pythonista does about exceptions (aka status != 0), I'm raising
#
def delete_password(service, account):
"""Delete the password for the given service/account from the keychain."""
try:
GenericPassword(service, account).delete()
except KeychainItemNotFoundError:
pass
def set_password(service, account, password):
"""Save a password for the given service and account in the keychain."""
GenericPassword(service, account).set_password(password)
def get_password(service, account):
"""Get the password for the given service/account that was previously stored in the keychain."""
try:
return GenericPassword(service, account).get_password()
except KeychainItemNotFoundError:
# Compatibility - Pythonista returns None if there's no password
return None
def get_services():
"""Return a list of all services and accounts that are stored in the keychain (each item is a 2-tuple)."""
try:
return [
(x.service, x.account)
for x in GenericPassword.query_items()
]
except KeychainItemNotFoundError:
# Compatibility - Pythonista returns empty List if there're no passwords
return []
def reset_keychain():
"""Delete all data from the keychain (including the master password) after showing a confirmation dialog."""
# Not a fan of this method :)
raise NotImplementedError('Use Pythonista keychain.reset_keychain() if you really need it')
#
# Tests
#
def test_delete_password():
set_password('s', 'a', 'password')
assert(get_password('s', 'a') == 'password')
delete_password('s', 'a')
assert(get_password('s', 'a') is None)
def test_pythonista_compatibility_delete_password_does_not_raise():
delete_password('s', 'a')
delete_password('s', 'a')
def test_set_password():
delete_password('s', 'a')
assert(get_password('s', 'a') is None)
set_password('s', 'a', 'password')
assert(get_password('s', 'a') == 'password')
delete_password('s', 'a')
def test_pythonista_compatibility_set_password_does_not_raise():
set_password('s', 'a', 'password')
set_password('s', 'a', 'password2')
delete_password('s', 'a')
def test_get_password():
set_password('s', 'a', 'password')
assert(get_password('s', 'a') == 'password')
delete_password('s', 'a')
def test_pythonista_compatibility_get_password_does_not_raise():
delete_password('s', 'a')
assert(get_password('s', 'a') is None)
def test_against_pythonista_keychain():
import keychain
set_password('s', 'a', 'password')
assert(keychain.get_password('s', 'a') == 'password')
keychain.set_password('s', 'a', 'anotherone')
assert(get_password('s', 'a') == 'anotherone')
keychain.delete_password('s', 'a')
assert(get_password('s', 'a') is None)
def test_get_services():
# We do not want to delete all items in tests -> no test for []
set_password('s', 'a', 'password')
set_password('s', 'a2', 'password')
services = get_services()
s_services = list(filter(lambda x: x[0] == 's', services))
assert(len(s_services) == 2)
s_accounts = sorted([x[1] for x in s_services])
assert(s_accounts == ['a', 'a2'])
delete_password('s', 'a')
delete_password('s', 'a2')
services = get_services()
s_services = list(filter(lambda x: x[0] == 's', services))
assert(len(s_services) == 0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment