diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..e3cfb30 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,18 @@ +name: Package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Package + run: make release + - name: Upload workflow artifact + uses: actions/upload-artifact@v1 + with: + name: gopass.alfredworkflow + path: gopass.alfredworkflow diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32a9147 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +*.iml +gopass.alfredworkflow diff --git a/Makefile b/Makefile index 47d205d..ecca816 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,3 @@ release: - zip -r ../gopass.alfredworkflow * + rm -f ../gopass.alfredworkflow + zip -r ./gopass.alfredworkflow ./* -x@alfred_package.ignore diff --git a/README.md b/README.md index 708216b..de90201 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Workflow to interact with gopass from [Alfred 3](https://www.alfredapp.com/workflows/) on MacOS. You will need an Alfred 3 license in order to use workflows. Currently querying entries and copying username or password to clipboard is supported. + ## Screenshot ![gopass alfred screenshot](./screenshot.png) @@ -11,6 +12,58 @@ Currently querying entries and copying username or password to clipboard is supp Download the [latest release package](https://github.com/gopasspw/gopass-alfred/releases/latest) from github or build your own via `make release` in checked out repository. +## Autotyping + +The plugin can also type a username / password combination automatically. You can start the action by issuing `gpa` in Alfred. + +By default, a field called `autotype` will be used. The value can contain various statements. + +Example: `user :tab :pass` + +### Trigger Options + +However, there are more options: + +| Option | Effect | +|---------------|-------------------------------------------------------------------------------| +| user / pass | A field name (not prefixed with `:`. Can reference any other available field. | +| `:tab` | Issues a tab key | +| `:enter` | Enter key | +| `:space` | Space key | +| `:delete` | Delete key | +| `:delay` | Waits one second before continuing | +| `:clearField` | Clears the current input field (Issues Ctrl+A + backspace) | + +A rather typical example looks like the following: + +``` +myPassword +--- +autotype: :clearField user :tab pass :tab :tab :enter :delay :someYk :enter +user: myUsername +usernamePassword: user :tab pass +``` + +### Custom autotype options + +You might have noted `:someYk` as option which was not listed in the table above. Not all commands can be hardcoded in a table like the one above. For example, you might have a form that requires a one-time-password such as from a YubiKey. You might be able to retrieve the information from a `bash` command - however. + +Those commands can be put to a `.gopass_autotype_handlers.json` json file. It can be place in `$XDG_CONFIG`, which defaults to your `HOME` directory. Example: `/users/someUser/.gopass_autotype_handlers.json` + +An example file content looks like the following: + +```json +{ + ":someYk": "/usr/local/bin/ykman oath code | grep \"YubiKey:name\" | sed 's/.*\\([0-9]\\{6\\}\\)$/\\1/g'" +} +``` + +This example will retrieve a current TOTP token from the YubiKey by asking `ykman` for the current value and converts the output to the token. + +### Autotyping other fields than `autotype` + +The `gpa` command will always run the `autotype` field. However, you can also use `gpf` to autotype any available field in the gopass file. This also includes the trigger options from the table above. + ## Development Contributions to this project are welcome! diff --git a/alfred_package.ignore b/alfred_package.ignore new file mode 100644 index 0000000..e84249e --- /dev/null +++ b/alfred_package.ignore @@ -0,0 +1,5 @@ +.idea +.env +.git +*.iml +.github diff --git a/get_password.py b/get_password.py index 96bf9de..859a712 100755 --- a/get_password.py +++ b/get_password.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import subprocess import sys diff --git a/gopass_autotype.py b/gopass_autotype.py new file mode 100755 index 0000000..2d8be2e --- /dev/null +++ b/gopass_autotype.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +import json +import os +import subprocess +import sys +import time +from os.path import expanduser + +home = expanduser("~") +my_env = os.environ.copy() +my_env['PATH'] = '/usr/local/bin:{}'.format(my_env['PATH']) + +special_autotype_handlers = { + ":tab": lambda: do_stroke(48), + ":space": lambda: do_stroke(49), + ":enter": lambda: do_stroke(36), + ":delete": lambda: do_stroke(51), + ":delay": lambda: time.sleep(1), + ":clearField": lambda: clear_field_content() +} + + +def get_additional_autotype_handlers_filename(): + base_path = os.getenv("XDG_CONFIG", home) + return '{base_path}/{filename}'.format(base_path=base_path, filename=".gopass_autotype_handlers.json") + + +def load_additional_autotype_handlers_config(): + handlers_path = get_additional_autotype_handlers_filename() + if os.path.exists(handlers_path): + with open(handlers_path) as f: + return json.load(f) + else: + return {} + + +def execute_additional_autotype_handler(command): + command_result = os.popen(command).read() + do_type(command_result) + + +def load_additional_autotype_handlers(): + config = load_additional_autotype_handlers_config() + return dict(map(lambda item: (item[0], lambda: execute_additional_autotype_handler(item[1])), config.items())) + + +def all_autotype_handlers(): + additional = load_additional_autotype_handlers() + return {**additional, **special_autotype_handlers} + + +def do_type(to_type): + os.system(f"echo 'tell application \"System Events\" to keystroke \"{to_type}\"' | osascript") + + +def do_stroke(stroke): + os.system(f"echo 'tell application \"System Events\" to key code \"{stroke}\"' | osascript") + + +def clear_field_content(): + os.system("""echo 'tell application "System Events" to keystroke "a" using command down' | osascript""") + do_stroke(51) + + +def get_gopass_data_for(query): + return subprocess.run(["gopass", "show", "-f", query], stdout=subprocess.PIPE, env=my_env).stdout.decode("utf-8") + + +def parse_gopass_data(data): + lines = data.split("\n") + password = lines[0] + non_empty_lines = list(filter(lambda line: len(line) > 0, lines)) + lines_splitted = map(lambda line: line.split(": "), non_empty_lines[1:]) + lines_with_two_items = filter(lambda elements: len(elements) == 2, lines_splitted) + items_dict = {item[0]: item[1] for item in lines_with_two_items} + items_dict["pass"] = password + return items_dict + + +def autotype(items, field): + autotype = items.get(field) + handlers = all_autotype_handlers() + if autotype is None: + return + to_type = autotype.split(" ") + for word in to_type: + handler = handlers.get(word) + if handler is None: + mapped = items.get(word) + do_type(word if mapped is None else mapped) + else: + handler() + + +def autotype_list(path, parsed): + return [ + { + "uid": result, + "title": result, + "subtitle": path, + "arg": f"{path} {result}", + "autocomplete": result + } for result in parsed.keys() + ] + + +action = sys.argv[1] +path = sys.argv[2].split(" ") + +data = get_gopass_data_for(path[0]) +parsed = parse_gopass_data(data) +autotype_action = "autotype" if len(path) == 1 else path[1] + +if action == 'list': + print(json.dumps({'items': autotype_list(path[0], parsed)})) +elif action == 'autotype': + autotype(parsed, autotype_action) diff --git a/gopass_filter.py b/gopass_filter.py index b9f0b6b..5b91267 100755 --- a/gopass_filter.py +++ b/gopass_filter.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import sys @@ -22,7 +22,7 @@ "title": result.split('/')[-1], "subtitle": '/'.join(result.split('/')[:-1]), "arg": result, - "match": " ".join(set(re.split('[. /\-]', result))) + ' ' + result, + "match": " ".join(set(re.split('[. /\-_]', result))) + ' ' + " ".join(set(re.split('[/]', result))) + ' ' + result, "autocomplete": result } for result in stdout.decode('ascii').splitlines() ] diff --git a/info.plist b/info.plist index 16403ca..c62d69d 100644 --- a/info.plist +++ b/info.plist @@ -4,8 +4,6 @@ bundleid pw.gopass.alfred - category - Productivity connections 0D5132AE-3620-4D5F-BA72-EFD481455463 @@ -31,6 +29,45 @@ + 1191CB7E-4E88-4BFB-BED0-579A0E6124C8 + + + destinationuid + 7B342C01-E6A7-4B26-A83E-D593DA90F4E1 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 2E65F512-65E5-47AD-83AD-EFBB0A988B69 + + + destinationuid + 1191CB7E-4E88-4BFB-BED0-579A0E6124C8 + modifiers + 0 + modifiersubtext + + vitoclose + + + + 437FA632-22A0-4FA7-9B33-E0CFAFE9E61A + + + destinationuid + FB6C8961-5CC1-464F-AEF7-A9BCF53F228F + modifiers + 0 + modifiersubtext + + vitoclose + + + B1FF35DC-433E-4CCC-9CB3-7C8E1BC532F7 @@ -91,6 +128,8 @@ exit $RESULT alfredfiltersresultsmatchmode 2 + argumenttreatemptyqueryasnil + argumenttrimmode 0 argumenttype @@ -130,7 +169,7 @@ exit $RESULT uid 0D5132AE-3620-4D5F-BA72-EFD481455463 version - 2 + 3 config @@ -157,6 +196,201 @@ echo $USER | pbcopy version 2 + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 2 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 0 + escaping + 70 + keyword + gpa + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + loading gopass secrets + script + /usr/local/bin/gopass list -f > /tmp/all + + scriptargtype + 1 + scriptfile + gopass_filter.py + subtext + secret + title + Gopass autotype + type + 8 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 437FA632-22A0-4FA7-9B33-E0CFAFE9E61A + version + 3 + + + config + + concurrently + + escaping + 68 + script + ./gopass_autotype.py autotype "{query}" + scriptargtype + 0 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + FB6C8961-5CC1-464F-AEF7-A9BCF53F228F + version + 2 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 2 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 0 + escaping + 70 + keyword + gpf + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + loading gopass secrets + script + /usr/local/bin/gopass list -f > /tmp/all + + scriptargtype + 1 + scriptfile + gopass_filter.py + subtext + secret + title + gopass type field + type + 8 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 2E65F512-65E5-47AD-83AD-EFBB0A988B69 + version + 3 + + + config + + concurrently + + escaping + 68 + script + ./gopass_autotype.py autotype "{query}" + scriptargtype + 0 + scriptfile + + type + 0 + + type + alfred.workflow.action.script + uid + 7B342C01-E6A7-4B26-A83E-D593DA90F4E1 + version + 2 + + + config + + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttreatemptyqueryasnil + + argumenttrimmode + 0 + argumenttype + 0 + escaping + 102 + keyword + list for key + queuedelaycustom + 3 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + + script + ./gopass_autotype.py list "{query}" + scriptargtype + 0 + scriptfile + + subtext + + title + + type + 0 + withspace + + + type + alfred.workflow.input.scriptfilter + uid + 1191CB7E-4E88-4BFB-BED0-579A0E6124C8 + version + 3 + readme @@ -169,6 +403,34 @@ echo $USER | pbcopy ypos 20 + 1191CB7E-4E88-4BFB-BED0-579A0E6124C8 + + xpos + 255 + ypos + 465 + + 2E65F512-65E5-47AD-83AD-EFBB0A988B69 + + xpos + 85 + ypos + 465 + + 437FA632-22A0-4FA7-9B33-E0CFAFE9E61A + + xpos + 90 + ypos + 330 + + 7B342C01-E6A7-4B26-A83E-D593DA90F4E1 + + xpos + 430 + ypos + 465 + B1FF35DC-433E-4CCC-9CB3-7C8E1BC532F7 xpos @@ -183,7 +445,18 @@ echo $USER | pbcopy ypos 20 + FB6C8961-5CC1-464F-AEF7-A9BCF53F228F + + xpos + 250 + ypos + 330 + + variablesdontexport + + version + webaddress https://www.gopass.pw