Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MacOS "Internet password" keychain items #624

Open
YKdvd opened this issue Mar 11, 2023 · 5 comments
Open

MacOS "Internet password" keychain items #624

YKdvd opened this issue Mar 11, 2023 · 5 comments

Comments

@YKdvd
Copy link

YKdvd commented Mar 11, 2023

I was hoping to use keyring to retrieve existing internet passwords on the MacOS keychain for things like ssh server passwords, etc. It looks like keyring deals with "application password" items ("kSecClassGenericPassword"), and can't retrieve "Internet password" items ("kSecClassInternetPassword"). I came up with a variation on your routine, and while I was at it, thought I'd try and retrieve the attributes on the item as well, by setting "kSecReturnAttributes" in the query as well.
It seemed to work - I apparently get the promised CFDictionary back, and I cobbled together a CFDictionaryGetValue() routine from other sources. I can get the item data value (the password) from the dictionary entry "kSecValueData", and convert it as you do with cfstr_to_str(). But while I can successfully retrieve other keys like "kSecAttrServer", which should be the name of the server and also a CFString, cfstr_to_str() crashes with "[__NSCFString bytes]: unrecognized selector sent to instance", so it doesn't seem to be a CFString as expected, or something?

I don't really know the MacOS APIs (at least since MacOS 9 or so), and I may be missing something obvious between that and the whole coercing into Python, so I thought I'd check if anyone here might have an idea.

Also, while the keyring API doesn't seem to be set up to handle multiple flavours of passwords like this, would there be any interest in having the macOS.api have superset functions that can handle some of the other types as a convenience for MacOS folks?

from keyring.backends.macOS import api
def find_internet_password(server, username, protocol="ssh ", not_found_ok=False):
    # https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items?language=objc
    q = api.create_query(
        kSecClass=api.k_('kSecClassInternetPassword'),
        kSecMatchLimit=api.k_('kSecMatchLimitOne'),
        kSecAttrServer=server,
        kSecAttrProtocol=protocol,
        kSecAttrAccount=username,
        kSecReturnAttributes=api.create_cfbool(True),
        kSecReturnData=api.create_cfbool(True),
    )
    data = api.c_void_p()
    status = api.SecItemCopyMatching(q, api.byref(data))
    if status == api.error.item_not_found and not_found_ok:
        return
    api.Error.raise_for_status(status)
    password = CFDictionaryGetValue(data, api.k_("kSecValueData"))  # should be a CFString
    password = api.cfstr_to_str(password)   # and the conversion works
    if True:    # now try the attributes
        retServer = CFDictionaryGetValue(data, api.k_("kSecAttrServer"))
        retServer = api.cfstr_to_str(retServer)    # should also be a CFString?   but crashes
        account = CFDictionaryGetValue(data, api.k_("kSecAttrAccount"))
        account = api.cfstr_to_str(account) # should also be a CFString?   but crashes
        lastmod = CFDictionaryGetValue(data, api.k_("kSecAttrModificationDate"))
        #lastmodd = api.ctypes.cast(api.CFDataGetBytePtr(lastmod), ctypes.c_double)
        #lastmod = CFDateGetAbsoluteTime(lastmod)

    return password

password = find_internet_password("myserver.mydomain.org", "myusername")
@YKdvd
Copy link
Author

YKdvd commented Mar 12, 2023

Actually, I see the problem - cfstr_to_str is actually dealing with converting the CFData that is represented by "kSecValueData", not CFstring, and the name mislead me.

I found some ancient but still useful stuff at https://github.com/mountainstorm/MobileDevice/blob/41645fd0e7e3e674f0e963daaa374e3f44f18d67/CoreFoundation.py and have something that can get the attributes of a "kSecClassInternetPassword". I might try and clean it up and submit something as a suggestion if I ever get a chance.

@vToMy
Copy link

vToMy commented May 24, 2023

Here is a working stand-alone version of the above code:

from keyring.backends.macOS import api


CFTypeRef = api.c_void_p
CFDictionaryRef = api.c_void_p

CFDictionaryGetValue = api._found.CFDictionaryGetValue
CFDictionaryGetValue.restype = CFTypeRef
CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFTypeRef]


def find_internet_password(server, protocol, username, not_found_ok=False):
    # https://developer.apple.com/documentation/security/keychain_services/keychain_items/searching_for_keychain_items?language=objc
    q = api.create_query(
        kSecClass=api.k_('kSecClassInternetPassword'),
        kSecMatchLimit=api.k_('kSecMatchLimitOne'),
        kSecAttrServer=server,
        kSecAttrProtocol=protocol,
        kSecAttrAccount=username,
        kSecReturnAttributes=api.create_cfbool(True),
        kSecReturnData=api.create_cfbool(True),
    )
    data = api.c_void_p()
    status = api.SecItemCopyMatching(q, api.byref(data))
    if status == api.error.item_not_found and not_found_ok:
        return
    api.Error.raise_for_status(status)
    password = CFDictionaryGetValue(data, api.k_('kSecValueData'))
    password = api.cfstr_to_str(password)
    return password


password = find_internet_password('http://proxy.com', 'htpx', 'username')
print(password)

@vToMy
Copy link

vToMy commented May 24, 2023

This is very useful in automatically getting proxy credentials from the machine.
Ideally, I'd like to be able to query only for the protocol (htpx is an http proxy), and then retrieve the server name, port, user and password.
It is possible to query just by the protocol type by removing the server and username from the create_query parameters (simply passing null does not work).
I still did not manage to parse the server, port, and username from the result though... if anyone is up to the task.

As a workaround, one can simply call:
security find-internet-password -r htpx
And parse the results.

@jaraco jaraco added the macOS label May 24, 2023
@jaraco
Copy link
Owner

jaraco commented May 24, 2023

Nice work. I'm very interested in providing a more complete interface. I'd very much like to build up the API module and possibly expose some of that through the Keyring (there's a mechanism by which environment variables can set properties on a keyring and thus affect behavior, so e.g. something like KEYRING_PROPERTY_KEY_CLASS="internet" would cause macOS.Keyring.key_class = "internet" and thus could signal this different kSecClass.

I recommend to start with expanding the APIs to support the functionality as best as possible and second to capture what are the use-cases that we're trying to support (do we want this to work with the CLI (keyring get ...), the API (keyring.get/set_password), or something else.

@shirakaba
Copy link

shirakaba commented Dec 30, 2023

For anyone who ends up here on a similar search: just a big heads-up that the underlying macOS CLI tool, security find-internet-password, searches all local keychains but not, bafflingly, iCloud Keychain. So as far as I can tell, there is no headless way to get internet passwords stored in iCloud Keychain.

The implementation above does provide value as it finds internet passwords from the login keychain (or any other local keychain), but if we were ever to support iCloud Keychain as well, it sounds like it would have to involve some GUI-based flow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants