diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de321a1..ad9d071 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,16 @@ -- repo: git://github.com/pre-commit/pre-commit-hooks - sha: 1553c96e2a0d0154f3aca4c5cb0156a74a8c703d +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.3.0 hooks: - - id: trailing-whitespace - - id: end-of-file-fixer - - id: autopep8-wrapper - exclude: ./src/vendor/.+ - args: - - -i - - --ignore=E265,E309,E501 - - id: debug-statements - exclude: ./src/vendor/.+ - language_version: python2.7 - - id: flake8 - exclude: ./src/vendor/.+ - language_version: python2.7 - - id: check-yaml - - id: check-json - - id: name-tests-test - exclude: tests/(common.py|util.py|(helpers)/(.+).py) -- repo: git://github.com/asottile/reorder_python_imports - sha: ab609b9b982729dfc287b4e75963c0c4de254a31 + - id: check-added-large-files + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - repo: git://github.com/dnephin/pre-commit-golang + rev: v0.3.5 hooks: - - id: reorder-python-imports - exclude: ./src/vendor/.+ - language_version: python2.7 + - id: go-fmt + - id: go-imports + # - id: gometalinter + # - id: golangci-lint diff --git a/Makefile b/Makefile deleted file mode 100644 index d0ad371..0000000 --- a/Makefile +++ /dev/null @@ -1,48 +0,0 @@ -.PHONY: default -default: run - -.PHONY: build -build: venv install-ports - -.PHONY: install -install: venv - ./replace-workflow.sh - -Yauth.alfredWorkflow: venv - mkdir Yauth.alfredWorkflow - cp -r alfred_yauth Yauth.alfredworkflow/ - cp -r venv Yauth.alfredWorkflow/ - cp info.plist Yauth.alfredWorkflow/ - cp icon.png Yauth.alfredWorkflow/ - -# Installs required MacPorts -.PHONY: install-ports -install-ports: - sudo port install swig swig-python ykpers libu2f-host libusb - -# Creates venv using MacPorts Python (Required for it to refrence libusb) -venv: - virtualenv --python=/opt/local/bin/python2.7 venv - ./venv/bin/pip install -r ./requirements.txt - -# Simple execution of the workflow to see all results -.PHONY: run -run: venv - @./venv/bin/python -m alfred_yauth.main - -# Runs workflow and prompts for Yubikey password -.PHONY: set-password -set-password: venv - @./venv/bin/python -m alfred_yauth.main set-password - -# Clears the virtualenv and other installed files -.PHONY: clean -clean: - rm -fr venv Yauth.alfredWorkflow - find . -name '*.pyc' -delete - find . -name '__pycache__' -delete - -# Install precommit hooks -.PHONY: intall-hooks -install-hooks: - tox -e pre-commit -- install -f --install-hooks diff --git a/README.md b/README.md index 7f25b56..662c68a 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,13 @@ # Alfred Yubico Auth -An Alfred Workflow for auto filling authentication codes stored on your Yubikey. +This workflow allows quick searching and filling, and copying of OTP codes from a supported Yubikey. -## Notes +## Building -This is definitely a work in progress. There are a lot of rough edges yet to be polished, but here it goes. +Building requires [`mage`](https://magefile.org/) -* Requires some to be installed with a package manager -* There is no way to input your key password through the UI yet. Do that with `make set-password` and then it should work fine. -* Error handling is terrible right now. If things don't work, check the debug log in Alfred - -## Installation - -Clone this repo - -```bash -git clone https://git.iamthefij.com/iamthefij/alfred-yubico-auth.git -``` - -Either install your dependencies manually or, if you have MacPorts, you can use: - -```bash -make install-ports -``` - -Otherwise you need to install `swig swig-python ykpers libu2f-host libusb` some other way. - -Finally up the virtualenv and install to your Alfred with - -```bash -make install -``` +To see all targets and their descriptions, run `mage -l`. ## Credits -Uses the amazing [deanishe/alfred-workflow](https://github.com/deanishe/alfred-workflow) package +This uses [deanishe/awgo](https://github.com/deanishe/awgo) to interface with Alfred and [yawn/ykoath](https://github.com/yawn/ykoath) for interracting with the Yubikey diff --git a/alfred_yauth/__init__.py b/alfred_yauth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/alfred_yauth/controller.py b/alfred_yauth/controller.py deleted file mode 100644 index d31b514..0000000 --- a/alfred_yauth/controller.py +++ /dev/null @@ -1,235 +0,0 @@ -import hashlib -from binascii import a2b_hex -from binascii import b2a_hex - -from ykman.descriptor import get_descriptors -from ykman.driver_ccid import APDUError -from ykman.driver_otp import YkpersError -from ykman.oath import Credential -from ykman.oath import OathController -from ykman.oath import SW -from ykman.util import CAPABILITY -from ykman.util import derive_key -from ykman.util import parse_b32_key -from ykman.util import TRANSPORT - -NON_FEATURE_CAPABILITIES = [CAPABILITY.CCID, CAPABILITY.NFC] - - -class DeviceNotFoundError(Exception): - pass - - -class CouldNotOpenDeviceError(Exception): - pass - - -class Controller(object): - _descriptor = None - _dev_info = None - - def get_features(self): - return [ - c.name for c in CAPABILITY if c not in NON_FEATURE_CAPABILITIES] - - def count_devices(self): - return len(list(get_descriptors())) - - def refresh(self): - descriptors = list(get_descriptors()) - if len(descriptors) != 1: - self._descriptor = None - raise DeviceNotFoundError() - return - - desc = descriptors[0] - if desc.fingerprint != ( - self._descriptor.fingerprint if self._descriptor else None): - dev = desc.open_device() - if not dev: - raise CouldNotOpenDeviceError() - return - self._descriptor = desc - self._dev_info = { - 'name': dev.device_name, - 'version': '.'.join(str(x) for x in dev.version), - 'serial': dev.serial or '', - 'enabled': [c.name for c in CAPABILITY if c & dev.enabled], - 'connections': [ - t.name for t in TRANSPORT if t & dev.capabilities], - } - - return self._dev_info - - def refresh_credentials(self, timestamp, password_key=None): - return [ - c.to_dict() for c in self._calculate_all(timestamp, password_key)] - - def calculate(self, credential, timestamp, password_key): - return self._calculate( - Credential.from_dict( - credential), timestamp, password_key).to_dict() - - def calculate_slot_mode(self, slot, digits, timestamp): - dev = self._descriptor.open_device(TRANSPORT.OTP) - code = dev.driver.calculate( - slot, challenge=timestamp, totp=True, digits=int(digits), - wait_for_touch=True) - return Credential( - self._slot_name(slot), code=code, oath_type='totp', touch=True, - algo='SHA1', expiration=self._expiration(timestamp)).to_dict() - - def refresh_slot_credentials(self, slots, digits, timestamp): - result = [] - if slots[0]: - cred = self._read_slot_cred(1, digits[0], timestamp) - if cred: - result.append(cred) - if slots[1]: - cred = self._read_slot_cred(2, digits[1], timestamp) - if cred: - result.append(cred) - return [c.to_dict() for c in result] - - def _read_slot_cred(self, slot, digits, timestamp): - try: - dev = self._descriptor.open_device(TRANSPORT.OTP) - code = dev.driver.calculate( - slot, challenge=timestamp, totp=True, digits=int(digits), - wait_for_touch=False) - return Credential( - self._slot_name(slot), code=code, oath_type='totp', - touch=False, algo='SHA1', - expiration=self._expiration(timestamp)) - except YkpersError as e: - if e.errno == 11: - return Credential( - self._slot_name(slot), oath_type='totp', touch=True, - algo='SHA1') - except: - pass - return None - - def _slot_name(self, slot): - return "YubiKey Slot {}".format(slot) - - def _expiration(self, timestamp): - return ((timestamp + 30) // 30) * 30 - - def needs_validation(self): - try: - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - return controller.locked - except: - return False - - def get_oath_id(self): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - return b2a_hex(controller.id).decode('utf-8') - - def derive_key(self, password): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - key = derive_key(controller.id, password) - return b2a_hex(key).decode('utf-8') - - def validate(self, key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if key is not None: - try: - controller.validate(a2b_hex(key)) - return True - except: - return False - - def set_password(self, new_password, password_key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if controller.locked and password_key is not None: - controller.validate(a2b_hex(password_key)) - if new_password is not None: - key = derive_key(controller.id, new_password) - controller.set_password(key) - else: - controller.clear_password() - - def add_credential( - self, name, key, oath_type, digits, algo, touch, password_key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if controller.locked and password_key is not None: - controller.validate(a2b_hex(password_key)) - try: - key = parse_b32_key(key) - except Exception as e: - return str(e) - try: - controller.put( - key, name, oath_type, digits, algo=algo, require_touch=touch) - except APDUError as e: - # NEO doesn't return a no space error if full, - # but a command aborted error. Assume it's because of - # no space in this context. - if e.sw == SW.NO_SPACE or e.sw == SW.COMMAND_ABORTED: - return 'No space' - else: - raise - - def add_slot_credential(self, slot, key, touch): - dev = self._descriptor.open_device(TRANSPORT.OTP) - key = parse_b32_key(key) - if len(key) > 64: # Keys longer than 64 bytes are hashed. - key = hashlib.sha1(key).digest() - if len(key) > 20: - raise ValueError( - 'YubiKey Slots cannot handle TOTP keys over 20 bytes.') - key += b'\x00' * (20 - len(key)) # Keys must be padded to 20 bytes. - dev.driver.program_chalresp(int(slot), key, touch) - - def delete_slot_credential(self, slot): - dev = self._descriptor.open_device(TRANSPORT.OTP) - dev.driver.zap_slot(slot) - - def delete_credential(self, credential, password_key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if controller.locked and password_key is not None: - controller.validate(a2b_hex(password_key)) - controller.delete(Credential.from_dict(credential)) - - def _calculate(self, credential, timestamp, password_key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if controller.locked and password_key is not None: - controller.validate(a2b_hex(password_key)) - cred = controller.calculate(credential, timestamp) - return cred - - def _calculate_all(self, timestamp, password_key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if controller.locked and password_key is not None: - controller.validate(a2b_hex(password_key)) - creds = controller.calculate_all(timestamp) - creds = [c for c in creds if not c.hidden] - return creds - - def reset(self): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - controller.reset() - - def slot_status(self): - dev = self._descriptor.open_device(TRANSPORT.OTP) - return list(dev.driver.slot_status) - - def list_credentials(self, password_key): - dev = self._descriptor.open_device(TRANSPORT.CCID) - controller = OathController(dev.driver) - if controller.locked and password_key is not None: - controller.validate(a2b_hex(password_key)) - creds = controller.list() - return [c.to_dict() for c in creds] diff --git a/alfred_yauth/main.py b/alfred_yauth/main.py deleted file mode 100644 index 3e54e4e..0000000 --- a/alfred_yauth/main.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- -import sys -from getpass import getpass -from time import time - -from controller import APDUError -from controller import Controller -from controller import DeviceNotFoundError -from workflow import ICON_ACCOUNT -from workflow import ICON_ERROR -from workflow import Workflow3 - - -YUBIKEY_CREDS_KEYCHAIN = 'yubico-auth-creds' - - -def cred_to_item_kwargs(cred): - if cred.get('hidden'): - return None - return { - 'icon': ICON_ACCOUNT, - 'title': cred['name'], - 'subtitle': 'Copy to clipboard', - 'copytext': cred['code'], - 'arg': cred['code'], - 'valid': True, - } - - -class YubicoAuth(Workflow3): - _controller = None - - def get_controller(self): - if not self._controller: - self._controller = Controller() - self._controller.refresh() - return self._controller - - def ask_yubikey_password(self): - """Prompts the user for their Yubikey password and stores it""" - self.logger.debug('Set password') - password_key = self.get_controller().derive_key( - getpass('Yubikey Password:') - ) - self.save_password(YUBIKEY_CREDS_KEYCHAIN, password_key) - - self.get_controller().refresh_credentials(time(), password_key) - self.add_item( - 'Yubikey password set successfully', - '', - icon=ICON_ACCOUNT, - ) - - def get_yubikey_password(self): - """Returns stored Yubikey password from keychain""" - return self.get_password(YUBIKEY_CREDS_KEYCHAIN) - - def _get_positional_arg(self, position): - """Safely return a positional argument""" - if len(self.args) > position: - return self.args[position] - return None - - def get_command(self): - """Get command out of the args as first parameter""" - return self._get_positional_arg(0) - - def get_query(self): - """Get query out of the args after first parameter""" - if len(self.args) < 2: - return None - return ' '.join(self.args[1:]) - - def _validate(self, command): - """Validates that we can handle the current command""" - # if self.get_api_key() is None: - # self.add_item( - # title='Missing API key', - # subtitle='Set variable in settings', - # icon=ICON_ACCOUNT, - # valid=False, - # ) - # return False - # if command == COMMAND_LOVE and self.get_recipient() is None: - # self.add_item( - # title='Recipient is required', - # icon=ICON_ERROR, - # valid=False, - # ) - # return False - return True - - def _add_cred_to_results(self, cred): - self.logger.debug('Read {}'.format(cred.get('name'))) - item_args = cred_to_item_kwargs(cred) - if item_args: - self.add_item(**item_args) - - def list_credentials(self): - password_key = self.get_yubikey_password() - for cred in self.get_controller().list_credentials(password_key): - self._add_cred_to_results(cred) - - def refresh_credentials(self): - key = self.get_yubikey_password() - for cred in self.get_controller().refresh_credentials(time(), key): - self._add_cred_to_results(cred) - - def main(self): - self.logger.debug('Starting...') - command = self.get_command() - - if not self._validate(command): - self.send_feedback() - return - - command_action = None - if command == 'set-password': - command_action = self.ask_yubikey_password - elif command == 'list': - command_action = self.list_credentials - else: - command_action = self.refresh_credentials - - try: - command_action() - except DeviceNotFoundError: - self.add_item( - 'Could not find device', - 'Is your Yubikey plugged in?', - icon=ICON_ERROR, - ) - except APDUError: - self.add_item( - 'Could not communicate with device', - 'Is your Yubikey password set correctly?', - icon=ICON_ERROR, - ) - - self.send_feedback() - - -def no_wf(): - controller = Controller() - print(controller.get_features()) - print(controller.count_devices()) - print(controller.refresh()) - - password = getpass('YubiKey password?') - password_key = controller.derive_key(password) - timestamp = time() - print(controller.refresh_credentials(timestamp, password_key)) - creds = controller.list_credentials(password_key) - - print(creds) - - -def main(wf=None): - if wf is None: - no_wf() - else: - wf.main() - - -if __name__ == '__main__': - # main() - wf = YubicoAuth() - sys.exit(wf.run(main)) diff --git a/alfred_yauth/version b/alfred_yauth/version deleted file mode 100644 index 8acdd82..0000000 --- a/alfred_yauth/version +++ /dev/null @@ -1 +0,0 @@ -0.0.1 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ff16f08 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module git.iamthefij.com/iamthefij/alfred-yubico-auth + +go 1.15 + +// Right now requires github.com/vividboarder/ykauth branch: validate +replace github.com/yawn/ykoath => ../ykoath + +require ( + git.iamthefij.com/iamthefij/slog v1.0.0 + github.com/deanishe/awgo v0.27.1 + github.com/magefile/mage v1.10.0 + github.com/yawn/ykoath v1.0.4 + golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f54f404 --- /dev/null +++ b/go.sum @@ -0,0 +1,60 @@ +git.iamthefij.com/iamthefij/slog v0.0.0-20201202020306-f6ae8b7d5a96 h1:uIr3Bz44Unzft7f2mUsYiF/wFB4lrCl82GUFfIlZCd4= +git.iamthefij.com/iamthefij/slog v0.0.0-20201202020306-f6ae8b7d5a96/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg= +git.iamthefij.com/iamthefij/slog v1.0.0 h1:S+njoK+dr5VUYSopISHm2QMq3IwrHfwmi/CrAmhXVbg= +git.iamthefij.com/iamthefij/slog v1.0.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg= +github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY= +github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deanishe/awgo v0.27.1 h1:Vf8v7yaGWN3fibT+db1Mfw95Q/rD/1Qbf4ahGa4tMSY= +github.com/deanishe/awgo v0.27.1/go.mod h1:Qen3509y1/sj7a5syefWc6FQHO1LE/tyoTyN9jRv1vU= +github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 h1:OM0MnUcXBysj7ZtXvThVWHMoahuKQ8FuwIdeSLcNdP4= +github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95/go.mod h1:8hHvF8DlEq5kE3KWOsZQezdWq1OTOVxZArZMscS954E= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= +github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yawn/ykoath v1.0.4 h1:kGYMr7um0mmLl/lnpgeH7+waQL9WyncsamMsVT/KWbQ= +github.com/yawn/ykoath v1.0.4/go.mod h1:dcXMmLrvt6WFkySkG2k8ZEqxiTbu/TWSI4+/Cb54+Lg= +go.deanishe.net/env v0.5.1 h1:WiOncK5uJj8Um57Vj2dc1bq1lMN7fgRag9up7I3LZy0= +go.deanishe.net/env v0.5.1/go.mod h1:ihEYfDm0K0hq3f5ACTCQDrMTWxH9fTiA1lh1i0aMqm0= +go.deanishe.net/fuzzy v1.0.0 h1:3Qp6PCX0DLb9z03b5OHwAGsbRSkgJpSLncsiDdXDt4Y= +go.deanishe.net/fuzzy v1.0.0/go.mod h1:2yEEMfG7jWgT1s5EO0TteVWmx2MXFBRMr5cMm84bQNY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= +howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= diff --git a/info.plist b/info.plist index 23c4076..aa3f73a 100644 --- a/info.plist +++ b/info.plist @@ -3,10 +3,10 @@ bundleid - com.vividboarder.alfred-yubico-auth + com.iamthefij.alfred-yubico-auth connections - 8486DCAA-AFB7-407D-A0E9-E57E09997B24 + 2D69982A-0DB6-4ABA-812F-C7F2A73650AE destinationuid @@ -18,6 +18,21 @@ vitoclose + + destinationuid + A8D2CCAC-5CA4-495E-BB62-5C7F596FA157 + modifiers + 0 + modifiersubtext + + sourceoutputuid + 94F60406-01FF-4991-A697-2C83147293EB + vitoclose + + + + 8486DCAA-AFB7-407D-A0E9-E57E09997B24 + destinationuid FB0DDF80-FF90-439A-BF3F-6EC58C2AA870 @@ -28,6 +43,16 @@ vitoclose + + destinationuid + 2D69982A-0DB6-4ABA-812F-C7F2A73650AE + modifiers + 0 + modifiersubtext + + vitoclose + + 9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD @@ -42,6 +67,19 @@ + A8D2CCAC-5CA4-495E-BB62-5C7F596FA157 + + + destinationuid + C252A5EC-1AEE-4EF4-864F-67483EAADCFA + modifiers + 0 + modifiersubtext + + vitoclose + + + BFB3A122-52BB-4FF1-B5B3-CECD42A730DB @@ -81,9 +119,22 @@ + FB0DDF80-FF90-439A-BF3F-6EC58C2AA870 + + + destinationuid + E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E + modifiers + 0 + modifiersubtext + + vitoclose + + + createdby - ViViDboarder + Ian Fijolek description 2FA for Yubikeys disabled @@ -96,18 +147,69 @@ config autopaste - + clipboardtext {query} + ignoredynamicplaceholders + transient type alfred.workflow.output.clipboard uid - 9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD + FB0DDF80-FF90-439A-BF3F-6EC58C2AA870 version - 2 + 3 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 1 + escaping + 0 + keyword + yubikey + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + + script + ./alfred-yubico-go + scriptargtype + 1 + scriptfile + + subtext + Get 2FA tokens from Yubikey + title + Yubikey 2FA + type + 0 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 8486DCAA-AFB7-407D-A0E9-E57E09997B24 + version + 3 config @@ -137,6 +239,8 @@ clipboardtext + ignoredynamicplaceholders + transient @@ -145,7 +249,39 @@ uid BFB3A122-52BB-4FF1-B5B3-CECD42A730DB version - 2 + 3 + + + config + + seconds + 10 + + type + alfred.workflow.utility.delay + uid + E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E + version + 1 + + + config + + autopaste + + clipboardtext + {query} + ignoredynamicplaceholders + + transient + + + type + alfred.workflow.output.clipboard + uid + 9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD + version + 3 config @@ -171,80 +307,77 @@ config - alfredfiltersresults - - alfredfiltersresultsmatchmode - 0 - argumenttrimmode - 0 - argumenttype - 1 - escaping - 0 - keyword - yubikey - queuedelaycustom - 3 - queuedelayimmediatelyinitially - - queuedelaymode - 0 - queuemode - 1 - runningsubtext - - script - ./venv/bin/python -m alfred_yauth.main - scriptargtype - 1 - scriptfile - main.py - subtext - Get 2FA tokens from Yubikey - title - Yubikey 2FA - type - 0 - withspace - + conditions + + + inputstring + {var:action} + matchcasesensitive + + matchmode + 0 + matchstring + set-password + outputlabel + Set password + uid + 94F60406-01FF-4991-A697-2C83147293EB + + + elselabel + else type - alfred.workflow.input.scriptfilter + alfred.workflow.utility.conditional uid - 8486DCAA-AFB7-407D-A0E9-E57E09997B24 - version - 2 - - - config - - seconds - 10 - - type - alfred.workflow.utility.delay - uid - E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E + 2D69982A-0DB6-4ABA-812F-C7F2A73650AE version 1 config - autopaste - - clipboardtext - {query} - transient - + concurrently + + escaping + 102 + script + ./alfred-yubico-go set-password + scriptargtype + 1 + scriptfile + + type + 0 type - alfred.workflow.output.clipboard + alfred.workflow.action.script uid - FB0DDF80-FF90-439A-BF3F-6EC58C2AA870 + A8D2CCAC-5CA4-495E-BB62-5C7F596FA157 version 2 + + config + + lastpathcomponent + + onlyshowifquerypopulated + + removeextension + + text + Password key is now stored in your keychain + title + Password saved + + type + alfred.workflow.output.notification + uid + C252A5EC-1AEE-4EF4-864F-67483EAADCFA + version + 1 + readme @@ -253,9 +386,16 @@ 0718204D-3398-4AEF-A621-DDDE1FC6ED75 xpos - 960 + 990 ypos - 140 + 145 + + 2D69982A-0DB6-4ABA-812F-C7F2A73650AE + + xpos + 270 + ypos + 245 8486DCAA-AFB7-407D-A0E9-E57E09997B24 @@ -267,37 +407,51 @@ 9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD xpos - 360 + 415 ypos - 140 + 235 + + A8D2CCAC-5CA4-495E-BB62-5C7F596FA157 + + xpos + 390 + ypos + 415 BFB3A122-52BB-4FF1-B5B3-CECD42A730DB xpos - 780 + 810 ypos - 140 + 145 + + C252A5EC-1AEE-4EF4-864F-67483EAADCFA + + xpos + 595 + ypos + 420 DA3E9CE8-7C4F-4B09-BFA5-F8CA83297968 xpos - 540 + 590 ypos - 140 + 235 E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E xpos - 700 + 730 ypos - 170 + 175 FB0DDF80-FF90-439A-BF3F-6EC58C2AA870 xpos - 360 + 455 ypos - 290 + 60 version diff --git a/magefile.go b/magefile.go new file mode 100644 index 0000000..0dae748 --- /dev/null +++ b/magefile.go @@ -0,0 +1,123 @@ +// +build mage + +package main + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/deanishe/awgo/util" + "github.com/deanishe/awgo/util/build" + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +var ( + info *build.Info + buildDir = "./build" + distDir = "./dist" + + // Default mage target + Default = Run +) + +func init() { + var err error + if info, err = build.NewInfo(); err != nil { + panic(err) + } +} + +// Build workflow +func Build() error { + mg.Deps(cleanBuild) + fmt.Println("Building...") + err := sh.RunWith(info.Env(), "go", "build", "-o", buildDir+"/alfred-yubico-go", ".") + if err != nil { + return err + } + + globs := build.Globs( + "*.png", + "info.plist", + "README.md", + "LICENSE.txt", + "password-prompt.js", + ) + + return build.SymlinkGlobs(buildDir, globs...) +} + +// Run workflow +func Run() error { + mg.Deps(Build) + fmt.Println("Running...") + return sh.RunWith(info.Env(), buildDir+"/alfred-yubico-go") +} + +// Dist packages workflow for distribution +func Dist() error { + mg.SerialDeps(Clean, Build) + p, err := build.Export(buildDir, distDir) + if err != nil { + return err + } + fmt.Printf("Exported %q\n", p) + return nil +} + +// Install symlinked workflow to Alfred +func Install() error { + mg.Deps(Build) + fmt.Printf("Installing (linking) %q to %q...\n", buildDir, info.InstallDir) + if err := sh.Rm(info.InstallDir); err != nil { + return err + } + return build.Symlink(info.InstallDir, buildDir, true) +} + +// InstallHooks will install pre-commit hooks +func InstallHooks() error { + return sh.RunV("pre-commit", "install", "--overwrite", "--install-hooks") +} + +// Check will run all pre-commit hooks +func Check() error { + return sh.RunV("pre-commit", "run", "--all-files") +} + +// Clean build files +func Clean() error { + fmt.Println("Cleaning...") + mg.Deps(cleanBuild, cleanMage) + return nil +} + +// DistClean build files and distribution files +func DistClean() error { + mg.Deps(Clean, cleanDist) + return nil +} + +func cleanDir(name string) error { + if !util.PathExists(name) { + return nil + } + + infos, err := ioutil.ReadDir(name) + if err != nil { + return err + } + + for _, fi := range infos { + if err := sh.Rm(filepath.Join(name, fi.Name())); err != nil { + return err + } + } + return nil +} + +func cleanBuild() error { return cleanDir(buildDir) } +func cleanDist() error { return cleanDir(distDir) } +func cleanMage() error { return sh.Run("mage", "-clean") } diff --git a/main.go b/main.go new file mode 100644 index 0000000..ec32dad --- /dev/null +++ b/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "flag" + + "git.iamthefij.com/iamthefij/slog" + aw "github.com/deanishe/awgo" + "github.com/deanishe/awgo/util" + "github.com/yawn/ykoath" + + "bytes" +) + +var ( + wf *aw.Workflow + oath *ykoath.OATH + keychainAccount = "yubico-auth-creds" +) + +func init() { + wf = aw.New() +} + +func main() { + wf.Run(run) +} + +func promptPassword() (string, error) { + out, err := util.Run("./password-prompt.js") + if err != nil { + return "", err + } + out = bytes.TrimRight(out, "\n") + return string(out), nil +} + +func setPassword(s *ykoath.Select) error { + passphrase, err := promptPassword() + if err != nil { + slog.Error("failed reading passphrase") + return err + } + key := s.DeriveKey(passphrase) + // TODO: test key before storing + err = wf.Keychain.Set(keychainAccount, string(key)) + if err != nil { + slog.Error("failed storing passphrase key in keychain") + return err + } + return nil +} + +func run() { + wf.Args() + flag.Parse() + + var err error + oath, err = ykoath.New() + if err != nil { + slog.Error("failed to iniatialize new oath: %v", err) + wf.FatalError(err) + } + defer oath.Close() + oath.Debug = slog.Debug + + // Select oath to begin + s, err := oath.Select() + if err != nil { + slog.Error("failed to select oath: %v", err) + wf.FatalError(err) + } + + // Check to see if we are trying to set a password + if flag.Arg(0) == "set-password" { + err = setPassword(s) + if err != nil { + wf.FatalError(err) + } + return + } + + // If required, authenticate with password from keychain + if s.Challenge != nil { + key, err := wf.Keychain.Get(keychainAccount) + if err != nil { + slog.Error("no key found in keychain but password is required") + wf.NewWarningItem("No password set", "↵ to set password"). + Var("action", "set-password"). + Valid(true) + wf.SendFeedback() + return + } + + ok, err := oath.Validate(s, []byte(key)) + slog.FatalOnErr(err, "validation failed") + if !ok { + panic("could not validate") + } + } + + if flag.Arg(0) == "list" { + // List names only + names, err := oath.List() + slog.FatalOnErr(err, "failed to list names") + for _, name := range names { + slog.Log(name.Name) + wf.NewItem(name.Name). + Valid(true) + } + } else { + // Default execution is to calculate all codes and return them in list + creds, err := oath.CalculateAll() + if err != nil { + slog.Error("failed to calculate all") + wf.FatalError(err) + } + + for cred, code := range creds { + slog.Log(cred) + wf.NewItem(cred). + Icon(aw.IconAccount). + Subtitle("Copy to clipboard"). + Arg(code). + Copytext(code). + Valid(true) + } + } + wf.SendFeedback() +} diff --git a/password-prompt.js b/password-prompt.js new file mode 100644 index 0000000..c71ba41 --- /dev/null +++ b/password-prompt.js @@ -0,0 +1,18 @@ +#! /usr/bin/osascript +// https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/PromptforText.html#//apple_ref/doc/uid/TP40016239-CH80-SW1 +function run(){ + var app = Application.currentApplication() + app.includeStandardAdditions = true + var response = app.displayDialog( + "Enter your Yubikey passphrase", + { + defaultAnswer: "", + withIcon: "stop", + buttons: ["Cancel", "Save"], + defaultButton: "Save", + cancelButton: "Cancel", + givingUpAfter: 120, + hiddenAnswer: true + }) + return response.textReturned +} diff --git a/replace-workflow.sh b/replace-workflow.sh deleted file mode 100755 index c64aeaf..0000000 --- a/replace-workflow.sh +++ /dev/null @@ -1,8 +0,0 @@ -#! /bin/bash -set -e - -echo "Warning! This will remove the workflow at the provided path and replace it with a link to this directory" -read -p "Path to workflow to replace: " existing_workflow - -rm -fr "$existing_workflow" -ln -s `pwd` "$existing_workflow" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7c25974..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -alfred-workflow==1.27 -yubikey-manager==0.4.0 diff --git a/set_version.sh b/set_version.sh index 9b5d69e..a5daa61 100755 --- a/set_version.sh +++ b/set_version.sh @@ -1,4 +1,3 @@ #! /bin/bash -echo "$1" > ./alfred_yauth/version plutil -replace version -string "$1" ./info.plist diff --git a/setup.py b/setup.py deleted file mode 100644 index 7748d22..0000000 --- a/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import unicode_literals - -import os - -from setuptools import find_packages -from setuptools import setup - - -def read_version(): - version_path = 'version' - try: - return open( - os.path.join(os.path.dirname(__file__), version_path) - ).read() - except: - pass - - -setup( - name='Yubico Auth Workflow', - version=read_version(), - description='Yubico Auth workflow for Alfred', - author='Ian Fijolek', - author_email='ian@iamthefij.com.com', - url='', - packages=find_packages(exclude=['tests*']), - install_requires=[], - license='MIT', -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 17e1c08..0000000 --- a/tox.ini +++ /dev/null @@ -1,31 +0,0 @@ -[tox] -envlist = py -indexserver = - default = https://pypi.python.org/simple/ - -[testenv] -deps = -rrequirements-dev.txt - -[testenv:py] -deps = {[testenv]deps} -commands = - coverage run --source=./src/,tests/ -m pytest --strict {posargs} - coverage report -m - pre-commit run --all-files - -[testenv:lint] -deps = {[testenv]deps} - flake8 -commands = flake8 . - -[testenv:pre-commit] -deps = pre-commit>=0.4.2 -commands = pre-commit {posargs} - -[flake8] -exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.ropeproject,.tox,docs,virtualenv_run -filename = *.py,*.wsgi -max-line-length = 80 - -[pytest] -norecursedirs = .* _darcs CVS docs virtualenv_run