From 3220fd7dec896a6ccdc16e857c102237209107ea Mon Sep 17 00:00:00 2001 From: Daniele Pizzolli Date: Sat, 2 Jan 2016 16:23:45 +0100 Subject: Add importer for Password Exporter for Firefox To assist the migration from the default Firefox password store to passff. Add also some basic tests. More info at: - - --- contrib/importers/password-exporter2pass.py | 181 ++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100755 contrib/importers/password-exporter2pass.py diff --git a/contrib/importers/password-exporter2pass.py b/contrib/importers/password-exporter2pass.py new file mode 100755 index 0000000..135feda --- /dev/null +++ b/contrib/importers/password-exporter2pass.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2016 Daniele Pizzolli +# +# This file is licensed under GPLv2+. Please see COPYING for more +# information. + +"""Import password(s) exported by Password Exporter for Firefox in +csv format to pass format. Supports Password Exporter format 1.1. +""" + +import argparse +import base64 +import csv +import sys +import subprocess + + +PASS_PROG = 'pass' +DEFAULT_USERNAME = 'login' + + +def main(): + "Parse the arguments and run the passimport with appropriate arguments." + description = """\ + Import password(s) exported by Password Exporter for Firefox in csv + format to pass format. Supports Password Exporter format 1.1. + + Check the first line of your exported file. + + Must start with: + + # Generated by Password Exporter; Export format 1.1; + + Support obfuscated export (wrongly called encrypted by Password Exporter). + + It should help you to migrate from the default Firefox password + store to passff. + + Please note that Password Exporter or passff may have problem with + fields containing characters like " or :. + + More info at: + + + """ + parser = argparse.ArgumentParser(description=description) + parser.add_argument( + "filepath", type=str, + help="The password Exporter generated file") + parser.add_argument( + "-p", "--prefix", type=str, + help="Prefix for pass store path, you may want to use: sites") + parser.add_argument( + "-d", "--force", action="store_true", + help="Call pass with --force option") + parser.add_argument( + "-v", "--verbose", action="store_true", + help="Show pass output") + parser.add_argument( + "-q", "--quiet", action="store_true", + help="No output") + + args = parser.parse_args() + + passimport(args.filepath, prefix=args.prefix, force=args.force, + verbose=args.verbose, quiet=args.quiet) + + +def passimport(filepath, prefix=None, force=False, verbose=False, quiet=False): + "Import the password from filepath to pass" + with open(filepath, 'rb') as csvfile: + # Skip the first line if starts with a comment, as usually are + # file exported with Password Exporter + first_line = csvfile.readline() + + if not first_line.startswith( + '# Generated by Password Exporter; Export format 1.1;'): + sys.exit('Input format not supported') + + # Auto detect if the file is obfuscated + obfuscation = False + if first_line.startswith( + ('# Generated by Password Exporter; ' + 'Export format 1.1; Encrypted: true')): + obfuscation = True + + if not first_line.startswith('#'): + csvfile.seek(0) + + reader = csv.DictReader(csvfile, delimiter=',', quotechar='"') + for row in reader: + try: + username = row['username'] + password = row['password'] + + if obfuscation: + username = base64.b64decode(row['username']) + password = base64.b64decode(row['password']) + + # Not sure if some fiel can be empty, anyway tries to be + # reasonably safe + text = '{}\n'.format(password) + if row['passwordField']: + text += '{}: {}\n'.format(row['passwordField'], password) + if username: + text += '{}: {}\n'.format( + row.get('usernameField', DEFAULT_USERNAME), username) + if row['hostname']: + text += 'Hostname: {}\n'.format(row['hostname']) + if row['httpRealm']: + text += 'httpRealm: {}\n'.format(row['httpRealm']) + if row['formSubmitURL']: + text += 'formSubmitURL: {}\n'.format(row['formSubmitURL']) + + # Remove the protocol prefix for http(s) + simplename = row['hostname'].replace( + 'https://', '').replace('http://', '') + + # Rough protection for fancy username like ā€œ; rm -Rf /\nā€ + userpath = "".join(x for x in username if x.isalnum()) + # TODO add some escape/protection also to the hostname + storename = '{}@{}'.format(userpath, simplename) + storepath = storename + + if prefix: + storepath = '{}/{}'.format(prefix, storename) + + cmd = [PASS_PROG, 'insert', '--multiline'] + + if force: + cmd.append('--force') + + cmd.append(storepath) + + proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = proc.communicate(text) + retcode = proc.wait() + + # TODO: please note that sometimes pass does not return an + # error + # + # After this command: + # + # pass git config --bool --add pass.signcommits true + # + # pass import will fail with: + # + # gpg: skipped "First Last ": + # secret key not available + # gpg: signing failed: secret key not available + # error: gpg failed to sign the data + # fatal: failed to write commit object + # + # But the retcode is still 0. + # + # Workaround: add the first signing key id explicitly with: + # + # SIGKEY=$(gpg2 --list-keys --with-colons user@example.com | \ + # awk -F : '/:s:$/ {printf "0x%s\n", $5; exit}') + # pass git config --add user.signingkey "${SIGKEY}" + + if retcode: + print 'command {}" failed with exit code {}: {}'.format( + " ".join(cmd), retcode, stdout + stderr) + + if not quiet: + print 'Imported {}'.format(storepath) + + if verbose: + print stdout + stderr + except: + print 'Error: corrupted line: {}'.format(row) + +if __name__ == '__main__': + main() -- cgit v1.2.3-59-g8ed1b