pass2bw/pass2csv

196 lines
6.5 KiB
Text
Raw Normal View History

2018-08-15 00:59:11 -04:00
#!/usr/bin/env python3
2016-05-29 19:00:02 -04:00
import csv
2018-12-27 12:12:06 -05:00
import logging
2016-05-29 19:00:02 -04:00
import os
2018-08-14 18:29:18 -04:00
import re
from argparse import Action, ArgumentParser
2016-05-29 19:00:02 -04:00
2018-12-27 12:12:06 -05:00
import gnupg
logger = logging.getLogger(__name__)
2018-12-27 12:12:06 -05:00
2020-06-08 02:16:58 -04:00
class CSVExporter:
def __init__(self, kpx_format, login_fields, get_url, exclude_rows):
2018-12-27 12:12:06 -05:00
logging.basicConfig(level=logging.INFO)
self.logger = logger
2018-12-27 12:12:06 -05:00
# Set to True to allow for alternate password csv to be created
# See README for differences
self.kpx_format = kpx_format
if self.kpx_format:
# A list of possible fields (in order) that could be converted to
# login fields
self.login_fields = login_fields or []
2018-12-27 12:12:06 -05:00
# Set to True to extract url fields
self.get_url = get_url
# A regular expression list of lines that should be excluded from
# the notes field
self.exclude_rows = exclude_rows or []
2018-12-27 12:12:06 -05:00
self.logger.info("Using KPX format: %s", self.kpx_format)
def traverse(self, 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)
2018-08-14 18:29:18 -04:00
2020-06-07 14:39:08 -04:00
def get_metadata(self, notes_raw):
2018-12-27 12:12:06 -05:00
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 = ''
2020-06-07 14:39:08 -04:00
# This will extract each field name (for example, if a line in notes
# was `user: user1`, fields should contain 'user')
2018-12-27 12:12:06 -05:00
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 = [
2020-06-07 14:39:08 -04:00
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
2018-12-27 12:12:06 -05:00
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)]:
2018-08-14 18:29:18 -04:00
continue
2018-12-27 12:12:06 -05:00
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
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
notes.append(line)
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
2020-06-07 14:39:08 -04:00
user, url, notes = self.get_metadata(notes)
2018-12-27 12:12:06 -05:00
return [group, name, user, password, url, notes]
else:
# We are not using KPX format; just use notes
return [group, name, password, notes]
def main(gpgbinary, use_agent, pass_path,
kpx_format, login_fields, get_url, exclude_rows):
exporter = CSVExporter(kpx_format, login_fields, get_url, exclude_rows)
2018-12-27 12:12:06 -05:00
gpg = gnupg.GPG(use_agent=use_agent, gpgbinary=gpgbinary)
2016-05-29 19:00:02 -04:00
gpg.encoding = 'utf-8'
csv_data = []
2018-12-27 12:12:06 -05:00
for file_path in exporter.traverse(pass_path):
2016-05-29 19:00:02 -04:00
if os.path.splitext(file_path)[1] == '.gpg':
with open(file_path, 'rb') as f:
data = str(gpg.decrypt_file(f))
if len(data) == 0:
logger.warning("Could not decrypt %s or it is empty.", file_path)
2018-12-27 12:12:06 -05:00
csv_data.append(exporter.parse(pass_path, file_path, data))
2016-05-29 19:00:02 -04:00
with open('pass.csv', 'w', newline='') as csv_file:
writer = csv.writer(csv_file, delimiter=',')
writer.writerows(csv_data)
class ExtendAction(Action):
# Python 3.8 has 'extend' built in.
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest) or []
items.extend(values)
setattr(namespace, self.dest, items)
2018-12-27 12:12:06 -05:00
class OptionsParser(ArgumentParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_argument(
2019-01-01 08:57:31 -05:00
'pass_path',
metavar='path',
2018-12-27 12:12:06 -05:00
type=str,
help="path to the password-store folder to export",
2018-12-27 12:12:06 -05:00
)
self.add_argument(
'-a', '--agent',
action='store_true',
help="ask gpg to use its auth agent",
2018-12-27 12:12:06 -05:00
dest='use_agent',
)
self.add_argument(
'-b', '--gpgbinary',
type=str,
help="path to the gpg binary you wish to use",
2018-12-27 12:12:06 -05:00
dest='gpgbinary',
default="gpg"
)
self.add_argument(
'-x', '--kpx',
action='store_true',
help="format the CSV for KeePassXC",
2018-12-27 12:12:06 -05:00
dest='kpx_format',
)
self.add_argument(
'-l', '--login-fields',
action=ExtendAction,
nargs='+',
type=str,
help="strings to interpret as names of login fields (only used with -x)"
)
self.add_argument(
'-u', '--get-url',
action='store_true',
help="match row starting with 'url:' and extract it (only used with -x)"
)
self.add_argument(
'-e', '--exclude-rows',
action=ExtendAction,
nargs='+',
type=str,
help="regexps to exclude from the notes field (only used with -x)"
)
2018-12-27 12:12:06 -05:00
2016-05-29 19:00:02 -04:00
if __name__ == '__main__':
2018-12-27 12:12:06 -05:00
PARSER = OptionsParser()
ARGS = PARSER.parse_args()
main(**vars(ARGS))