├── .gitignore ├── src ├── icon.png ├── time.png ├── error.png ├── warning.png ├── 7DD3BDE5-A157-42E5-9376-F681FB50A4EE.png ├── LICENSE.txt ├── otp.py ├── alfred.py ├── workflow.py └── info.plist ├── screenshots ├── 1.png ├── 2.png ├── 3.png └── anim.gif ├── .travis.yml ├── Google Authenticator.alfredworkflow ├── CHANGELOG.md ├── bundle.sh ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *# 4 | .#* 5 | *.pyc 6 | .idea 7 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/src/icon.png -------------------------------------------------------------------------------- /src/time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/src/time.png -------------------------------------------------------------------------------- /src/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/src/error.png -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/screenshots/3.png -------------------------------------------------------------------------------- /src/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/src/warning.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | install: pip install pep8 4 | script: pep8 src/*.py 5 | -------------------------------------------------------------------------------- /screenshots/anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/screenshots/anim.gif -------------------------------------------------------------------------------- /Google Authenticator.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/Google Authenticator.alfredworkflow -------------------------------------------------------------------------------- /src/7DD3BDE5-A157-42E5-9376-F681FB50A4EE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/alfred-workflow-gauth/master/src/7DD3BDE5-A157-42E5-9376-F681FB50A4EE.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.6.0 (2014-03-18) 4 | - We can now add secrets from Alfred 5 | - The .gauth config is being automatically created 6 | 7 | ## 1.5.0 (2014-03-15) 8 | - Added more syntax error handling 9 | 10 | ## 1.4.0 (2014-03-14) 11 | - Added LargeType and Write-At-Cursor outputs (with key modifier) 12 | - Updated description 13 | 14 | ## 1.3.0 (2014-03-14) 15 | - Improved documentation for Forum post 16 | 17 | ## 1.2.0 (2014-03-07) 18 | - Thanks to @golimpio 19 | - PEP8 fixes 20 | - Updated icons 21 | - Enhanced error handling 22 | - Added auto-complete support 23 | - Remaining-time item always at the bottom of the list 24 | - Added build and install script 25 | 26 | ## 1.1.0 (2014-02-28) 27 | - Initial version 28 | -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will create the Alfred workflow file and optionally it will install it. 4 | # To install it, pass the argument "-i" or "--install", e.g. 5 | # bundle.sh --install 6 | 7 | echo "Creating Google Authenticator workflow file..." 8 | 9 | WORKFLOW_FILE=Google\ Authenticator.alfredworkflow 10 | if [ -f "$WORKFLOW_FILE" ]; then 11 | echo "Removing previous workflow..." 12 | rm "$WORKFLOW_FILE" 13 | fi 14 | 15 | echo "Cleaning it..." 16 | find src \( -name "*~" -or -name ".??*~" -or -name "*.pyc" -or -name "#*#" -or -name ".DS_Store" \) -delete 17 | 18 | echo "Bundling it..." 19 | cd src && zip -r "../$WORKFLOW_FILE" * && cd .. 20 | 21 | while test $# -gt 0 22 | do 23 | case "$1" in 24 | --install | -i) 25 | echo "Installing $WORKFLOW_FILE..." 26 | open "$WORKFLOW_FILE" 27 | ;; 28 | esac 29 | shift 30 | done 31 | 32 | echo "$WORKFLOW_FILE is ready!" 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manfred Touron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manfred Touron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/otp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import hmac 4 | import base64 5 | import struct 6 | import hashlib 7 | import time 8 | 9 | 10 | def get_hotp_token(key, intervals_no): 11 | msg = struct.pack(">Q", intervals_no) 12 | h = hmac.new(key, msg, hashlib.sha1).digest() 13 | o = h[19] & 15 14 | h = (struct.unpack(">I", h[o:o + 4])[0] & 0x7fffffff) % 1000000 15 | return h 16 | 17 | 18 | def get_totp_token(key): 19 | token = get_hotp_token(key, intervals_no=int(time.time()) // 30) 20 | return str(token).zfill(6) 21 | 22 | 23 | def get_totp_time_remaining(): 24 | return int(30 - (time.time() % 30)) 25 | 26 | 27 | def pad_base32_str(str, padding_char): 28 | str_len = len(str) 29 | missing_padding = str_len % 8 30 | if missing_padding != 0: 31 | # str += '=' * (8 - missing_padding) 32 | str = str.ljust(str_len + 8 - missing_padding, padding_char) 33 | return str 34 | 35 | 36 | def is_otp_secret_valid(secret): 37 | try: 38 | secret = secret.replace(' ', '') 39 | if not len(secret): 40 | return False 41 | secret = pad_base32_str(secret, '=') 42 | key = base64.b32decode(secret, casefold=True) 43 | get_totp_token(key) 44 | except: 45 | return False 46 | 47 | return True 48 | 49 | 50 | def get_hotp_key(key=None, secret=None, hexkey=None): 51 | if hexkey: 52 | key = hexkey.decode('hex') 53 | if secret: 54 | secret = secret.replace(' ', '') 55 | secret = pad_base32_str(secret, '=') 56 | key = base64.b32decode(secret, casefold=True) 57 | return key 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Alfred Workflow: Google Authenticator 2 | ===================================== 3 | 4 | [![Build Status](https://travis-ci.org/moul/alfred-workflow-gauth.svg?branch=master)](https://travis-ci.org/moul/alfred-workflow-gauth) 5 | 6 | An Alfred 2 workflow for Google Authenticator / a.k.a. Two-Factors Authentication / a.k.a. Time-Based Authentication Token / a.k.a. TOTP 7 | 8 | ![](https://raw.github.com/moul/alfred-workflow-gauth/master/screenshots/anim.gif) 9 | ![](https://raw.github.com/moul/alfred-workflow-gauth/master/screenshots/3.png) 10 | 11 | An Alfred workflow equivalent of the mobile applications [Google Authenticator](https://itunes.apple.com/en/app/google-authenticator/id388497605?mt=8) and [Authy](https://www.authy.com). 12 | 13 | I personally use it on Gmail, Amazon AWS, Github, Facebook, Evernote and Dropbox 14 | 15 | A bigger list is available on [Wikipedia](http://en.wikipedia.org/wiki/Two-step_verification) 16 | 17 | Installation 18 | ------------ 19 | 20 | Create a `~/.gauth` file with your secrets, ie: 21 | 22 | ```ini 23 | [google - bob@gmail.com] 24 | secret=xxxxxxxxxxxxxxxxxx 25 | 26 | [evernote - robert] 27 | secret=yyyyyyyyyyyyyyyyyy 28 | ``` 29 | 30 | [Download](https://github.com/moul/alfred-workflow-gauth/raw/master/Google%20Authenticator.alfredworkflow) and import to Alfred 31 | 32 | Dependencies 33 | ------------ 34 | 35 | - Alfred 2 or 3 with PowerPack 36 | - Python3 37 | 38 | Non-exhaustive list of links for "secret" installation 39 | ------------------------------------------------------ 40 | 41 | - [Google](http://www.google.com/landing/2step/) 42 | - [Dropbox](https://www.dropbox.com/help/363/en) 43 | - [Evernote](http://blog.evernote.com/blog/2013/05/30/evernotes-three-new-security-features/) 44 | - [Github](https://github.com/blog/1614-two-factor-authentication) 45 | - [Amazon AWS](http://aws.amazon.com/iam/details/mfa/) 46 | - [Facebook](https://www.facebook.com/settings?tab=security) 47 | 48 | Links 49 | ----- 50 | 51 | - [Packal: Gauth](http://www.packal.org/workflow/gauth) 52 | - [Official Forum Post](http://www.alfredforum.com/topic/4062-gauth-google-authenticator-time-based-two-factor-authentication/) 53 | - [Source Code](https://github.com/moul/alfred-workflow-gauth/) 54 | 55 | Development 56 | ----------- 57 | 58 | After modifying files locally, run `build.sh` to update the workflow file, and 59 | commit the workflow file update to this repository. 60 | 61 | Acknowledgment 62 | -------------- 63 | 64 | - Original alarm clock icon 65 | - [Alex Auda Samora from The Noun Project](http://thenounproject.com/razerk/) 66 | - Licensed under Creative Commons Attribution 67 | - Status & signs icons 68 | - [Hereldar Terkenya](http://hereldar.deviantart.com/) 69 | - Licensed under a Creative Commons Attribution-Share Alike 3.0 License 70 | - Original source code 71 | - [Manfred Touron](https://github.com/moul) 72 | - Serial contributor 73 | - [Gilberto Olimpio](https://github.com/golimpio) 74 | 75 | License 76 | ------- 77 | 78 | MIT 79 | -------------------------------------------------------------------------------- /src/alfred.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import itertools 3 | import os 4 | import plistlib 5 | import unicodedata 6 | import sys 7 | 8 | from xml.etree.ElementTree import Element, SubElement, tostring 9 | 10 | """ 11 | You should run your script via /bin/bash with all escape options ticked. 12 | The command line should be 13 | 14 | python3 yourscript.py "{query}" arg2 arg3 ... 15 | """ 16 | 17 | 18 | UNESCAPE_CHARACTERS = u""" ;()""" 19 | 20 | _MAX_RESULTS_DEFAULT = 9 21 | 22 | with open('info.plist', 'rb') as f: 23 | preferences = plistlib.load(f) 24 | bundleid = preferences['bundleid'] 25 | 26 | 27 | class Item(object): 28 | @classmethod 29 | def unicode(cls, value): 30 | try: 31 | items = value.items() 32 | except AttributeError: 33 | return str(value) 34 | else: 35 | return dict(map(str, item) for item in items) 36 | 37 | def __init__(self, attributes, title, subtitle, icon=None): 38 | self.attributes = attributes 39 | self.title = title 40 | self.subtitle = subtitle 41 | self.icon = icon 42 | 43 | def __str__(self): 44 | return tostring(self.xml(), encoding='utf-8') 45 | 46 | def xml(self): 47 | item = Element(u'item', self.unicode(self.attributes)) 48 | for attribute in (u'title', u'subtitle', u'icon'): 49 | value = getattr(self, attribute) 50 | if value is None: 51 | continue 52 | try: 53 | (value, attributes) = value 54 | except: 55 | attributes = {} 56 | elem = SubElement(item, attribute, self.unicode(attributes)) 57 | elem.text = str(value) 58 | return item 59 | 60 | 61 | def args(characters=None): 62 | return tuple(unescape(decode(arg), characters) for arg in sys.argv[1:]) 63 | 64 | 65 | def config(): 66 | return _create('config') 67 | 68 | 69 | def decode(s): 70 | return unicodedata.normalize('NFC', s.encode("utf-8").decode('utf-8')) 71 | 72 | 73 | def get_uid(uid): 74 | return u'-'.join(map(str, (bundleid, uid))) 75 | 76 | 77 | def unescape(query, characters=None): 78 | if not characters: 79 | characters = UNESCAPE_CHARACTERS 80 | for character in characters: 81 | query = query.replace('\\%s' % character, character) 82 | return query 83 | 84 | 85 | def write(text): 86 | sys.stdout.buffer.write(text) 87 | 88 | 89 | def xml(items, maxresults=_MAX_RESULTS_DEFAULT): 90 | root = Element('items') 91 | for item in itertools.islice(items, maxresults): 92 | root.append(item.xml()) 93 | return tostring(root, encoding='utf-8') 94 | 95 | 96 | def _create(path): 97 | if not os.path.isdir(path): 98 | os.mkdir(path) 99 | if not os.access(path, os.W_OK): 100 | raise IOError('No write access: %s' % path) 101 | return path 102 | 103 | 104 | def work(volatile): 105 | path = { 106 | True: '~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data', 107 | False: '~/Library/Application Support/Alfred 2/Workflow Data' 108 | }[bool(volatile)] 109 | return _create(os.path.join(os.path.expanduser(path), bundleid)) 110 | 111 | 112 | def config_set(key, value, volatile=True): 113 | filepath = os.path.join(work(volatile), 'config.plist') 114 | try: 115 | conf = plistlib.readPlist(filepath) 116 | except IOError: 117 | conf = {} 118 | conf[key] = value 119 | plistlib.writePlist(conf, filepath) 120 | 121 | 122 | def config_get(key, default=None, volatile=True): 123 | filepath = os.path.join(work(volatile), 'config.plist') 124 | try: 125 | conf = plistlib.readPlist(filepath) 126 | except IOError: 127 | conf = {} 128 | if key in conf: 129 | return conf[key] 130 | return default 131 | 132 | 133 | class AlfredWorkflow(object): 134 | _reserved_words = [] 135 | 136 | def write_text(self, text): 137 | print(text) 138 | 139 | def write_item(self, item): 140 | return self.write_items([item]) 141 | 142 | def write_items(self, items): 143 | return write(xml(items, maxresults=self.max_results)) 144 | 145 | def message_item(self, title, message, icon=None, uid=0): 146 | return Item({ 147 | u'uid': get_uid(uid), 148 | u'arg': '', 149 | u'ignore': 'yes' 150 | }, title, message, icon) 151 | 152 | def warning_item(self, title, message, uid=0): 153 | return self.message_item(title=title, message=message, uid=uid, 154 | icon='warning.png') 155 | 156 | def error_item(self, title, message, uid=0): 157 | return self.message_item(title=title, message=message, uid=uid, 158 | icon='error.png') 159 | 160 | def exception_item(self, title, exception, uid=0): 161 | message = str(exception).replace('\n', ' ') 162 | return self.error_item(title=title, message=message, uid=uid) 163 | 164 | def route_action(self, action, query=None): 165 | method_name = 'do_{}'.format(action) 166 | if not hasattr(self, method_name): 167 | raise RuntimeError('Unknown action {}'.format(action)) 168 | 169 | method = getattr(self, method_name) 170 | return method(query) 171 | 172 | def is_command(self, query): 173 | try: 174 | command, rest = query.split(' ', 1) 175 | except ValueError: 176 | command = query 177 | command = command.strip() 178 | return command in self._reserved_words or \ 179 | hasattr(self, 'do_{}'.format(command)) 180 | 181 | -------------------------------------------------------------------------------- /src/workflow.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import configparser 5 | import time 6 | 7 | import alfred 8 | import otp 9 | 10 | 11 | class AlfredGAuth(alfred.AlfredWorkflow): 12 | _config_file_initial_content = """ 13 | #Examples of valid configurations: 14 | #[google - bob@gmail.com] 15 | #secret=xxxxxxxxxxxxxxxxxx 16 | # 17 | #[evernote - robert] 18 | #secret=yyyyyyyyyyyyyyyyyy 19 | """ 20 | 21 | _reserved_words = ['add', 'update', 'remove'] 22 | 23 | def __init__(self, config_file='~/.gauth', max_results=20): 24 | self.max_results = max_results 25 | 26 | self._config_file = config_file 27 | self.config_file = os.path.expanduser(self._config_file) 28 | self.config = configparser.RawConfigParser() 29 | self.config.read(self.config_file) 30 | 31 | # If the configuration file doesn't exist, create an empty one 32 | if not os.path.isfile(self.config_file): 33 | self.create_config() 34 | 35 | try: 36 | if not self.config.sections(): 37 | # If the configuration file is empty, 38 | # tell the user to add secrets to it 39 | self.write_item(self.config_file_is_empty_item()) 40 | return 41 | except Exception as e: 42 | item = self.exception_item(title='{}: Invalid syntax' 43 | .format(self._config_file), 44 | exception=e) 45 | self.write_item(item) 46 | 47 | def create_config(self): 48 | with open(self.config_file, 'w') as f: 49 | f.write(self._config_file_initial_content) 50 | f.close() 51 | 52 | def config_get_account_token(self, account): 53 | try: 54 | secret = self.config.get(account, 'secret') 55 | except: 56 | secret = None 57 | 58 | try: 59 | key = self.config.get(account, 'key') 60 | except: 61 | key = None 62 | 63 | try: 64 | hexkey = self.config.get(account, 'hexkey') 65 | except: 66 | hexkey = None 67 | 68 | try: 69 | key = otp.get_hotp_key(secret=secret, key=key, hexkey=hexkey) 70 | except: 71 | key = '' 72 | 73 | return otp.get_totp_token(key) 74 | 75 | def config_list_accounts(self): 76 | return self.config.sections() 77 | 78 | def filter_by_account(self, account, query): 79 | return len(query.strip()) and not query.lower() in str(account).lower() 80 | 81 | def account_item(self, account, token, uid=None): 82 | return alfred.Item({u'uid': alfred.get_uid(uid), u'arg': token, 83 | u'autocomplete': account}, account, 84 | 'Post {} at cursor'.format(token), 'icon.png') 85 | 86 | def time_remaining_item(self): 87 | # The uid for the remaining time will be the current time, 88 | # so it will appears always at the last position in the list 89 | time_remaining = otp.get_totp_time_remaining() 90 | return alfred.Item({u'uid': time.time(), u'arg': '', u'ignore': 'yes'}, 91 | 'Time Remaining: {}s'.format(time_remaining), 92 | None, 'time.png') 93 | 94 | def config_file_is_empty_item(self): 95 | return self.warning_item(title='GAuth is not yet configured', 96 | message='You must add your secrets to ' 97 | 'the {} file (see documentation)' 98 | .format(self._config_file)) 99 | 100 | def search_by_account_iter(self, query): 101 | if self.is_command(query): 102 | return 103 | i = 0 104 | for account in self.config_list_accounts(): 105 | if self.filter_by_account(account, query): 106 | continue 107 | token = self.config_get_account_token(account) 108 | entry = self.account_item(uid=i, account=account, token=token) 109 | if entry: 110 | yield entry 111 | i += 1 112 | if i > 0: 113 | yield self.time_remaining_item() 114 | else: 115 | yield self.warning_item('Account not found', 116 | 'There is no account matching "{}" ' 117 | 'on your configuration file ' 118 | '({})'.format(query, 119 | self._config_file)) 120 | 121 | def add_account(self, account, secret): 122 | if not otp.is_otp_secret_valid(secret): 123 | return "Invalid secret:\n[{0}]".format(secret) 124 | 125 | config_file = open(self.config_file, 'r+') 126 | try: 127 | self.config.add_section(account) 128 | self.config.set(account, "secret", secret) 129 | self.config.write(config_file) 130 | except configparser.DuplicateSectionError: 131 | return "Account already exists:\n[{0}]".format(account) 132 | finally: 133 | config_file.close() 134 | 135 | return "A new account was added:\n[{0}]".format(account) 136 | 137 | def do_search_by_account(self, query): 138 | self.write_items(self.search_by_account_iter(query)) 139 | 140 | def do_add_account(self, query): 141 | try: 142 | account, secret = query.split(",", 1) 143 | account = account.strip() 144 | secret = secret.strip() 145 | except ValueError: 146 | return self.write_text('Invalid arguments!\n' 147 | 'Please enter: account, secret.') 148 | 149 | return self.write_text(self.add_account(account, secret)) 150 | 151 | 152 | def main(action, query): 153 | alfred_gauth = AlfredGAuth() 154 | alfred_gauth.route_action(action, query) 155 | 156 | 157 | if __name__ == "__main__": 158 | main(action=alfred.args()[0], query=alfred.args()[1]) 159 | 160 | -------------------------------------------------------------------------------- /src/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.alfredapp.moul.gauth 7 | connections 8 | 9 | 3859F17F-F608-473F-B690-DDAB90827E02 10 | 11 | 12 | destinationuid 13 | 0C69FA9F-6875-40F5-ADFB-CD20428410A0 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | vitoclose 19 | 20 | 21 | 22 | 7BEE3C7D-A99B-42F8-BCB9-BC625434942E 23 | 24 | 25 | destinationuid 26 | 8D4FBD76-2AC8-4B24-A980-15CEA567F80A 27 | modifiers 28 | 0 29 | modifiersubtext 30 | 31 | vitoclose 32 | 33 | 34 | 35 | 7DD3BDE5-A157-42E5-9376-F681FB50A4EE 36 | 37 | 38 | destinationuid 39 | D2D03A99-8E4B-4042-896C-4126662B8CD4 40 | modifiers 41 | 0 42 | modifiersubtext 43 | 44 | vitoclose 45 | 46 | 47 | 48 | destinationuid 49 | C7915157-5379-4CCC-9076-D0918BB66AB0 50 | modifiers 51 | 1048576 52 | modifiersubtext 53 | Copy {query} to Clipboard 54 | vitoclose 55 | 56 | 57 | 58 | destinationuid 59 | 3859F17F-F608-473F-B690-DDAB90827E02 60 | modifiers 61 | 524288 62 | modifiersubtext 63 | Large Type {query} 64 | vitoclose 65 | 66 | 67 | 68 | 8D4FBD76-2AC8-4B24-A980-15CEA567F80A 69 | 70 | 71 | destinationuid 72 | EBE66B7E-A102-4C7D-8E1E-FFC0D86ADF1F 73 | modifiers 74 | 0 75 | modifiersubtext 76 | 77 | vitoclose 78 | 79 | 80 | 81 | C7915157-5379-4CCC-9076-D0918BB66AB0 82 | 83 | 84 | destinationuid 85 | C74DB286-B179-49BB-947D-91B3706ADB20 86 | modifiers 87 | 0 88 | modifiersubtext 89 | 90 | vitoclose 91 | 92 | 93 | 94 | destinationuid 95 | D28AB140-ED93-4395-AD40-4A240B9BDD8D 96 | modifiers 97 | 0 98 | modifiersubtext 99 | 100 | vitoclose 101 | 102 | 103 | 104 | D2D03A99-8E4B-4042-896C-4126662B8CD4 105 | 106 | 107 | destinationuid 108 | 1086994E-3BDA-4202-8551-27D286CC5E7A 109 | modifiers 110 | 0 111 | modifiersubtext 112 | 113 | vitoclose 114 | 115 | 116 | 117 | destinationuid 118 | 9C87E9F8-DFE4-4A19-AE02-F468FE5DA32B 119 | modifiers 120 | 0 121 | modifiersubtext 122 | 123 | vitoclose 124 | 125 | 126 | 127 | 128 | createdby 129 | Manfred Touron 130 | description 131 | Google Authenticator Workflow 132 | disabled 133 | 134 | name 135 | Google Authenticator 136 | objects 137 | 138 | 139 | config 140 | 141 | autopaste 142 | 143 | clipboardtext 144 | {query} 145 | ignoredynamicplaceholders 146 | 147 | transient 148 | 149 | 150 | type 151 | alfred.workflow.output.clipboard 152 | uid 153 | 1086994E-3BDA-4202-8551-27D286CC5E7A 154 | version 155 | 3 156 | 157 | 158 | config 159 | 160 | concurrently 161 | 162 | escaping 163 | 127 164 | script 165 | printf "{query}" 166 | scriptargtype 167 | 0 168 | scriptfile 169 | 170 | type 171 | 0 172 | 173 | type 174 | alfred.workflow.action.script 175 | uid 176 | D2D03A99-8E4B-4042-896C-4126662B8CD4 177 | version 178 | 2 179 | 180 | 181 | config 182 | 183 | lastpathcomponent 184 | 185 | onlyshowifquerypopulated 186 | 187 | removeextension 188 | 189 | text 190 | Verification code ({query}) has been written at the Cursor 191 | title 192 | Google Authenticator 193 | 194 | type 195 | alfred.workflow.output.notification 196 | uid 197 | 9C87E9F8-DFE4-4A19-AE02-F468FE5DA32B 198 | version 199 | 1 200 | 201 | 202 | config 203 | 204 | lastpathcomponent 205 | 206 | onlyshowifquerypopulated 207 | 208 | removeextension 209 | 210 | text 211 | Verification code ({query}) has been copied to the clipboard 212 | title 213 | Google Authenticator 214 | 215 | type 216 | alfred.workflow.output.notification 217 | uid 218 | C74DB286-B179-49BB-947D-91B3706ADB20 219 | version 220 | 1 221 | 222 | 223 | config 224 | 225 | concurrently 226 | 227 | escaping 228 | 127 229 | script 230 | printf "{query}" 231 | 232 | scriptargtype 233 | 0 234 | scriptfile 235 | 236 | type 237 | 0 238 | 239 | type 240 | alfred.workflow.action.script 241 | uid 242 | C7915157-5379-4CCC-9076-D0918BB66AB0 243 | version 244 | 2 245 | 246 | 247 | config 248 | 249 | alfredfiltersresults 250 | 251 | alfredfiltersresultsmatchmode 252 | 0 253 | argumenttreatemptyqueryasnil 254 | 255 | argumenttrimmode 256 | 0 257 | argumenttype 258 | 1 259 | escaping 260 | 63 261 | keyword 262 | gauth 263 | queuedelaycustom 264 | 1 265 | queuedelayimmediatelyinitially 266 | 267 | queuedelaymode 268 | 0 269 | queuemode 270 | 1 271 | runningsubtext 272 | Please wait... 273 | script 274 | python3 workflow.py search_by_account "{query}" 275 | scriptargtype 276 | 0 277 | scriptfile 278 | 279 | subtext 280 | Account 281 | title 282 | Google Authenticator 283 | type 284 | 0 285 | withspace 286 | 287 | 288 | type 289 | alfred.workflow.input.scriptfilter 290 | uid 291 | 7DD3BDE5-A157-42E5-9376-F681FB50A4EE 292 | version 293 | 3 294 | 295 | 296 | config 297 | 298 | autopaste 299 | 300 | clipboardtext 301 | {query} 302 | ignoredynamicplaceholders 303 | 304 | transient 305 | 306 | 307 | type 308 | alfred.workflow.output.clipboard 309 | uid 310 | D28AB140-ED93-4395-AD40-4A240B9BDD8D 311 | version 312 | 3 313 | 314 | 315 | config 316 | 317 | argumenttype 318 | 0 319 | keyword 320 | gauth add 321 | subtext 322 | Account name, secret 323 | text 324 | Add a new secret 325 | withspace 326 | 327 | 328 | type 329 | alfred.workflow.input.keyword 330 | uid 331 | 7BEE3C7D-A99B-42F8-BCB9-BC625434942E 332 | version 333 | 1 334 | 335 | 336 | config 337 | 338 | alignment 339 | 0 340 | backgroundcolor 341 | 342 | fadespeed 343 | 0 344 | fillmode 345 | 0 346 | font 347 | 348 | ignoredynamicplaceholders 349 | 350 | largetypetext 351 | 352 | textcolor 353 | 354 | wrapat 355 | 50 356 | 357 | type 358 | alfred.workflow.output.largetype 359 | uid 360 | 0C69FA9F-6875-40F5-ADFB-CD20428410A0 361 | version 362 | 3 363 | 364 | 365 | config 366 | 367 | concurrently 368 | 369 | escaping 370 | 127 371 | script 372 | echo "{query}" 373 | scriptargtype 374 | 0 375 | scriptfile 376 | 377 | type 378 | 0 379 | 380 | type 381 | alfred.workflow.action.script 382 | uid 383 | 3859F17F-F608-473F-B690-DDAB90827E02 384 | version 385 | 2 386 | 387 | 388 | config 389 | 390 | concurrently 391 | 392 | escaping 393 | 127 394 | script 395 | python3 workflow.py add_account "{query}" 396 | scriptargtype 397 | 0 398 | scriptfile 399 | 400 | type 401 | 0 402 | 403 | type 404 | alfred.workflow.action.script 405 | uid 406 | 8D4FBD76-2AC8-4B24-A980-15CEA567F80A 407 | version 408 | 2 409 | 410 | 411 | config 412 | 413 | lastpathcomponent 414 | 415 | onlyshowifquerypopulated 416 | 417 | removeextension 418 | 419 | text 420 | {query} 421 | title 422 | Google Authenticator 423 | 424 | type 425 | alfred.workflow.output.notification 426 | uid 427 | EBE66B7E-A102-4C7D-8E1E-FFC0D86ADF1F 428 | version 429 | 1 430 | 431 | 432 | readme 433 | Description 434 | ——————————— 435 | Equivalent of the mobile versions of Google Authenticator: https://itunes.apple.com/en/app/google-authenticator/id388497605?mt=8. 436 | 437 | System Modifications 438 | ———————————————————— 439 | Create a ~/.gauth file with your secrets, ie: 440 | [google - bob@gmail.com] 441 | secret = xxxxxxxxxxxxxxxxxx 442 | 443 | [evernote - robert] 444 | secret = yyyyyyyyyyyyyyyyyy 445 | Source Code: Github 446 | https://github.com/m...-workflow-gauth 447 | 448 | Links 449 | ————— 450 | - Packal: http://www.packal.org/workflow/gauth-google-authenticator 451 | - Direct Download: https://github.com/packal/repository/raw/master/com.alfredapp.moul.gauth/google_authenticator.alfredworkflow 452 | - Official Forum Post: http://www.alfredforum.com/topic/4062-gauth-google-authenticator-time-based-two-factor-authentication/ 453 | - Github: https://github.com/moul/alfred-workflow-gauth/ 454 | 455 | Acknowledgments 456 | ——————————————- 457 | - Original alarm clock icon 458 | - Alex Auda Samora from The Noun Project 459 | - Licensed under Creative Commons Attribution 460 | - Status & signs icons 461 | - Hereldar Terkenya 462 | - Licensed under a Creative Commons Attribution-Share Alike 3.0 License 463 | - Original source code 464 | - Manfred Touron 465 | - Serial contributor 466 | - Gilberto Olimpio 467 | uidata 468 | 469 | 0C69FA9F-6875-40F5-ADFB-CD20428410A0 470 | 471 | xpos 472 | 700 473 | ypos 474 | 650 475 | 476 | 1086994E-3BDA-4202-8551-27D286CC5E7A 477 | 478 | xpos 479 | 700 480 | ypos 481 | 10 482 | 483 | 3859F17F-F608-473F-B690-DDAB90827E02 484 | 485 | xpos 486 | 500 487 | ypos 488 | 650 489 | 490 | 7BEE3C7D-A99B-42F8-BCB9-BC625434942E 491 | 492 | xpos 493 | 300 494 | ypos 495 | 520 496 | 497 | 7DD3BDE5-A157-42E5-9376-F681FB50A4EE 498 | 499 | xpos 500 | 300 501 | ypos 502 | 400 503 | 504 | 8D4FBD76-2AC8-4B24-A980-15CEA567F80A 505 | 506 | xpos 507 | 500 508 | ypos 509 | 770 510 | 511 | 9C87E9F8-DFE4-4A19-AE02-F468FE5DA32B 512 | 513 | xpos 514 | 700 515 | ypos 516 | 150 517 | 518 | C74DB286-B179-49BB-947D-91B3706ADB20 519 | 520 | xpos 521 | 700 522 | ypos 523 | 330 524 | 525 | C7915157-5379-4CCC-9076-D0918BB66AB0 526 | 527 | xpos 528 | 500 529 | ypos 530 | 400 531 | 532 | D28AB140-ED93-4395-AD40-4A240B9BDD8D 533 | 534 | xpos 535 | 700 536 | ypos 537 | 470 538 | 539 | D2D03A99-8E4B-4042-896C-4126662B8CD4 540 | 541 | xpos 542 | 500 543 | ypos 544 | 80 545 | 546 | EBE66B7E-A102-4C7D-8E1E-FFC0D86ADF1F 547 | 548 | xpos 549 | 700 550 | ypos 551 | 770 552 | 553 | 554 | variablesdontexport 555 | 556 | version 557 | 558 | webaddress 559 | https://github.com/moul/alfred-workflow-gauth 560 | 561 | 562 | --------------------------------------------------------------------------------