From 53295f53dda3ac72da861b197d5d2482a93b808f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 12 Feb 2025 22:05:25 +0200 Subject: [PATCH] Implemented Apple Keychain support with migration procedure --- Google Authenticator.alfredworkflow | Bin 59525 -> 58234 bytes src/otp.py | 12 +- src/storage/apple_keychain_storage.py | 123 +++++++++++++++++++++ src/storage/exceptions.py | 3 + src/storage/plain_text_storage.py | 67 ++++++++++++ src/storage/storage_interface.py | 21 ++++ src/workflow.py | 152 ++++++++++++++------------ 7 files changed, 300 insertions(+), 78 deletions(-) create mode 100644 src/storage/apple_keychain_storage.py create mode 100644 src/storage/exceptions.py create mode 100644 src/storage/plain_text_storage.py create mode 100644 src/storage/storage_interface.py diff --git a/Google Authenticator.alfredworkflow b/Google Authenticator.alfredworkflow index fddb4ddde35080dc19910b4f3080793363b84d0c..9e32e70fb9e2e3d3199520935c00e343c812133d 100644 GIT binary patch delta 10541 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
%aJ6KyFBefk_>ash?R^{<&M3v?3!- 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 WICu~iL=h~=w*d|WDqXnW-~R{X3J5X) delta 11864 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"([^"]+)"', line) + if regs: + account = regs.group(1) + continue + + if line == "data:" and account: + capture_secret = True + continue + + if capture_secret: + results[account] = line.strip('"') + + # Reset for the next entry + account = None + capture_secret = False + + return results + + def get_accounts(self): + return self._data.keys() + + def add_account(self, account, secret): + if account in self._data: + return False + + try: + subprocess.run( + ["security", "add-generic-password", "-a", self._name, "-s", account, "-w", secret, self._file], + check=True + ) + + self._data[account] = secret + + return True + except subprocess.CalledProcessError as e: + return False + + def _run_command(self, command): + try: + result = subprocess.run(command, capture_output=True, text=True, check=True) + return result.stdout.splitlines() + except subprocess.CalledProcessError as e: + print(f"Error executing security command: {e}") + return [] diff --git a/src/storage/exceptions.py b/src/storage/exceptions.py new file mode 100644 index 0000000..401cf37 --- /dev/null +++ b/src/storage/exceptions.py @@ -0,0 +1,3 @@ +class StorageException(Exception): + def __init__(self, file): + self.file = file diff --git a/src/storage/plain_text_storage.py b/src/storage/plain_text_storage.py new file mode 100644 index 0000000..ae1a506 --- /dev/null +++ b/src/storage/plain_text_storage.py @@ -0,0 +1,67 @@ +import os +import configparser +from storage.exceptions import StorageException +from storage.storage_interface import StorageInterface + + +class PlainTextStorage(StorageInterface): + _config_file_initial_content = """ +#Examples of valid configurations: +#[google - bob@gmail.com] +#secret=xxxxxxxxxxxxxxxxxx +# +#[evernote - robert] +#secret=yyyyyyyyyyyyyyyyyy +""" + + def __init__(self, config_file='~/.gauth'): + self._config_file = config_file + self.config_file = os.path.expanduser(self._config_file) + + self.config = configparser.RawConfigParser() + + # If the configuration file doesn't exist, create an empty one + if not os.path.isfile(self.config_file): + self.create_storage() + + def get_file(self): + return self._config_file + + def create_storage(self): + if os.path.isfile(self.config_file): + return + + with open(self.config_file, 'w') as f: + f.write(self._config_file_initial_content) + f.close() + + def validate_storage(self): + try: + self.config.read(self.config_file) + except Exception as e: + raise StorageException(self._config_file) from e + + def is_empty(self): + return not self.config.sections() + + def get_secret(self, account): + try: + return self.config.get(account, 'secret') + except: + raise Exception("Service not found") + + def get_accounts(self): + return self.config.sections() + + def add_account(self, account, secret): + config_file = open(self.config_file, 'r+') + try: + self.config.add_section(account) + self.config.set(account, "secret", secret) + self.config.write(config_file) + except configparser.DuplicateSectionError: + return False + finally: + config_file.close() + + return True diff --git a/src/storage/storage_interface.py b/src/storage/storage_interface.py new file mode 100644 index 0000000..acf7139 --- /dev/null +++ b/src/storage/storage_interface.py @@ -0,0 +1,21 @@ +class StorageInterface: + def get_file(self): + pass + + def create_storage(self): + pass + + def validate_storage(self): + pass + + def is_empty(self): + pass + + def get_secret(self, account): + pass + + def get_accounts(self): + pass + + def add_account(self, account, secret): + pass diff --git a/src/workflow.py b/src/workflow.py index 6851470..173b651 100644 --- a/src/workflow.py +++ b/src/workflow.py @@ -1,80 +1,42 @@ # -*- coding: utf-8 -*- -import os -import configparser import time import alfred import otp +from storage.plain_text_storage import PlainTextStorage +from storage.apple_keychain_storage import AppleKeychainStorage +from storage.exceptions import StorageException class AlfredGAuth(alfred.AlfredWorkflow): - _config_file_initial_content = """ -#Examples of valid configurations: -#[google - bob@gmail.com] -#secret=xxxxxxxxxxxxxxxxxx -# -#[evernote - robert] -#secret=yyyyyyyyyyyyyyyyyy -""" - _reserved_words = ['add', 'update', 'remove'] - def __init__(self, config_file='~/.gauth', max_results=20): + def __init__(self, max_results=20): self.max_results = max_results - self._config_file = config_file - self.config_file = os.path.expanduser(self._config_file) - self.config = configparser.RawConfigParser() - self.config.read(self.config_file) + self._init_storage() - # If the configuration file doesn't exist, create an empty one - if not os.path.isfile(self.config_file): - self.create_config() + def _init_storage(self): + self._storage = AppleKeychainStorage() try: - if not self.config.sections(): - # If the configuration file is empty, - # tell the user to add secrets to it - self.write_item(self.config_file_is_empty_item()) - return - except Exception as e: + self._storage.validate_storage() + except StorageException as e: item = self.exception_item(title='{}: Invalid syntax' - .format(self._config_file), - exception=e) + .format(str(e)), + exception=e.__context__) self.write_item(item) - def create_config(self): - with open(self.config_file, 'w') as f: - f.write(self._config_file_initial_content) - f.close() - def config_get_account_token(self, account): try: - secret = self.config.get(account, 'secret') - except: - secret = None - - try: - key = self.config.get(account, 'key') - except: - key = None - - try: - hexkey = self.config.get(account, 'hexkey') - except: - hexkey = None - - try: - key = otp.get_hotp_key(secret=secret, key=key, hexkey=hexkey) + secret = self._storage.get_secret(account) + key = otp.get_hotp_key(secret) except: key = '' return otp.get_totp_token(key) - def config_list_accounts(self): - return self.config.sections() - def filter_by_account(self, account, query): return len(query.strip()) and not query.lower() in str(account).lower() @@ -85,30 +47,40 @@ def account_item(self, account, token, uid=None): def time_remaining_item(self): # The uid for the remaining time will be the current time, - # so it will appears always at the last position in the list + # so it will appear always at the last position in the list time_remaining = otp.get_totp_time_remaining() return alfred.Item({u'uid': time.time(), u'arg': '', u'ignore': 'yes'}, 'Time Remaining: {}s'.format(time_remaining), None, 'time.png') + def detect_empty_storage(self): + if self._storage.is_empty(): + # If the configuration file is empty, + # tell the user to add secrets to it + self.write_item(self.config_file_is_empty_item()) + def config_file_is_empty_item(self): return self.warning_item(title='GAuth is not yet configured', message='You must add your secrets to ' 'the {} file (see documentation)' - .format(self._config_file)) + .format(self._storage.get_file())) def search_by_account_iter(self, query): if self.is_command(query): return + i = 0 - for account in self.config_list_accounts(): + for account in self._storage.get_accounts(): if self.filter_by_account(account, query): continue + token = self.config_get_account_token(account) entry = self.account_item(uid=i, account=account, token=token) + if entry: yield entry i += 1 + if i > 0: yield self.time_remaining_item() else: @@ -116,28 +88,26 @@ def search_by_account_iter(self, query): 'There is no account matching "{}" ' 'on your configuration file ' '({})'.format(query, - self._config_file)) + self._storage.get_file())) - def add_account(self, account, secret): + def _add_account(self, account, secret): if not otp.is_otp_secret_valid(secret): - return "Invalid secret:\n[{0}]".format(secret) - - config_file = open(self.config_file, 'r+') - try: - self.config.add_section(account) - self.config.set(account, "secret", secret) - self.config.write(config_file) - except configparser.DuplicateSectionError: - return "Account already exists:\n[{0}]".format(account) - finally: - config_file.close() + raise Exception("Invalid secret:\n[{0}]".format(secret)) - return "A new account was added:\n[{0}]".format(account) + return self._storage.add_account(account, secret) def do_search_by_account(self, query): + self.detect_empty_storage() + self.write_items(self.search_by_account_iter(query)) def do_add_account(self, query): + if query == "migrate": + self._migrate_storage() + return + + self.detect_empty_storage() + try: account, secret = query.split(",", 1) account = account.strip() @@ -146,8 +116,46 @@ def do_add_account(self, query): return self.write_text('Invalid arguments!\n' 'Please enter: account, secret.') - return self.write_text(self.add_account(account, secret)) + try: + if self._add_account(account, secret): + return self.write_text("A new account was added:\n[{0}]".format(account)) + else: + return self.write_text("Account already exists:\n[{0}]".format(account)) + except Exception as e: + return self.write_text(str(e)) + + def _migrate_storage(self): + if self._storage.__class__.__name__ != 'AppleKeychainStorage': + self.write_text('Storage is already migrated') + return + source_storage = PlainTextStorage() + source_storage_file = source_storage.get_file() + + try: + source_storage.validate_storage() + except StorageException as e: + self.write_text(str(e)) + return + + if source_storage.is_empty(): + self.write_text('No accounts to migrate. Please delete "%s" file.' % source_storage_file) + return + + migrated_account_count = 0 + + try: + for account in source_storage.get_accounts(): + secret = source_storage.get_secret(account) + + if self._add_account(account, secret): + migrated_account_count += 1 + + self.write_text('Migrated %s accounts. Please delete "%s" file.' + % (migrated_account_count, source_storage_file)) + except Exception as e: + # Happens, when secret is invalid. + self.write_text(str(e)) def main(action, query): alfred_gauth = AlfredGAuth() @@ -155,5 +163,9 @@ def main(action, query): if __name__ == "__main__": - main(action=alfred.args()[0], query=alfred.args()[1]) + if len(alfred.args()) < 2: + query = "" + else: + query = alfred.args()[1] + main(action=alfred.args()[0], query=query)