├── .gitignore ├── README.md ├── setup.py └── touchid.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-touch-id [![PEP8](https://img.shields.io/badge/PEP8-compliant-brightgreen.svg)](https://www.python.org/dev/peps/pep-0008/) 2 | 3 | > Access the Touch ID sensor from Python 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ pip install touchid 9 | ``` 10 | 11 | 12 | ## Usage 13 | 14 | ```python 15 | import touchid 16 | 17 | success = touchid.authenticate() 18 | ``` 19 | 20 | ## API 21 | 22 | #### `touchid.is_available()` 23 | Check whether Touch ID is available on the current machine 24 | 25 | 26 | #### `touchid.authenticate(reason='authenticate via Touch ID')` 27 | Authenticate via Touch ID. 28 | This method returns a `bool` determining whether the Touch ID authentication completed successfully. 29 | If the user cancels the authentication, this method will raise an Exception 30 | 31 | ## License 32 | 33 | MIT © [Lukas Kollmer](https://lukas.vip) 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="touchid", 5 | py_modules=["touchid"], 6 | version="1.2.0", 7 | description="Access the Touch ID sensor", 8 | long_description="`touchid` provides an easy to use API for accessing the Touch ID sensor via Apple's LocalAuthentication framework", 9 | keywords=["touch-id", "LocalAuthentication", "LAContext"], 10 | license="MIT", 11 | author="Lukas Kollmer", 12 | author_email="lukas.kollmer@gmail.com", 13 | url="https://github.com/lukaskollmer/py-touch-id/", 14 | install_requires=["pyobjc"] 15 | ) -------------------------------------------------------------------------------- /touchid.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module for accessing the Touch ID sensor in your Mac's Touch Bar. 3 | 4 | Requires pyobjc to be installed 5 | """ 6 | 7 | import sys 8 | import ctypes 9 | from LocalAuthentication import LAContext 10 | from LocalAuthentication import LAPolicyDeviceOwnerAuthenticationWithBiometrics 11 | 12 | kTouchIdPolicy = LAPolicyDeviceOwnerAuthenticationWithBiometrics 13 | 14 | c = ctypes.cdll.LoadLibrary(None) 15 | 16 | PY3 = sys.version_info[0] >= 3 17 | if PY3: 18 | DISPATCH_TIME_FOREVER = sys.maxsize 19 | else: 20 | DISPATCH_TIME_FOREVER = sys.maxint 21 | 22 | dispatch_semaphore_create = c.dispatch_semaphore_create 23 | dispatch_semaphore_create.restype = ctypes.c_void_p 24 | dispatch_semaphore_create.argtypes = [ctypes.c_int] 25 | 26 | dispatch_semaphore_wait = c.dispatch_semaphore_wait 27 | dispatch_semaphore_wait.restype = ctypes.c_long 28 | dispatch_semaphore_wait.argtypes = [ctypes.c_void_p, ctypes.c_uint64] 29 | 30 | dispatch_semaphore_signal = c.dispatch_semaphore_signal 31 | dispatch_semaphore_signal.restype = ctypes.c_long 32 | dispatch_semaphore_signal.argtypes = [ctypes.c_void_p] 33 | 34 | 35 | def is_available(): 36 | context = LAContext.new() 37 | return context.canEvaluatePolicy_error_(kTouchIdPolicy, None)[0] 38 | 39 | 40 | def authenticate(reason='authenticate via Touch ID'): 41 | context = LAContext.new() 42 | 43 | can_evaluate = context.canEvaluatePolicy_error_(kTouchIdPolicy, None)[0] 44 | if not can_evaluate: 45 | raise Exception("Touch ID isn't available on this machine") 46 | 47 | sema = dispatch_semaphore_create(0) 48 | 49 | # we can't reassign objects from another scope, but we can modify them 50 | res = {'success': False, 'error': None} 51 | 52 | def cb(_success, _error): 53 | res['success'] = _success 54 | if _error: 55 | res['error'] = _error.localizedDescription() 56 | dispatch_semaphore_signal(sema) 57 | 58 | context.evaluatePolicy_localizedReason_reply_(kTouchIdPolicy, reason, cb) 59 | dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER) 60 | 61 | if res['error']: 62 | raise Exception(res['error']) 63 | 64 | return res['success'] 65 | --------------------------------------------------------------------------------