diff --git a/pass2csv.py b/pass2csv.py index d68b538..d276ae4 100755 --- a/pass2csv.py +++ b/pass2csv.py @@ -1,114 +1,168 @@ #!/usr/bin/env python3 import csv -import os -import sys -import gnupg -import re import logging +import os +import re +from argparse import ArgumentParser -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +import gnupg -# Set to True to allow for alternate password csv to be created -# See README for differences -KPX_FORMAT=True -if KPX_FORMAT: - # A list of possible fields (in order) that could be converted to login fields - LOGIN_FIELDS=['login', 'user', 'username', 'email'] - # Set to True to extract url fields - GET_URL=True - # A regular expression list of lines that should be excluded from the notes field - EXCLUDE_ROWS=['^---$', '^autotype ?: ?'] +class CSVExporter(): -logger.info("Using KPX format: %s", KPX_FORMAT) + def __init__(self, kpx_format): -def traverse(path): - for root, dirs, files in os.walk(path): - if '.git' in dirs: - dirs.remove('.git') - for name in files: - yield os.path.join(root, name) + logging.basicConfig(level=logging.INFO) + self.logger = logging.getLogger(__name__) -def getMetadata(notes_raw): - lines = notes_raw.split('\n') + # Set to True to allow for alternate password csv to be created + # See README for differences + self.kpx_format = kpx_format - # A list of lines to keep as notes (will be joined by newline) - notes = [] - # The extracted user field - user = '' - # The extracted URL field - url = '' + if self.kpx_format: + # A list of possible fields (in order) that could be converted to login fields + self.login_fields = ['login', 'user', 'username', 'email'] + # Set to True to extract url fields + self.get_url = True + # A regular expression list of lines that should be excluded from the notes field + self.exclude_rows = ['^---$', '^autotype ?: ?'] - # This will extract each field name (for example, if a line in notes was `user: user1`, fields should contain 'user') - all_fields = set() - for line in lines: - field_search = re.search('^(.*) ?: ?.*$', line, re.I) - if field_search: - all_fields.add(field_search.group(1)) + self.logger.info("Using KPX format: %s", self.kpx_format) - # Check if any of the fields match the login names - login_fields = [field for field in LOGIN_FIELDS if field in all_fields] - # Get the field to use for the login. Since LOGIN_FIELDS is in order, the 0th element will contain the first match - login_field = None if not login_fields else login_fields[0] + def traverse(self, path): - # Iterate through the file again to build the return array - for line in lines: + for root, dirs, files in os.walk(path): + if '.git' in dirs: + dirs.remove('.git') + for name in files: + yield os.path.join(root, name) - # If any of the exclusion patterns match, ignore the line - if len([pattern for pattern in EXCLUDE_ROWS if re.search(pattern, line, re.I)]) != 0: - continue + def getMetadata(self, notes_raw): - if login_field: - user_search = re.search('^' + login_field + ' ?: ?(.*)$', line, re.I) - if user_search: - user = user_search.group(1) - # The user was matched, don't add it to notes + lines = notes_raw.split('\n') + + # A list of lines to keep as notes (will be joined by newline) + notes = [] + # The extracted user field + user = '' + # The extracted URL field + url = '' + + # This will extract each field name (for example, if a line in notes was `user: user1`, fields should contain 'user') + all_fields = set() + for line in lines: + field_search = re.search('^(.*) ?: ?.*$', line, re.I) + if field_search: + all_fields.add(field_search.group(1)) + + # Check if any of the fields match the login names + login_fields = [ + field for field in self.login_fields if field in all_fields] + # Get the field to use for the login. Since self.login_fields is in order, the 0th element will contain the first match + login_field = None if not login_fields else login_fields[0] + + # Iterate through the file again to build the return array + for line in lines: + + # If any of the exclusion patterns match, ignore the line + if [pattern for pattern in self.exclude_rows if re.search(pattern, line, re.I)]: continue - if GET_URL: - url_search = re.search('^url ?: ?(.*)$', line, re.I) - if url_search: - url = url_search.group(1) - # The url was matched, don't add it to notes - continue + if login_field: + user_search = re.search( + '^' + login_field + ' ?: ?(.*)$', line, re.I) + if user_search: + user = user_search.group(1) + # The user was matched, don't add it to notes + continue - notes.append(line) + if self.get_url: + url_search = re.search('^url ?: ?(.*)$', line, re.I) + if url_search: + url = url_search.group(1) + # The url was matched, don't add it to notes + continue - return (user, url, '\n'.join(notes).strip()) + notes.append(line) -def parse(basepath, path, data): - name = os.path.splitext(os.path.basename(path))[0] - group = os.path.dirname(os.path.os.path.relpath(path, basepath)) - split_data = data.split('\n', maxsplit=1) - password = split_data[0] - # Perform if/else in case there are no notes for a field - notes = split_data[1] if len(split_data) > 1 else "" - logger.info("Processing %s" % (name,)) - if KPX_FORMAT: - # We are using the advanced format; try extracting user and url - user, url, notes = getMetadata(notes) - return [group, name, user, password, url, notes] - else: - # We are not using KPX format; just use notes - return [group, name, password, notes] + return (user, url, '\n'.join(notes).strip()) + + def parse(self, basepath, path, data): + + name = os.path.splitext(os.path.basename(path))[0] + group = os.path.dirname(os.path.os.path.relpath(path, basepath)) + split_data = data.split('\n', maxsplit=1) + password = split_data[0] + # Perform if/else in case there are no notes for a field + notes = split_data[1] if len(split_data) > 1 else "" + self.logger.info("Processing %s", name) + if self.kpx_format: + # We are using the advanced format; try extracting user and url + user, url, notes = self.getMetadata(notes) + return [group, name, user, password, url, notes] + else: + # We are not using KPX format; just use notes + return [group, name, password, notes] -def main(path): - gpg = gnupg.GPG() +def main(kpx_format, gpgbinary, use_agent, pass_path): + """Main script entrypoint.""" + + exporter = CSVExporter(kpx_format) + gpg = gnupg.GPG(use_agent=use_agent, gpgbinary=gpgbinary) gpg.encoding = 'utf-8' csv_data = [] - for file_path in traverse(path): + for file_path in exporter.traverse(pass_path): if os.path.splitext(file_path)[1] == '.gpg': with open(file_path, 'rb') as f: data = str(gpg.decrypt_file(f)) - csv_data.append(parse(path, file_path, data)) + if str == "": + raise ValueError("The password file is empty") + csv_data.append(exporter.parse(pass_path, file_path, data)) with open('pass.csv', 'w', newline='') as csv_file: writer = csv.writer(csv_file, delimiter=',') writer.writerows(csv_data) +class OptionsParser(ArgumentParser): + """Regular ArgumentParser with the script's options.""" + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + self.add_argument( + 'pass_path', + metavar='path', + type=str, + help="Path to the PasswordStore folder to use", + ) + + self.add_argument( + '-a', '--agent', + action='store_true', + help="Use this option to ask gpg to use it's auth agent", + dest='use_agent', + ) + + self.add_argument( + '-b', '--gpgbinary', + type=str, + help="Path to the gpg binary you wish to use", + dest='gpgbinary', + default="gpg" + ) + + self.add_argument( + '-x', '--kpx', + action='store_true', + help="Use this option to format the CSV for KeePassXC", + dest='kpx_format', + ) + + if __name__ == '__main__': - path = os.path.abspath(sys.argv[1]) - main(path) + PARSER = OptionsParser() + ARGS = PARSER.parse_args() + main(**vars(ARGS)) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d272d34 --- /dev/null +++ b/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup + +setup( + name='pass2csv', + version="0.1.0", + description='pass2csv', + scripts=['pass2csv.py'], + install_requires=["python-gnupg"] +)