Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Keychain & TouchID
-
@zrzka This looks very interesting, and I'm sure that it'll give me a few ideas about how to improve Pythonista's built-in
keychain
module.But I'd appreciate if you could rename your module to something else (
better_keychain
,keychain2
, or whatever). Conflicting module names are a pretty frequent support issue for Pythonista. All kinds of weird things can happen when people have modules in site-packages that have the same name as a built-in module. -
@shaun-h yep, FaceID is biometrics as well. This shared gist is a proof of concept. If you'd like to play with it, rename it to
security.py
, place in tosite-packages-3
and thenimport security
. This will be included in BM asblackmamba.framework.security
. iOS keychain support comes from theSecurity.framework
and I'll put all wrappers in to theblackmamba.framework
& framework name. Do not usekeychain.py
, just to avoid clashes. If you do not want to rewrite existing code, you can doimport security as keychain
:)I'll introduce more convenience funcs later when I'll finish it. Would like to add InternetPassword, probably certificates as well, ... Then will have to think again about classes, methods, ... again, refactor little bit and then convenience funcs. Do not want to refactor all these convenience funcs as well when the underlying GenericPassword will be modified for sure. Saving some time :)
-
@omz yup, will rename it in the gist as well. As I wrote, it will be
blackmamba.framework.security
. It's just WIP for now.Edit: Done.
-
BTW for
ctypes
users, found a way how to extract symbols. Example forkSecClass
:load_framework('Security') def _symbol_ptr(name): return c_void_p.in_dll(c, name) def _str_symbol(name): return ObjCInstance(_symbol_ptr(name)).UTF8String().decode() kSecClass = _str_symbol('kSecClass')
And
kSecClass
now containsclass
, which iskSecClass
symbol value in Security framework. Was extracting them on Mac and I no longer need it now :) -
(@zrzka You probably found this out already, but I'll explain it just in case, and for others reading the thread)
in_dll
only works for variables declared asextern
in the headers, which is often the case forNSString
s and other object constants. On the other hand, integer constants are almost always preprocessor macros orstatic
variables, which are not accessible at runtime. Those constants' values cannot be looked up usingin_dll
and have to be copied from the appropriate headers.Sometimes the values of
extern const
variables are written in the headers, so they could be copied from there, but it's better to usein_dll
where possible. That way, if Apple changes the value of a string constant in a new iOS version, the new value will be used automatically (sincein_dll
loads it at runtime from the respective library/framework). If the value is copy-pasted from the headers, it may become incorrect in future iOS versions, and the code will break. -
@dgelessus thanks for pointing this out. I'll add one more -
ObjCInstance
with symbol pointer works in this case because of toll-free bridging. One can be confused howCFStringRef
can be used withObjCInstance
and then treated asNSString
. Here's the documentation quote:There are a number of data types in the Core Foundation framework and the Foundation framework that can be used interchangeably. Data types that can be used interchangeably are also referred to as toll-free bridged data types. This means that you can use the same data structure as the argument to a Core Foundation function call or as the receiver of an Objective-C message invocation.
You can find all supported types at the end of the linked page.
-
Okay, some news, Friday, something to play with over weekend. Did update the gist.
Exceptions
They're pretty self explanatory, but ...
KeychainUserInteractionNotAllowedError
- this exception is raised whenever you try to get keychain item which is protected andauthentication_ui
is set to.FAIL
KeychainItemNotFoundError
- this exception is raised whenever you try to get existing keychain item which is protected andauthentication_ui
is set to.SKIP
KeychainUserCanceledError
- this exception is raised whenever you try to get keychain item which is protected and you tap on the Cancel button (system dialog)
Enums
AuthenticationPolicy
It controls how user should authenticate to gain access. It's
IntFlag
(Python 3.6) and you can combine them.Accessibility
It controls when the keychain item is available. You can't combine them. Basically it's about always, after first unlock, when unlocked and combined with this device only. Special value
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
says that the item will be stored when you have a passcode, fingerprints, ... Whenever you remove fingerprints and/or passcode, item will be automatically deleted.AuthenticationUI
ALLOW
- is allowed (default) and if item is protected, UI will appearSKIP
- if item is protected, it behaves like it doesn't existFAIL
- if item is protected,KeychainUserInteractionNotAllowedError
is raised
Classes
AccessControl
Here you can combine accessibility & authentication policy.
GenericPasswordAttributes
Almost all available attributes of generic password keychain items. You can get it via
GenericPassword
instanceget_attributes
method. Or you can get a list of them viaGenericPassword
classquery_items
method.GenericPassword
Class for manipulation with generic passwords.
Goals
- Provide a way to protect password items
- Prepare for other keychain item classes (like internet password)
- Raise for errors, silent errors are evil
- Provide Python enums, classes, ... to hide CF* stuff
- Provide compatibility layer with Pythonista
keychain
module
If you open the gist, you'll see lot of stuff there. You should use only and only things defined in
__all__
. Nothing else. At least now :)Tried to hide complexity, but still pretty complex :) Will think about it more. Enjoy :)
Examples
First line of any example below should be
from security import *
.Pythonista way (compatibility)
set_password('s', 'a', 'p') assert get_password('s', 'a') == 'p' delete_password('s', 'a') assert get_password('s', 'a') is None
GenericPassword way
p = GenericPassword('s', 'a') p.set_password('p') assert p.get_password() == 'p' p.delete() try: p.get_password() except KeychainItemNotFoundError: pass else: assert False
Password attributes
p = GenericPassword('s', 'a') p.comment = 'Comment' p.label = 'Label' p.description = 'Description' p.is_invisible = False p.is_negative = False p.generic = b'custom data' # not a password p.accessibility = Accessibility.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY p.set_password('p') a = p.get_attributes() assert a.comment == p.comment assert a.label == p.label assert a.description == p.description assert a.is_invisible == p.is_invisible assert a.is_negative == p.is_negative assert a.generic == p.generic assert a.accessibility is p.accessibility
Protect with user presence (pass code, touch id, ...)
p = GenericPassword('s', 'a') p.access_control = AccessControl( Accessibility.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY, AuthenticationPolicy.USER_PRESENCE ) p.set_password('p') # System UI will appear, if you hit Cancel, KeychainUserCanceledError is raised assert p.get_password() == 'p'
Access protected with prompt
assert p.get_password(prompt='Zrzka wants your password') == 'p'
Disable authentication UI and auto fail for these items
# We have lost our finger, just ask system to fail automatically if it's protected try: p.get_password( prompt='Zrzka wants your password', authentication_ui=AuthenticationUI.FAIL ) except KeychainUserInteractionNotAllowedError: print('Ooops, my finger is lost and I cannot retrieve my password') else: assert False
Skip protected items
# We have lost our finger, just ask system to skip all these items try: p.get_password( prompt='Zrzka wants your password', authentication_ui=AuthenticationUI.SKIP ) except KeychainItemNotFoundError: pass else: assert False
Query for services & account
attrs = GenericPassword.query_items() for x in attrs: print(f'{x.service} {x.account} {x.creation_date}')
Query for accounts for specific service
attrs = GenericPassword.query_items(service='service') for x in attrs: print(f'{x.service} {x.account} {x.creation_date}')
Skip protected accounts
attrs = GenericPassword.query_items(authentication_ui=AuthenticationUI.SKIP) for x in attrs: print(f'{x.service} {x.account} {x.creation_date}')
Use prompt for protected accounts
attrs = GenericPassword.query_items(prompt='Your finger boy!') for x in attrs: print(f'{x.service} {x.account} {x.creation_date}')
-
Two more things ...
- all methods / funcs can show system UI except
delete
, this one is not protected, - you should never call
get_attributes
,get_data
,get_password
,set_data
,set_password
,query_items
on the main thread ifauthentication_ui
is set to.ALLOW
(default in iOS), - you can if you explicitly pass
.FAIL
or.SKIP
.
- all methods / funcs can show system UI except
-
Enhanced & documented gist part of the Black Mamba as the
blackmamba.framework.security
package. Basically did addInternetPassword
support, polished it little bit, tried to document everything what can be used, etc.Several notes:
- Not yet released, just in the master branch.
- Because not yet released, you have to use latest documentation to see documentation for this module.
-
very interesting.
I just tried it on my iPhone X, but get an error like "failed to get keychain item". any suggestion?