pass2bw/pass2csv
2021-05-19 21:30:10 +02:00

277 lines
7.6 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import csv
import logging
import pathlib
import re
import sys
import gnupg
logging.basicConfig(level=logging.INFO)
def set_meta(entry, path, grouping_base):
pure_path = pathlib.PurePath(path)
group = pure_path.relative_to(grouping_base).parent
if group.name == '':
group = ''
entry['group'] = group
entry['title'] = pure_path.stem
def set_data(entry, data, exclude, get_fields, get_lines):
lines = data.splitlines()
tail = lines[1:]
entry['password'] = lines[0]
filtered_tail = []
for line in tail:
for exclude_pattern in exclude:
if exclude_pattern.search(line):
break
else:
filtered_tail.append(line)
matching_indices = set()
fields = entry.setdefault('fields', {})
for i, line in enumerate(filtered_tail):
for (name, pattern) in get_fields:
if name in fields:
# multiple patterns with same name, we've already found a match
continue
match = pattern.search(line)
if not match:
continue
inverse_match = line[0:match.start()] + line[match.end():]
value = inverse_match.strip()
fields[name] = value
matching_indices.add(i)
break
matching_lines = {}
for i, line in enumerate(filtered_tail):
for (name, pattern) in get_lines:
match = pattern.search(line)
if not match:
continue
matches = matching_lines.setdefault(name, [])
matches.append(line)
matching_indices.add(i)
break
for name, matches in matching_lines.items():
fields[name] = '\n'.join(matching_lines)
final_tail = []
for i, line in enumerate(filtered_tail):
if i not in matching_indices:
final_tail.append(line)
entry['notes'] = '\n'.join(final_tail).strip()
def write(file, entries, get_fields, get_lines):
get_field_names = set(x[0] for x in get_fields)
get_line_names = set(x[0] for x in get_lines)
field_names = get_field_names | get_line_names
header = ["Group(/)", "Title", "Password", *field_names, "Notes"]
csvw = csv.writer(file)
logging.info("Writing data to %s", file.name)
csvw.writerow(header)
for entry in entries:
fields = [entry['fields'].get(name) for name in field_names]
columns = [
entry['group'], entry['title'], entry['password'],
*fields,
entry['notes']
]
csvw.writerow(columns)
def main(store_path, grouping_base, outfile, gpgbinary, use_agent, encoding,
exclude, get_fields, get_lines):
entries = []
failures = []
path = pathlib.Path(store_path)
grouping_path = pathlib.Path(grouping_base)
gpg = gnupg.GPG(gpgbinary=gpgbinary, use_agent=use_agent)
gpg.encoding = encoding
for file in path.glob('**/*.gpg'):
logging.info("Processing %s", file)
with open(file, 'rb') as fp:
decrypted = gpg.decrypt_file(fp)
if not decrypted.ok:
logging.error("Could not decrypt %s: %s", file, decrypted.status)
failures.append((file, decrypted.status))
continue
entry = {}
set_meta(entry, file, grouping_path)
set_data(entry, str(decrypted), exclude, get_fields, get_lines)
entries.append(entry)
if failures:
for file, status in failures:
logging.warning("Could not decrypt %s: %s", file, status)
write(outfile, entries, get_fields, get_lines)
def parse_args(args):
class ExtendAction(argparse.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)
parser = argparse.ArgumentParser()
parser.add_argument(
'store_path',
type=str,
help="path to the password-store to export",
)
parser.add_argument(
'-b', '--base',
metavar='path',
type=str,
help="path to use as base for grouping passwords",
dest='base_path'
)
parser.add_argument(
'-g', '--gpg',
metavar='executable',
type=str,
default="gpg",
help="path to the gpg binary you wish to use (default 'gpg')",
dest='gpgbinary'
)
parser.add_argument(
'-a', '--use-agent',
action='store_true',
default=False,
help="ask gpg to use its auth agent",
dest='use_agent'
)
parser.add_argument(
'--encoding',
metavar='encoding',
type=str,
default="utf-8",
help="text encoding to use when reading gpg output (default 'utf-8')",
dest='encoding'
)
parser.add_argument(
'-o', '--outfile',
metavar='file',
type=argparse.FileType('w'),
default="-",
help="file to write exported data to (default stdin)",
dest='outfile'
)
parser.add_argument(
'-e', '--exclude',
metavar='pattern',
action=ExtendAction,
nargs='+',
type=str,
default=[],
help="regexps for lines which should not be exported",
dest='exclude'
)
parser.add_argument(
'-f', '--get-field',
metavar=('name', 'pattern'),
action='append',
nargs=2,
type=str,
default=[],
help=(
"a name and a regexp, the part of the line matching the regexp"
" will be removed and the remaining line will be added to a field"
" with the chosen name. only one match per password, matching"
" stops after the first match"
),
dest='get_fields'
)
parser.add_argument(
'-l', '--get-line',
metavar=('name', 'pattern'),
action='append',
nargs=2,
type=str,
default=[],
help=(
"a name and a regexp for which all lines that match are included"
" in a field with the chosen name"
),
dest='get_lines'
)
return parser.parse_args(args)
def compile_regexp(pattern):
try:
regexp = re.compile(pattern, re.I)
except re.error as e:
logging.error(
"Could not compile pattern '%s', %s at position %s",
pattern.replace("'", "\\'"), e.msg, e.pos
)
return None
return regexp
if __name__ == '__main__':
parsed = parse_args(sys.argv[1:])
failed = False
exclude_patterns = []
for pattern in parsed.exclude:
regexp = compile_regexp(pattern)
if not regexp:
failed = True
exclude_patterns.append(regexp)
get_fields = []
for (name, pattern) in parsed.get_fields:
regexp = compile_regexp(pattern)
if not regexp:
failed = True
get_fields.append((name, regexp))
get_lines = []
for (name, pattern) in parsed.get_lines:
regexp = compile_regexp(pattern)
if not regexp:
failed = True
get_lines.append((name, regexp))
if failed:
sys.exit(1)
if parsed.base_path:
grouping_base = parsed.base_path
else:
grouping_base = parsed.store_path
kwargs = {
'store_path': parsed.store_path,
'grouping_base': grouping_base,
'gpgbinary': parsed.gpgbinary,
'use_agent': parsed.use_agent,
'encoding': parsed.encoding,
'outfile': parsed.outfile,
'exclude': exclude_patterns,
'get_fields': get_fields,
'get_lines': get_lines
}
main(**kwargs)