Compare commits

...

17 Commits

Author SHA1 Message Date
IamTheFij 76be0d8967 Bump version to 1.1.1
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2023-04-04 09:42:37 -07:00
IamTheFij 45ffa8efbd Use fork of awgo supporting alfred5 2023-04-04 09:42:37 -07:00
IamTheFij cfb231b0c6 Update pre-commit hooks
continuous-integration/drone/push Build is failing Details
2023-04-04 08:58:58 -07:00
IamTheFij 5d07c50083 Update go version and build for alfred 5 2023-04-04 08:52:55 -07:00
IamTheFij 744d531d4d Change pre-commit url scheme
continuous-integration/drone/push Build is passing Details
2022-07-08 12:58:28 -07:00
IamTheFij 5fc62963e7 go fmt update
continuous-integration/drone/push Build is failing Details
2022-06-01 14:12:36 -07:00
IamTheFij de31d9ed3a Bump version to v1.1.0
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/tag Build is failing Details
2022-06-01 14:04:43 -07:00
IamTheFij e1fd24135f Add show code option 2022-06-01 14:04:24 -07:00
IamTheFij f164b3a0af Improve support for YK5
continuous-integration/drone/push Build is passing Details
2021-01-18 21:00:56 -08:00
IamTheFij fc3e8a356b Add drone pipeline
continuous-integration/drone/push Build is passing Details
2021-01-08 16:45:05 -05:00
IamTheFij fa21e10153 Bump version to v1.0.0 2021-01-04 13:40:16 -05:00
IamTheFij a8215904a5 Add additional build instructions 2021-01-04 13:34:27 -05:00
IamTheFij 17b3eaa569 Add ykoath submodule 2021-01-04 13:34:08 -05:00
IamTheFij 96b4aaccfc Update readme to describe what I've tested with 2021-01-04 13:24:15 -05:00
IamTheFij c7ad5cb60c Add license file 2021-01-04 13:06:30 -05:00
IamTheFij aa260324d0 Rename output and bump version to v0.1.0 2020-12-10 11:48:39 -05:00
IamTheFij 480936c085 First working go version 2020-12-10 11:42:50 -05:00
22 changed files with 1164 additions and 667 deletions

31
.drone.yml Normal file
View File

@ -0,0 +1,31 @@
---
kind: pipeline
name: test
steps:
- name: check
image: iamthefij/drone-pre-commit:personal
---
kind: pipeline
name: notify
depends_on:
- test
trigger:
status:
- failure
steps:
- name: notify
image: drillster/drone-email
settings:
host:
from_secret: SMTP_HOST # pragma: whitelist secret
username:
from_secret: SMTP_USER # pragma: whitelist secret
password:
from_secret: SMTP_PASS # pragma: whitelist secret
from: drone@iamthefij.com

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "ykoath"]
path = ykoath
url = https://github.com/vividboarder/ykoath

View File

@ -1,26 +1,13 @@
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: 1553c96e2a0d0154f3aca4c5cb0156a74a8c703d
---
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.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: https://github.com/golangci/golangci-lint
rev: v1.52.2
hooks:
- id: reorder-python-imports
exclude: ./src/vendor/.+
language_version: python2.7
- id: golangci-lint

19
LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2020 Ian Fijolek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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,23 @@
# 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
So far, it has been tested with a Yubikey NEO on a MacBook Pro running macOS Catalina. I have no other devices to test with, but bug reports and patches may still be reviewed.
This is definitely a work in progress. There are a lot of rough edges yet to be polished, but here it goes.
## Cloning
* 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
Currently this package depends on a fork of [yawn/ykoath](https://github.com/yawn/ykoath). To allow this to be built directly from this repo, the fork is added as a git submodule. This can be cloned using `git clone --recurse-submodules` or cloning normally and then executing `git submodule update --init`. Once the change has been merged upstream, the submodule and this notice will go away.
## Installation
## Building
Clone this repo
Building requires [`mage`](https://magefile.org/)
```bash
git clone https://git.iamthefij.com/iamthefij/alfred-yubico-auth.git
```
To see all targets and their descriptions, run `mage -l`. The most basic ones are as follows:
Either install your dependencies manually or, if you have MacPorts, you can use:
* `mage install`: Build and install into your local machine for testing
* `mage dist`: Build bundle for distribution
```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
```
## 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

27
go.mod Normal file
View File

@ -0,0 +1,27 @@
module git.iamthefij.com/iamthefij/alfred-yubico-auth
go 1.20
// Right now requires https://github.com/vividboarder/ykoath branch: validate
replace github.com/yawn/ykoath => ./ykoath
// Right now requires https://github.com/iamthefij/awgo branch: alfred-5
replace github.com/deanishe/awgo => github.com/iamthefij/awgo v0.29.1-pre1
require (
git.iamthefij.com/iamthefij/slog v1.0.0
github.com/deanishe/awgo v0.29.1
github.com/magefile/mage v1.14.0
github.com/yawn/ykoath v1.0.4
)
require (
github.com/bmatcuk/doublestar v1.3.4 // indirect
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 // indirect
github.com/pkg/errors v0.8.1 // indirect
go.deanishe.net/env v0.5.1 // indirect
go.deanishe.net/fuzzy v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 // indirect
golang.org/x/text v0.8.0 // indirect
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
)

61
go.sum Normal file
View File

@ -0,0 +1,61 @@
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.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
github.com/bmatcuk/doublestar v1.3.4/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/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95 h1:OM0MnUcXBysj7ZtXvThVWHMoahuKQ8FuwIdeSLcNdP4=
github.com/ebfe/scard v0.0.0-20190212122703-c3d1b1916a95/go.mod h1:8hHvF8DlEq5kE3KWOsZQezdWq1OTOVxZArZMscS954E=
github.com/iamthefij/awgo v0.29.1-pre1 h1:QrkVt0y3axBJl4tfdwHrsWXbDN7lsJKHNpqtp2WJOiA=
github.com/iamthefij/awgo v0.29.1-pre1/go.mod h1:1yGF+uQfWXX99TiDfAYYKjJpHTq5lHEmvHFEVCHo6KA=
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/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.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 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
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/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=

View File

@ -3,14 +3,100 @@
<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>0149DE47-0232-434D-BB9E-B2C0B419A2E3</key>
<array>
<dict>
<key>destinationuid</key>
<string>55DFD4B6-922A-4259-8625-EC14889FBACD</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>27BC434A-1663-44A7-85AF-C4AC6E1BEFB7</key>
<array>
<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>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</key>
<array>
<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>
<dict>
<key>destinationuid</key>
<string>0149DE47-0232-434D-BB9E-B2C0B419A2E3</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>55DFD4B6-922A-4259-8625-EC14889FBACD</key>
<array>
<dict>
<key>destinationuid</key>
<string>AD82ED59-033E-4860-B371-8128574E2FBC</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>
<dict>
<key>destinationuid</key>
<string>506787F2-9A61-492C-8C49-30EE04FB70BC</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>8486DCAA-AFB7-407D-A0E9-E57E09997B24</key>
<array>
<dict>
<key>destinationuid</key>
<string>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</string>
<string>DB934647-74DE-4182-ACDF-93C629AA99D9</string>
<key>modifiers</key>
<integer>524288</integer>
<key>modifiersubtext</key>
<string>Show</string>
<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>
@ -20,11 +106,11 @@
</dict>
<dict>
<key>destinationuid</key>
<string>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</string>
<string>27BC434A-1663-44A7-85AF-C4AC6E1BEFB7</string>
<key>modifiers</key>
<integer>1048576</integer>
<key>modifiersubtext</key>
<string>Paste token</string>
<string>Paste code</string>
<key>vitoclose</key>
<false/>
</dict>
@ -42,6 +128,56 @@
<false/>
</dict>
</array>
<key>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</key>
<array>
<dict>
<key>destinationuid</key>
<string>DA99BA2E-7234-491D-BD0F-044151FA98E2</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>AD82ED59-033E-4860-B371-8128574E2FBC</key>
<array>
<dict>
<key>destinationuid</key>
<string>F3AEDAF9-44BC-4E39-B908-724974ACA17B</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>sourceoutputuid</key>
<string>7B2EFCB5-ED69-4B10-84C5-4F6320C91491</string>
<key>vitoclose</key>
<false/>
</dict>
<dict>
<key>destinationuid</key>
<string>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</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>
<dict>
<key>destinationuid</key>
<string>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</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>
@ -68,6 +204,44 @@
<false/>
</dict>
</array>
<key>DA99BA2E-7234-491D-BD0F-044151FA98E2</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>sourceoutputuid</key>
<string>94F60406-01FF-4991-A697-2C83147293EB</string>
<key>vitoclose</key>
<false/>
</dict>
<dict>
<key>destinationuid</key>
<string>506787F2-9A61-492C-8C49-30EE04FB70BC</string>
<key>modifiers</key>
<integer>0</integer>
<key>modifiersubtext</key>
<string></string>
<key>vitoclose</key>
<false/>
</dict>
</array>
<key>DB934647-74DE-4182-ACDF-93C629AA99D9</key>
<array>
<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>E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E</key>
<array>
<dict>
@ -81,9 +255,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>
@ -95,19 +282,224 @@
<dict>
<key>config</key>
<dict>
<key>autopaste</key>
<false/>
<key>clipboardtext</key>
<key>argument</key>
<string>{query}</string>
<key>transient</key>
<key>passthroughargument</key>
<false/>
<key>variables</key>
<dict>
<key>result_action</key>
<string>show</string>
</dict>
</dict>
<key>type</key>
<string>alfred.workflow.utility.argument</string>
<key>uid</key>
<string>DB934647-74DE-4182-ACDF-93C629AA99D9</string>
<key>version</key>
<integer>1</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>
<dict>
<key>config</key>
<dict>
<key>concurrently</key>
<false/>
<key>escaping</key>
<integer>102</integer>
<key>script</key>
<string>./alfred-yubico-auth -run-script 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.action.script</string>
<key>uid</key>
<string>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</string>
<key>version</key>
<integer>2</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-auth list</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.output.clipboard</string>
<string>alfred.workflow.input.scriptfilter</string>
<key>uid</key>
<string>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</string>
<string>8486DCAA-AFB7-407D-A0E9-E57E09997B24</string>
<key>version</key>
<integer>2</integer>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>conditions</key>
<array>
<dict>
<key>inputstring</key>
<string>{var:result}</string>
<key>matchcasesensitive</key>
<false/>
<key>matchmode</key>
<integer>0</integer>
<key>matchstring</key>
<string>success</string>
<key>outputlabel</key>
<string>success</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.utility.conditional</string>
<key>uid</key>
<string>DA99BA2E-7234-491D-BD0F-044151FA98E2</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<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.utility.conditional</string>
<key>uid</key>
<string>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>argument</key>
<string>{query}</string>
<key>passthroughargument</key>
<false/>
<key>variables</key>
<dict>
<key>result_action</key>
<string>paste</string>
</dict>
</dict>
<key>type</key>
<string>alfred.workflow.utility.argument</string>
<key>uid</key>
<string>27BC434A-1663-44A7-85AF-C4AC6E1BEFB7</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>alignment</key>
<integer>0</integer>
<key>backgroundcolor</key>
<string></string>
<key>fadespeed</key>
<integer>0</integer>
<key>fillmode</key>
<integer>0</integer>
<key>font</key>
<string></string>
<key>ignoredynamicplaceholders</key>
<false/>
<key>largetypetext</key>
<string>{query}</string>
<key>textcolor</key>
<string></string>
<key>wrapat</key>
<integer>50</integer>
</dict>
<key>type</key>
<string>alfred.workflow.output.largetype</string>
<key>uid</key>
<string>F3AEDAF9-44BC-4E39-B908-724974ACA17B</string>
<key>version</key>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
@ -130,6 +522,69 @@
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>conditions</key>
<array>
<dict>
<key>inputstring</key>
<string>{var:result_action}</string>
<key>matchcasesensitive</key>
<false/>
<key>matchmode</key>
<integer>0</integer>
<key>matchstring</key>
<string>show</string>
<key>outputlabel</key>
<string>show</string>
<key>uid</key>
<string>7B2EFCB5-ED69-4B10-84C5-4F6320C91491</string>
</dict>
<dict>
<key>inputstring</key>
<string>{var:result_action}</string>
<key>matchcasesensitive</key>
<false/>
<key>matchmode</key>
<integer>0</integer>
<key>matchstring</key>
<string>paste</string>
<key>outputlabel</key>
<string>paste</string>
<key>uid</key>
<string>94F60406-01FF-4991-A697-2C83147293EB</string>
</dict>
</array>
<key>elselabel</key>
<string>copy</string>
</dict>
<key>type</key>
<string>alfred.workflow.utility.conditional</string>
<key>uid</key>
<string>AD82ED59-033E-4860-B371-8128574E2FBC</string>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>autopaste</key>
<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>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</string>
<key>version</key>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
<dict>
@ -137,6 +592,8 @@
<false/>
<key>clipboardtext</key>
<string></string>
<key>ignoredynamicplaceholders</key>
<false/>
<key>transient</key>
<false/>
</dict>
@ -145,8 +602,95 @@
<key>uid</key>
<string>BFB3A122-52BB-4FF1-B5B3-CECD42A730DB</string>
<key>version</key>
<integer>3</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>concurrently</key>
<false/>
<key>escaping</key>
<integer>102</integer>
<key>script</key>
<string>query=$1
./alfred-yubico-auth -run-script "$query"</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.action.script</string>
<key>uid</key>
<string>0149DE47-0232-434D-BB9E-B2C0B419A2E3</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>
<key>version</key>
<integer>1</integer>
</dict>
<dict>
<key>config</key>
<dict>
<key>conditions</key>
<array>
<dict>
<key>inputstring</key>
<string>{var:result}</string>
<key>matchcasesensitive</key>
<false/>
<key>matchmode</key>
<integer>0</integer>
<key>matchstring</key>
<string>success</string>
<key>outputlabel</key>
<string>success</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.utility.conditional</string>
<key>uid</key>
<string>55DFD4B6-922A-4259-8625-EC14889FBACD</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>
<dict>
@ -171,137 +715,158 @@
<dict>
<key>config</key>
<dict>
<key>alfredfiltersresults</key>
<key>lastpathcomponent</key>
<false/>
<key>onlyshowifquerypopulated</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>removeextension</key>
<false/>
<key>text</key>
<string>{query}</string>
<key>title</key>
<string>Yubikey 2FA</string>
<key>type</key>
<integer>0</integer>
<key>withspace</key>
<true/>
<string>Alfred Yubikey Error</string>
</dict>
<key>type</key>
<string>alfred.workflow.input.scriptfilter</string>
<string>alfred.workflow.output.notification</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>506787F2-9A61-492C-8C49-30EE04FB70BC</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/>
</dict>
<key>type</key>
<string>alfred.workflow.output.clipboard</string>
<key>uid</key>
<string>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</string>
<key>version</key>
<integer>2</integer>
</dict>
</array>
<key>readme</key>
<string></string>
<key>uidata</key>
<dict>
<key>0149DE47-0232-434D-BB9E-B2C0B419A2E3</key>
<dict>
<key>xpos</key>
<integer>180</integer>
<key>ypos</key>
<integer>305</integer>
</dict>
<key>0718204D-3398-4AEF-A621-DDDE1FC6ED75</key>
<dict>
<key>xpos</key>
<integer>960</integer>
<integer>1245</integer>
<key>ypos</key>
<integer>140</integer>
<integer>225</integer>
</dict>
<key>27BC434A-1663-44A7-85AF-C4AC6E1BEFB7</key>
<dict>
<key>xpos</key>
<integer>260</integer>
<key>ypos</key>
<integer>155</integer>
</dict>
<key>2D69982A-0DB6-4ABA-812F-C7F2A73650AE</key>
<dict>
<key>xpos</key>
<integer>365</integer>
<key>ypos</key>
<integer>90</integer>
</dict>
<key>506787F2-9A61-492C-8C49-30EE04FB70BC</key>
<dict>
<key>xpos</key>
<integer>835</integer>
<key>ypos</key>
<integer>540</integer>
</dict>
<key>55DFD4B6-922A-4259-8625-EC14889FBACD</key>
<dict>
<key>xpos</key>
<integer>365</integer>
<key>ypos</key>
<integer>330</integer>
</dict>
<key>8486DCAA-AFB7-407D-A0E9-E57E09997B24</key>
<dict>
<key>xpos</key>
<integer>120</integer>
<integer>70</integer>
<key>ypos</key>
<integer>140</integer>
<integer>65</integer>
</dict>
<key>9F48DDE6-BBE7-42ED-ABE5-C9255C92F1CD</key>
<dict>
<key>xpos</key>
<integer>360</integer>
<integer>650</integer>
<key>ypos</key>
<integer>140</integer>
<integer>415</integer>
</dict>
<key>A8D2CCAC-5CA4-495E-BB62-5C7F596FA157</key>
<dict>
<key>xpos</key>
<integer>515</integer>
<key>ypos</key>
<integer>55</integer>
</dict>
<key>AD82ED59-033E-4860-B371-8128574E2FBC</key>
<dict>
<key>xpos</key>
<integer>485</integer>
<key>ypos</key>
<integer>285</integer>
</dict>
<key>BFB3A122-52BB-4FF1-B5B3-CECD42A730DB</key>
<dict>
<key>xpos</key>
<integer>780</integer>
<integer>1100</integer>
<key>ypos</key>
<integer>140</integer>
<integer>300</integer>
</dict>
<key>C252A5EC-1AEE-4EF4-864F-67483EAADCFA</key>
<dict>
<key>xpos</key>
<integer>800</integer>
<key>ypos</key>
<integer>45</integer>
</dict>
<key>DA3E9CE8-7C4F-4B09-BFA5-F8CA83297968</key>
<dict>
<key>xpos</key>
<integer>540</integer>
<integer>835</integer>
<key>ypos</key>
<integer>140</integer>
<integer>415</integer>
</dict>
<key>DA99BA2E-7234-491D-BD0F-044151FA98E2</key>
<dict>
<key>xpos</key>
<integer>670</integer>
<key>ypos</key>
<integer>75</integer>
</dict>
<key>DB934647-74DE-4182-ACDF-93C629AA99D9</key>
<dict>
<key>xpos</key>
<integer>260</integer>
<key>ypos</key>
<integer>40</integer>
</dict>
<key>E86DC7C1-35C1-4BA9-8B33-A95DE7082F7E</key>
<dict>
<key>xpos</key>
<integer>700</integer>
<integer>1015</integer>
<key>ypos</key>
<integer>170</integer>
<integer>330</integer>
</dict>
<key>F3AEDAF9-44BC-4E39-B908-724974ACA17B</key>
<dict>
<key>xpos</key>
<integer>655</integer>
<key>ypos</key>
<integer>160</integer>
</dict>
<key>FB0DDF80-FF90-439A-BF3F-6EC58C2AA870</key>
<dict>
<key>xpos</key>
<integer>360</integer>
<integer>650</integer>
<key>ypos</key>
<integer>290</integer>
<integer>285</integer>
</dict>
</dict>
<key>version</key>
<string>0.0.1</string>
<string>1.1.1</string>
<key>webaddress</key>
<string></string>
</dict>

146
magefile.go Normal file
View File

@ -0,0 +1,146 @@
//go: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"
)
const (
buildDir = "./build"
distDir = "./dist"
binName = "alfred-yubico-auth"
)
var (
info *build.Info
// Default mage target
Default = Run
// Output binary path
binPath = filepath.Join(buildDir, binName)
)
func InfoWithVersion(v int) build.Option {
return func(i *build.Info) {
i.AlfredMajorVersion = v
}
}
func init() {
var err error
if info, err = build.NewInfo(InfoWithVersion(5)); err != nil {
panic(err)
}
}
// Build workflow.
func Build() error {
mg.Deps(cleanBuild)
fmt.Println("Building...")
if err := sh.RunWith(info.Env(), "go", "build", "-o", binPath, "."); err != nil {
return fmt.Errorf("error building binary %w", 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(), binPath)
}
// Dist packages workflow for distribution.
func Dist() error {
mg.SerialDeps(Clean, Build)
fmt.Println("Exporting dist...")
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 fmt.Errorf("error cleaning previously installed workflow: %w", 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 fmt.Errorf("cleanDir could not read folder: %w", err)
}
for _, fi := range infos {
if err := sh.Rm(filepath.Join(name, fi.Name())); err != nil {
return fmt.Errorf("cleanDir could not remove file: %w", err)
}
}
return nil
}
func cleanBuild() error { return cleanDir(buildDir) }
func cleanDist() error { return cleanDir(distDir) }
func cleanMage() error { return sh.Run("mage", "-clean") }

179
main.go Normal file
View File

@ -0,0 +1,179 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"git.iamthefij.com/iamthefij/slog"
aw "github.com/deanishe/awgo"
"github.com/deanishe/awgo/util"
"github.com/yawn/ykoath"
)
var (
wf *aw.Workflow
oath *ykoath.OATH
keychainAccount = "yubico-auth-creds"
errIncorrectPassword = errors.New("incorrect password")
)
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 "", fmt.Errorf("error reading password from prompt: %w", err)
}
out = bytes.TrimRight(out, "\n")
return string(out), nil
}
func setPassword(s *ykoath.Select) error {
passphrase, err := promptPassword()
if err != nil {
return fmt.Errorf("failed reading passphrase: %w", err)
}
err = validatePassphrase(s, passphrase)
if err != nil {
return fmt.Errorf("failed validating passphrase: %w", err)
}
err = wf.Keychain.Set(keychainAccount, passphrase)
if err != nil {
return fmt.Errorf("failed storing passphrase in keychain: %w", err)
}
return nil
}
func sendResult(result string, args ...string) error {
results := aw.NewArgVars()
results.Arg(args...)
results.Var("result", result)
return results.Send()
}
func validatePassphrase(s *ykoath.Select, passphrase string) error {
key := s.DeriveKey(passphrase)
// verify password is correct with a validate call
ok, err := oath.Validate(s, key)
if err != nil {
return fmt.Errorf("error in validate: %w", err)
}
if !ok {
return errIncorrectPassword
}
return nil
}
func run() {
runScript := flag.Bool("run-script", false, "change output to script output")
wf.Args()
flag.Parse()
if *runScript {
wf.Configure(aw.TextErrors(true))
}
var err error
oath, err = ykoath.New()
if err != nil {
wf.FatalError(fmt.Errorf("failed to iniatialize new oath: %w", err))
}
defer oath.Close()
oath.Debug = slog.Debug
// Select oath to begin
s, err := oath.Select()
if err != nil {
wf.FatalError(fmt.Errorf("failed to select oath: %w", 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(fmt.Errorf("failed to set password: %w", err))
}
if err = sendResult("success"); err != nil {
wf.FatalError(fmt.Errorf("failed to send password set result: %w", err))
}
return
}
// If required, authenticate with password from keychain
if s.Challenge != nil {
passphrase, 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
}
err = validatePassphrase(s, passphrase)
if err != nil {
wf.FatalError(fmt.Errorf("passphrase failed: %w", err))
}
}
if flag.Arg(0) == "list" {
// List names only
names, err := oath.List()
if err != nil {
wf.FatalError(fmt.Errorf("failed to list names: %w", err))
}
for _, name := range names {
slog.Log(name.Name)
wf.NewItem(name.Name).
Icon(aw.IconAccount).
Subtitle("Copy to clipboard").
Arg(name.Name).
Valid(true)
}
} else {
name := flag.Arg(0)
code, err := oath.CalculateOne(name)
if err != nil {
// TODO: Check for error "requires-auth" and notify touch
wf.FatalError(fmt.Errorf("failed to generate code: %w", err))
}
slog.Log(code)
if err = sendResult("success", code); err != nil {
wf.FatalError(fmt.Errorf("failed to send code: %w", err))
}
}
if !*runScript {
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

1
ykoath Submodule

@ -0,0 +1 @@
Subproject commit fd081cb213d030585bfdd03305e03bff4d6e7a09