First working go version

This commit is contained in:
IamTheFij 2020-12-10 11:42:50 -05:00
parent b01ca5eefe
commit 480936c085
18 changed files with 595 additions and 657 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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))

View File

@ -1 +0,0 @@
0.0.1

14
go.mod Normal file
View File

@ -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
)

60
go.sum Normal file
View File

@ -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=

View File

@ -3,10 +3,10 @@
<plist version="1.0">
<dict>
<key>bundleid</key>
<string>com.vividboarder.alfred-yubico-auth</string>
<string>com.iamthefij.alfred-yubico-auth</string>
<key>connections</key>
<dict>
<key>8486DCAA-AFB7-407D-A0E9-E57E09997B24</key>
<key>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</key>
<array>
<dict>
<key>destinationuid</key>
@ -18,6 +18,21 @@
<key>vitoclose</key>
<false/>
</dict>
<dict>
<key>destinationuid</key>
<string>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>sourceoutputuid</key>
<string>94F60406-01FF-4991-A697-2C83147293EB</string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>8486DCAA-AFB7-407D-A0E9-E57E09997B24</key>
<array>
<dict>
<key>destinationuid</key>
<string>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</string>
@ -28,6 +43,16 @@
<key>vitoclose</key>
<false/>
</dict>
<dict>
<key>destinationuid</key>
<string>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</key>
<array>
@ -42,6 +67,19 @@
<false/>
</dict>
</array>
<key>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</key>
<array>
<dict>
<key>destinationuid</key>
<string>C252A5EC-1AEE-4EF4-864F-67483EAADCFA</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>BFB3A122-52BB-4FF1-B5B3-CECD42A730DB</key>
<array>
<dict>
@ -81,9 +119,22 @@
<false/>
</dict>
</array>
<key>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</key>
<array>
<dict>
<key>destinationuid</key>
<string>E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
</dict>
<key>createdby</key>
<string>ViViDboarder</string>
<string>Ian Fijolek</string>
<key>description</key>
<string>2FA for Yubikeys</string>
<key>disabled</key>
@ -96,18 +147,69 @@
<key>config</key>
<dict>
<key>autopaste</key>
<false/>
<true/>
<key>clipboardtext</key>
<string>{query}</string>
<key>ignoredynamicplaceholders</key>
<false/>
<key>transient</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.output.clipboard</string>
<key>uid</key>
<string>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</string>
<string>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</string>
<key>version</key>
<integer>2</integer>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>alfredfiltersresults</key>
<true/>
<key>alfredfiltersresultsmatchmode</key>
<integer>0</integer>
<key>argumenttreatemptyqueryasnil</key>
<false/>
<key>argumenttrimmode</key>
<integer>0</integer>
<key>argumenttype</key>
<integer>1</integer>
<key>escaping</key>
<integer>0</integer>
<key>keyword</key>
<string>yubikey</string>
<key>queuedelaycustom</key>
<integer>3</integer>
<key>queuedelayimmediatelyinitially</key>
<true/>
<key>queuedelaymode</key>
<integer>0</integer>
<key>queuemode</key>
<integer>1</integer>
<key>runningsubtext</key>
<string></string>
<key>script</key>
<string>./alfred-yubico-go</string>
<key>scriptargtype</key>
<integer>1</integer>
<key>scriptfile</key>
<string></string>
<key>subtext</key>
<string>Get 2FA tokens from Yubikey</string>
<key>title</key>
<string>Yubikey 2FA</string>
<key>type</key>
<integer>0</integer>
<key>withspace</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.input.scriptfilter</string>
<key>uid</key>
<string>8486DCAA-AFB7-407D-A0E9-E57E09997B24</string>
<key>version</key>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
@ -137,6 +239,8 @@
<false/>
<key>clipboardtext</key>
<string></string>
<key>ignoredynamicplaceholders</key>
<false/>
<key>transient</key>
<false/>
</dict>
@ -145,7 +249,39 @@
<key>uid</key>
<string>BFB3A122-52BB-4FF1-B5B3-CECD42A730DB</string>
<key>version</key>
<integer>2</integer>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>seconds</key>
<string>10</string>
</dict>
<key>type</key>
<string>alfred.workflow.utility.delay</string>
<key>uid</key>
<string>E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>autopaste</key>
<false/>
<key>clipboardtext</key>
<string>{query}</string>
<key>ignoredynamicplaceholders</key>
<false/>
<key>transient</key>
<true/>
</dict>
<key>type</key>
<string>alfred.workflow.output.clipboard</string>
<key>uid</key>
<string>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</string>
<key>version</key>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
@ -171,80 +307,77 @@
<dict>
<key>config</key>
<dict>
<key>alfredfiltersresults</key>
<true/>
<key>alfredfiltersresultsmatchmode</key>
<integer>0</integer>
<key>argumenttrimmode</key>
<integer>0</integer>
<key>argumenttype</key>
<integer>1</integer>
<key>escaping</key>
<integer>0</integer>
<key>keyword</key>
<string>yubikey</string>
<key>queuedelaycustom</key>
<integer>3</integer>
<key>queuedelayimmediatelyinitially</key>
<true/>
<key>queuedelaymode</key>
<integer>0</integer>
<key>queuemode</key>
<integer>1</integer>
<key>runningsubtext</key>
<string></string>
<key>script</key>
<string>./venv/bin/python -m alfred_yauth.main</string>
<key>scriptargtype</key>
<integer>1</integer>
<key>scriptfile</key>
<string>main.py</string>
<key>subtext</key>
<string>Get 2FA tokens from Yubikey</string>
<key>title</key>
<string>Yubikey 2FA</string>
<key>type</key>
<integer>0</integer>
<key>withspace</key>
<true/>
<key>conditions</key>
<array>
<dict>
<key>inputstring</key>
<string>{var:action}</string>
<key>matchcasesensitive</key>
<false/>
<key>matchmode</key>
<integer>0</integer>
<key>matchstring</key>
<string>set-password</string>
<key>outputlabel</key>
<string>Set password</string>
<key>uid</key>
<string>94F60406-01FF-4991-A697-2C83147293EB</string>
</dict>
</array>
<key>elselabel</key>
<string>else</string>
</dict>
<key>type</key>
<string>alfred.workflow.input.scriptfilter</string>
<string>alfred.workflow.utility.conditional</string>
<key>uid</key>
<string>8486DCAA-AFB7-407D-A0E9-E57E09997B24</string>
<key>version</key>
<integer>2</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>seconds</key>
<string>10</string>
</dict>
<key>type</key>
<string>alfred.workflow.utility.delay</string>
<key>uid</key>
<string>E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E</string>
<string>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>autopaste</key>
<true/>
<key>clipboardtext</key>
<string>{query}</string>
<key>transient</key>
<true/>
<key>concurrently</key>
<false/>
<key>escaping</key>
<integer>102</integer>
<key>script</key>
<string>./alfred-yubico-go set-password</string>
<key>scriptargtype</key>
<integer>1</integer>
<key>scriptfile</key>
<string></string>
<key>type</key>
<integer>0</integer>
</dict>
<key>type</key>
<string>alfred.workflow.output.clipboard</string>
<string>alfred.workflow.action.script</string>
<key>uid</key>
<string>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</string>
<string>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</string>
<key>version</key>
<integer>2</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>lastpathcomponent</key>
<false/>
<key>onlyshowifquerypopulated</key>
<false/>
<key>removeextension</key>
<false/>
<key>text</key>
<string>Password key is now stored in your keychain</string>
<key>title</key>
<string>Password saved</string>
</dict>
<key>type</key>
<string>alfred.workflow.output.notification</string>
<key>uid</key>
<string>C252A5EC-1AEE-4EF4-864F-67483EAADCFA</string>
<key>version</key>
<integer>1</integer>
</dict>
</array>
<key>readme</key>
<string></string>
@ -253,9 +386,16 @@
<key>0718204D-3398-4AEF-A621-DDDE1FC6ED75</key>
<dict>
<key>xpos</key>
<integer>960</integer>
<integer>990</integer>
<key>ypos</key>
<integer>140</integer>
<integer>145</integer>
</dict>
<key>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</key>
<dict>
<key>xpos</key>
<integer>270</integer>
<key>ypos</key>
<integer>245</integer>
</dict>
<key>8486DCAA-AFB7-407D-A0E9-E57E09997B24</key>
<dict>
@ -267,37 +407,51 @@
<key>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</key>
<dict>
<key>xpos</key>
<integer>360</integer>
<integer>415</integer>
<key>ypos</key>
<integer>140</integer>
<integer>235</integer>
</dict>
<key>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</key>
<dict>
<key>xpos</key>
<integer>390</integer>
<key>ypos</key>
<integer>415</integer>
</dict>
<key>BFB3A122-52BB-4FF1-B5B3-CECD42A730DB</key>
<dict>
<key>xpos</key>
<integer>780</integer>
<integer>810</integer>
<key>ypos</key>
<integer>140</integer>
<integer>145</integer>
</dict>
<key>C252A5EC-1AEE-4EF4-864F-67483EAADCFA</key>
<dict>
<key>xpos</key>
<integer>595</integer>
<key>ypos</key>
<integer>420</integer>
</dict>
<key>DA3E9CE8-7C4F-4B09-BFA5-F8CA83297968</key>
<dict>
<key>xpos</key>
<integer>540</integer>
<integer>590</integer>
<key>ypos</key>
<integer>140</integer>
<integer>235</integer>
</dict>
<key>E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E</key>
<dict>
<key>xpos</key>
<integer>700</integer>
<integer>730</integer>
<key>ypos</key>
<integer>170</integer>
<integer>175</integer>
</dict>
<key>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</key>
<dict>
<key>xpos</key>
<integer>360</integer>
<integer>455</integer>
<key>ypos</key>
<integer>290</integer>
<integer>60</integer>
</dict>
</dict>
<key>version</key>

123
magefile.go Normal file
View File

@ -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") }

129
main.go Normal file
View File

@ -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()
}

18
password-prompt.js Normal file
View File

@ -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
}

View File

@ -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"

View File

@ -1,2 +0,0 @@
alfred-workflow==1.27
yubikey-manager==0.4.0

View File

@ -1,4 +1,3 @@
#! /bin/bash
echo "$1" > ./alfred_yauth/version
plutil -replace version -string "$1" ./info.plist

View File

@ -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',
)

31
tox.ini
View File

@ -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