Skip to content

Instantly share code, notes, and snippets.

@acdha
Created March 14, 2009 17:01
Show Gist options
  • Save acdha/79119 to your computer and use it in GitHub Desktop.
Save acdha/79119 to your computer and use it in GitHub Desktop.
# encoding: utf-8
import ctypes
# This is not particularly elegant but to avoid everything having to load the
# Security framework we use a single copy hanging of this module so everything
# else can simply use Security.lib.SecKeychainFoo(…)
lib = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Security.framework/Versions/Current/Security')
# TODO: Expand this considerably?
CSSM_DB_RECORDTYPE_APP_DEFINED_START = 0x80000000
CSSM_DL_DB_RECORD_X509_CERTIFICATE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x1000
kSecCertificateItemClass = CSSM_DL_DB_RECORD_X509_CERTIFICATE
#!/usr/bin/env python2.5
from PyMacAdmin import carbon_call
from PyMacAdmin.Security import kSecCertificateItemClass
from PyMacAdmin.Security.Keychain import SecKeychainAttribute, SecKeychainAttributeList
import sys
import ctypes
Security = ctypes.cdll.LoadLibrary('/System/Library/Frameworks/Security.framework/Versions/Current/Security')
label = "<some label text here>"
plabel = ctypes.c_char_p(label)
tag = 'labl'
attr = SecKeychainAttribute(tag, 1, plabel)
attrList = SecKeychainAttributeList(1, attr)
# http://developer.apple.com/DOCUMENTATION/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/c/tdef/SecItemClass
searchRef = ctypes.c_void_p()
itemRef = ctypes.c_void_p()
try:
carbon_call(Security.SecKeychainSearchCreateFromAttributes,
None,
kSecCertificateItemClass,
ctypes.byref(attrList),
ctypes.pointer(searchRef)
)
carbon_call(Security.SecKeychainSearchCopyNext,
searchRef,
ctypes.byref(itemRef)
)
if searchRef:
CFRelease(searchRef)
carbon_call(Security.SecKeychainItemDelete, itemRef )
if itemRef:
CFRelease(itemRef)
except RuntimeError, e:
print >>sys.stderr, "ERROR: %s" % e
sys.exit(1)
#!/usr/bin/env python
# encoding: utf-8
"""
Wrapper for the core Keychain API:
http://developer.apple.com/documentation/Security/Reference/keychainservices/Reference/reference.html#//apple_ref/doc/uid/TP30000898-CH1-SW1
Created by Chris Adams on 2009-01-06.
"""
import os
import ctypes
from PyMacAdmin import Security
class SecKeychainAttribute(ctypes.Structure):
"""ctypes version of the native version"""
_fields_ = [
('tag', ctypes.c_char_p),
('length', ctypes.c_int32),
('data', ctypes.c_char_p)
]
class SecKeychainAttributeList(ctypes.Structure):
"""ctypes version of the native version"""
_fields_ = [
('count', ctypes.c_int),
('attr', SecKeychainAttribute)
]
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 = ctypes.c_void_p()
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.pointer(item)
)
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)
return GenericPassword(service_name=service_name, account_name=account_name, password=password, keychain_item=item)
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
# TODO: Add item name support
account_name = None
service_name = 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']:
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))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment