#!/usr/bin/env python # encoding: utf-8 """ Wrapper for the core Keychain API Most of the internals are directly based on the native Keychain API. Apple's developer documentation is highly relevant: http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/uid/TP30000898-CH1-SW1 """ import os import ctypes from PyMacAdmin import Security class Keychain(object): """A friendlier wrapper for the Keychain API""" # TODO: Add support for SecKeychainSetUserInteractionAllowed def __init__(self, keychain_name=None): self.keychain_handle = self.open_keychain(keychain_name) def open_keychain(self, path=None): """Open a keychain file - if no path is provided, the user's default keychain will be used""" if not path: return None if path and not os.path.exists(path): raise IOError("Keychain %s does not exist" % path) keychain = ctypes.c_void_p() keychain_ptr = ctypes.pointer(keychain) rc = Security.lib.SecKeychainOpen(path, keychain_ptr) if rc != 0: raise RuntimeError("Couldn't open system keychain: rc=%d" % rc) return keychain def find_generic_password(self, service_name="", account_name=""): """Pythonic wrapper for SecKeychainFindGenericPassword""" item_p = ctypes.c_uint32() password_length = ctypes.c_uint32(0) password_data = ctypes.c_char_p(256) # For our purposes None and "" should be equivalent but we need a real # string for len() below: if not service_name: service_name = "" if not account_name: account_name = "" rc = Security.lib.SecKeychainFindGenericPassword ( self.keychain_handle, len(service_name), # Length of service name service_name, # Service name len(account_name), # Account name length account_name, # Account name ctypes.byref(password_length), # Will be filled with pw length ctypes.pointer(password_data), # Will be filled with pw data ctypes.byref(item_p) ) if rc == -25300: raise KeyError('No keychain entry for generic password: service=%s, account=%s' % (service_name, account_name)) elif rc != 0: raise RuntimeError('Unable to retrieve generic password (service=%s, account=%s): rc=%d' % (service_name, account_name, rc)) password = password_data.value[0:password_length.value] Security.lib.SecKeychainItemFreeContent(None, password_data) # itemRef: A reference to the keychain item from which you wish to # retrieve data or attributes. # # info: A pointer to a list of tags of attributes to retrieve. # # itemClass: A pointer to the item’s class. You should pass NULL if not # required. See “Keychain Item Class Constants” for valid constants. # # attrList: On input, the list of attributes in this item to get; on # output the attributes are filled in. You should call the function # SecKeychainItemFreeAttributesAndData when you no longer need the # attributes and data. # # length: On return, a pointer to the actual length of the data. # # outData: A pointer to a buffer containing the data in this item. Pass # NULL if not required. You should call the function # SecKeychainItemFreeAttributesAndData when you no longer need the # attributes and data. info = SecKeychainAttributeInfo() attrs_p = SecKeychainAttributeList_p() # Thank you Wil Shipley: # http://www.wilshipley.com/blog/2006/10/pimp-my-code-part-12-frozen-in.html info.count = 1 info.tag.contents = Security.kSecLabelItemAttr Security.lib.SecKeychainItemCopyAttributesAndData(item_p, ctypes.pointer(info), None, ctypes.byref(attrs_p), None, None) attrs = attrs_p.contents assert(attrs.count >= 1) label = attrs.attr[0].data[:attrs.attr[0].length] Security.lib.SecKeychainItemFreeAttributesAndData(attrs_p) return GenericPassword(service_name=service_name, account_name=account_name, password=password, keychain_item=item_p, label=label) def find_internet_password(self, account_name="", password="", server_name="", security_domain="", path="", port=0, protocol_type=None, authentication_type=None): """Pythonic wrapper for SecKeychainFindInternetPassword""" item = ctypes.c_void_p() password_length = ctypes.c_uint32(0) password_data = ctypes.c_char_p(256) if protocol_type and len(protocol_type) != 4: raise TypeError("protocol_type must be a valid FourCharCode - see http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/c_ref/SecProtocolType") if authentication_type and len(authentication_type) != 4: raise TypeError("authentication_type must be a valid FourCharCode - see http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/c_ref/SecAuthenticationType") if not isinstance(port, int): port = int(port) rc = Security.lib.SecKeychainFindInternetPassword( self.keychain_handle, len(server_name), server_name, len(security_domain) if security_domain else 0, security_domain, len(account_name), account_name, len(path), path, port, protocol_type, authentication_type, ctypes.byref(password_length), # Will be filled with pw length ctypes.pointer(password_data), # Will be filled with pw data ctypes.pointer(item) ) if rc == -25300: raise KeyError('No keychain entry for internet password: server=%s, account=%s' % (server_name, account_name)) elif rc != 0: raise RuntimeError('Unable to retrieve internet password (server=%s, account=%s): rc=%d' % (server_name, account_name, rc)) password = password_data.value[0:password_length.value] Security.lib.SecKeychainItemFreeContent(None, password_data) return InternetPassword(server_name=server_name, account_name=account_name, password=password, keychain_item=item, security_domain=security_domain, path=path, port=port, protocol_type=protocol_type, authentication_type=authentication_type) def add(self, item): """Add the provided GenericPassword or InternetPassword object to this Keychain""" assert(isinstance(item, GenericPassword)) item_ref = ctypes.c_void_p() if isinstance(item, InternetPassword): rc = Security.lib.SecKeychainAddInternetPassword( self.keychain_handle, len(item.server_name), item.server_name, len(item.security_domain), item.security_domain, len(item.account_name), item.account_name, len(item.path), item.path, item.port, item.protocol_type, item.authentication_type, len(item.password), item.password, ctypes.pointer(item_ref) ) else: rc = Security.lib.SecKeychainAddGenericPassword( self.keychain_handle, len(item.service_name), item.service_name, len(item.account_name), item.account_name, len(item.password), item.password, ctypes.pointer(item_ref) ) if rc != 0: raise RuntimeError("Error adding %s: rc=%d" % (item, rc)) item.keychain_item = item_ref def remove(self, item): """Remove the provided keychain item as the reverse of Keychain.add()""" assert(isinstance(item, GenericPassword)) item.delete() class GenericPassword(object): """Generic keychain password used with SecKeychainAddGenericPassword and SecKeychainFindGenericPassword""" # TODO: Add support for access control and attributes account_name = None service_name = None label = None password = None keychain_item = None # An SecKeychainItemRef treated as an opaque object def __init__(self, **kwargs): super(GenericPassword, self).__init__() for k, v in kwargs.items(): if not hasattr(self, k): raise AttributeError("Unknown property %s" % k) setattr(self, k, v) def update_password(self, new_password): """Change the stored password""" rc = Security.lib.SecKeychainItemModifyAttributesAndData( self.keychain_item, None, len(new_password), new_password ) if rc == -61: raise RuntimeError("Permission denied updating %s" % self) elif rc != 0: raise RuntimeError("Unable to update password for %s: rc = %d" % rc) def delete(self): """Removes this item from the keychain""" rc = Security.lib.SecKeychainItemDelete(self.keychain_item) if rc != 0: raise RuntimeError("Unable to delete %s: rc=%d" % (self, rc)) from CoreFoundation import CFRelease CFRelease(self.keychain_item) self.keychain_item = None self.service_name = None self.account_name = None self.password = None def __str__(self): return repr(self) def __repr__(self): props = [] for k in ['service_name', 'account_name', 'label']: props.append("%s=%s" % (k, repr(getattr(self, k)))) return "%s(%s)" % (self.__class__.__name__, ", ".join(props)) class InternetPassword(GenericPassword): """Specialized keychain item for internet passwords used with SecKeychainAddInternetPassword and SecKeychainFindInternetPassword""" account_name = "" password = None keychain_item = None server_name = "" security_domain = "" path = "" port = 0 protocol_type = None authentication_type = None def __init__(self, **kwargs): super(InternetPassword, self).__init__(**kwargs) def __repr__(self): props = [] for k in ['account_name', 'server_name', 'security_domain', 'path', 'port', 'protocol_type', 'authentication_type']: if getattr(self, k): props.append("%s=%s" % (k, repr(getattr(self, k)))) return "%s(%s)" % (self.__class__.__name__, ", ".join(props)) class SecKeychainAttribute(ctypes.Structure): """Contains keychain attributes tag: A 4-byte attribute tag. length: The length of the buffer pointed to by data. data: A pointer to the attribute data. """ _fields_ = [ ('tag', ctypes.c_uint32), ('length', ctypes.c_uint32), ('data', ctypes.c_char_p) ] class SecKeychainAttributeList(ctypes.Structure): """Represents a list of keychain attributes count: An unsigned 32-bit integer that represents the number of keychain attributes in the array. attr: A pointer to the first keychain attribute in the array. """ # TODO: Standard iterator support for SecKeychainAttributeList: # # for offset in range(0, attrs.count): # print "[%d]: %s: %s" % (offset, attrs.attr[offset].tag, attrs.attr[offset].data[:attrs.attr[offset].length]) # # becomes: # # for tag, data in attrs: # … # # attrs[tag] should also work # _fields_ = [ ('count', ctypes.c_uint), ('attr', ctypes.POINTER(SecKeychainAttribute)) ] class SecKeychainAttributeInfo(ctypes.Structure): """Represents a keychain attribute as a pair of tag and format values. count: The number of tag-format pairs in the respective arrays tag: A pointer to the first attribute tag in the array format: A pointer to the first CSSM_DB_ATTRIBUTE_FORMAT in the array """ # TODO: SecKeychainAttributeInfo should allow .append(tag, [data]) _fields_ = [ ('count', ctypes.c_uint), ('tag', ctypes.POINTER(ctypes.c_uint)), ('format', ctypes.POINTER(ctypes.c_uint)) ] # The APIs expect pointers to SecKeychainAttributeInfo objects: SecKeychainAttributeInfo_p = ctypes.POINTER(SecKeychainAttributeInfo) SecKeychainAttributeList_p = ctypes.POINTER(SecKeychainAttributeList)