diff options
Diffstat (limited to 'dot_local/bin/executable_pass2csv')
-rw-r--r-- | dot_local/bin/executable_pass2csv | 308 |
1 files changed, 308 insertions, 0 deletions
diff --git a/dot_local/bin/executable_pass2csv b/dot_local/bin/executable_pass2csv new file mode 100644 index 0000000..d4af0f9 --- /dev/null +++ b/dot_local/bin/executable_pass2csv @@ -0,0 +1,308 @@ +#!/bin/python +import argparse +import csv +import logging +import pathlib +import re +import sys + +import gnupg + +logging.basicConfig(level=logging.INFO, format='%(message)s') + + +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(matches) + + 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("\nWriting data to %s\n", 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, outfile, grouping_base, gpgbinary, use_agent, encodings, + 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) + files = path.glob('**/*.gpg') + if not path.is_dir(): + if path.is_file(): + files = [path] + else: + err = "No such file or directory: {}".format(path) + logging.error(err) + sys.exit(1) + for file in files: + logging.info("Processing %s", file) + with open(file, 'rb') as fp: + decrypted = gpg.decrypt_file(fp) + if not decrypted.ok: + err = "Could not decrypt {}: {}".format(file, decrypted.status) + logging.error(err) + failures.append(err) + continue + for i, encoding in enumerate(encodings): + try: + # decrypted.data is bytes + decrypted_data = decrypted.data.decode(encoding) + except Exception as e: + logging.warning( + "Could not decode {} with encoding {}: {}" + .format(file, encoding, e) + ) + continue + if i > 0: + # don't log if the first encoding worked + logging.warning("Decoded {} with encoding {}".format(file, encoding)) + break + else: + err = "Could not decode {}, see messages above for more info.".format(file) + failures.append(err) + continue + entry = {} + set_meta(entry, file, grouping_path) + set_data(entry, decrypted_data, exclude, get_fields, get_lines) + entries.append(entry) + if failures: + logging.warning("\nGot errors while processing files:") + for err in failures: + logging.warning(err) + write(outfile, entries, get_fields, get_lines) + + +def parse_args(args=None): + parser = argparse.ArgumentParser() + parser.add_argument( + 'store_path', + metavar='STOREPATH', + type=str, + help="path to the password-store to export", + ) + + parser.add_argument( + 'outfile', + metavar='OUTFILE', + type=argparse.FileType('w'), + help="file to write exported data to, use - for stdout", + ) + + 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( + '--encodings', + metavar='encodings', + type=str, + default="utf-8", + help=( + "comma-separated text encodings to try, in order, when decoding" + " gpg output (default 'utf-8')" + ), + dest='encodings' + ) + + parser.add_argument( + '-e', '--exclude', + metavar='pattern', + action='append', + type=str, + default=[], + help=( + "regexp for lines which should not be exported, can be specified" + " multiple times" + ), + 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() + + 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 + + encodings = [e for e in parsed.encodings.split(',') if e] + if not encodings: + logging.error( + "Did not understand '--encodings {}'".format(parsed.encoding) + ) + sys.exit(1) + + kwargs = { + 'store_path': parsed.store_path, + 'outfile': parsed.outfile, + 'grouping_base': grouping_base, + 'gpgbinary': parsed.gpgbinary, + 'use_agent': parsed.use_agent, + 'encodings': encodings, + 'exclude': exclude_patterns, + 'get_fields': get_fields, + 'get_lines': get_lines + } + + main(**kwargs) |