Skip to content

Commit

Permalink
Keychain label support
Browse files Browse the repository at this point in the history
The cosmetic difference is that generic passwords now have a label and GenericPassword.__repr__ will use it.

This involved adding or fixing several struct definitions, multiple constants and dealing with several misleading parts of the Keychain documentation. The next step will be to move this into the classes and expand the support beyond labels to allow any keychain attribute to be retrieved or, finally, updated.

--HG--
extra : convert_revision : d12eed8
  • Loading branch information
acdha committed Mar 21, 2009
1 parent 0dcfe86 commit d533cb4
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 16 deletions.
2 changes: 1 addition & 1 deletion bin/keychain-delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def main():
if not options.keychain and os.getuid() == 0:
options.keychain = "/Library/Keychains/System.keychain"

if not options.account and options.service:
if not (options.account or options.service):
parser.error("You must specify either an account or service name")

try:
Expand Down
65 changes: 54 additions & 11 deletions lib/PyMacAdmin/Security/Keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def open_keychain(self, path=None):

def find_generic_password(self, service_name="", account_name=""):
"""Pythonic wrapper for SecKeychainFindGenericPassword"""
item = ctypes.c_void_p()
item_p = ctypes.c_uint32()
password_length = ctypes.c_uint32(0)
password_data = ctypes.c_char_p(256)

Expand All @@ -57,7 +57,7 @@ def find_generic_password(self, service_name="", account_name=""):
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)
ctypes.byref(item_p)
)

if rc == -25300:
Expand All @@ -67,9 +67,48 @@ def find_generic_password(self, service_name="", account_name=""):

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)
# 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.

d_len = ctypes.c_int(0)
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
# SecKeychainAttributeInfo should allow .append(tag, [data])
info.count = 1
info.tag.contents = ctypes.c_ulong(7) # TODO: add kSecLabelItemAttr define

Security.lib.SecKeychainItemCopyAttributesAndData(item_p, ctypes.pointer(info), None, ctypes.byref(attrs_p), ctypes.byref(d_len), None)
attrs = attrs_p.contents
assert(attrs.count == 1)
# TODO: This should move into standard iterator support for SecKeychainAttributeList:
# for offset in range(0, attrs.count):
# print "[%d]: %s(%d): %s" % (offset, attrs.attr[offset].tag, attrs.attr[offset].length, attrs.attr[offset].data)

label = attrs.attr[0].data[:attrs.attr[0].length]

Security.lib.SecKeychainItemFreeContent(None, item_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"""
Expand All @@ -86,7 +125,7 @@ def find_internet_password(self, account_name="", password="", server_name="", s
if not isinstance(port, int):
port = int(port)

rc = Security.lib.SecKeychainFindInternetPassword (
rc = Security.lib.SecKeychainFindInternetPassword(
self.keychain_handle,
len(server_name),
server_name,
Expand Down Expand Up @@ -165,10 +204,10 @@ def remove(self, item):
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
label = None
password = None
keychain_item = None # An SecKeychainItemRef treated as an opaque object

Expand Down Expand Up @@ -213,7 +252,7 @@ def __str__(self):

def __repr__(self):
props = []
for k in ['service_name', 'account_name']:
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))
Expand Down Expand Up @@ -250,8 +289,8 @@ class SecKeychainAttribute(ctypes.Structure):
data: A pointer to the attribute data.
"""
_fields_ = [
('tag', ctypes.c_char_p),
('length', ctypes.c_int32),
('tag', ctypes.c_uint32),
('length', ctypes.c_uint32),
('data', ctypes.c_char_p)
]

Expand Down Expand Up @@ -282,5 +321,9 @@ class SecKeychainAttributeInfo(ctypes.Structure):
# The APIs expect pointers to SecKeychainAttributeInfo objects and we'd
# like to avoid having to manage memory manually:
SecKeychainAttributeInfo_p = ctypes.POINTER(SecKeychainAttributeInfo)
SecKeychainAttributeInfo_p.__del__ = lambda s: Security.lib.SecKeychainFreeAttributeInfo(s)
# BUG: This causes a crash if the Python object is never initialized correctly. We should define a checked free function instead:
# SecKeychainAttributeInfo_p.__del__ = lambda self: Security.lib.SecKeychainFreeAttributeInfo(self)

SecKeychainAttributeList_p = ctypes.POINTER(SecKeychainAttributeList)
# BUG: This causes a crash if the Python object is never initialized correctly. We should define a checked free function instead:
# SecKeychainAttributeList_p.__del__ = lambda self: Security.lib.SecKeychainFreeAttributeInfo(self)
12 changes: 10 additions & 2 deletions lib/PyMacAdmin/Security/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
# encoding: utf-8
import ctypes
import PyMacAdmin
import struct

# 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 = PyMacAdmin.load_carbon_framework('/System/Library/Frameworks/Security.framework/Versions/Current/Security')

# TODO: Expand this considerably?
# TODO: Expand this considerably or find a better way to deal with pre-defined constants
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

# typedef FourCharCode SecItemClass;
# SecKeychainItem.h
# TODO: Expand these constants to avoid calling struct.unpack()
kSecInternetPasswordItemClass = struct.unpack('BBBB', 'inet')
kSecGenericPasswordItemClass = struct.unpack('BBBB', 'genp')
kSecAppleSharePasswordItemClass = struct.unpack('BBBB', 'ashp')
kSecCertificateItemClass = CSSM_DL_DB_RECORD_X509_CERTIFICATE
3 changes: 1 addition & 2 deletions lib/PyMacAdmin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,15 @@ def load_carbon_framework(f_path):
<CDLL '/System/Library/Frameworks/Security.framework/Versions/Current/Security', handle 318320 at 2515f0>
"""
framework = ctypes.cdll.LoadLibrary(f_path)
old_getitem = framework.__getitem__

# TODO: Do we ever need to wrap framework.__getattr__ too?
old_getitem = framework.__getitem__
@wraps(old_getitem)
def new_getitem(k):
v = old_getitem(k)
if hasattr(v, "errcheck") and not v.errcheck:
v.errcheck = carbon_errcheck
return v

framework.__getitem__ = new_getitem

return framework
Expand Down

0 comments on commit d533cb4

Please sign in to comment.